Free
0
4
Emberlyn
By Priya Shah
Emberlyn is a quiet, editorial portfolio template for wedding and portrait photographers, with a black-and-white hero slider, a handwritten about section, a staggered journal, a hover-preview collections list and a wedding date availability calendar.
Download Codebase
Commercial License<%# letstemplates: access=free category=portfolio %>
<!DOCTYPE html>
<html lang="en">
<head>
<script>
window.FontAwesomeConfig = { autoReplaceSvg: 'nest' };
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emberlyn | Tailwind CSS Portfolio Template</title>
<meta name="description" content="Emberlyn is a quiet, editorial Tailwind CSS portfolio template for wedding and portrait photographers, with a black-and-white hero slider, a handwritten about section, a staggered journal, a hover-preview collections list and a wedding date availability calendar.">
<meta name="keywords" content="Emberlyn, Tailwind CSS template, HTML website template, portfolio template, wedding photographer website, photography portfolio, elopement photographer, photo studio template, editorial portfolio">
<meta name="author" content="Let's templates">
<link rel="canonical" href="">
<meta property="og:type" content="product">
<meta property="og:site_name" content="Let's templates">
<meta property="og:locale" content="en_US">
<meta property="og:url" content="">
<meta property="og:title" content="Emberlyn | Tailwind CSS Portfolio Template">
<meta property="og:description" content="A quiet, editorial Tailwind CSS portfolio template for wedding photographers, with a black-and-white hero slider, a staggered journal and a date availability calendar.">
<meta property="og:image" content="">
<meta property="og:image:alt" content="Emberlyn template preview">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="">
<meta name="twitter:title" content="Emberlyn | Tailwind CSS Portfolio Template">
<meta name="twitter:description" content="A quiet, editorial Tailwind CSS portfolio template for wedding photographers, with a black-and-white hero slider, a staggered journal and a date availability calendar.">
<meta name="twitter:image" content="">
<meta name="twitter:image:alt" content="Emberlyn template preview">
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Mrs+Saint+Delafield&family=Playfair+Display:wght@400;500&family=Poppins:wght@300;400;500&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: { paper: '#f7f6f2', sand: '#efede8', sage: { DEFAULT: '#5f6c5f', deep: '#4b564b' }, ink: '#232323', mute: '#707070' },
fontFamily: { sans: ['Poppins', 'sans-serif'], serif: ['Playfair Display', 'serif'], caps: ['Cormorant Garamond', 'serif'], script: ['Mrs Saint Delafield', 'cursive'] }
}
}
}
</script>
<style>
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: 180ms ease-out both page-fade-out; }
::view-transition-new(root) { animation: 220ms ease-in both page-fade-in; }
@keyframes page-fade-out { to { opacity: 0; } }
@keyframes page-fade-in { from { opacity: 0; } }
html { scroll-behavior: smooth; }
[hidden] { display: none !important; }
.slide { transition: opacity .7s ease, transform 6s ease; }
.slide:not(.is-active) { opacity: 0; }
.slide.is-active { transform: scale(1.04); }
.notch { clip-path: polygon(0 0, 100% 0, 100% calc(100% - 1.25rem), calc(50% + 1.25rem) calc(100% - 1.25rem), 50% 100%, calc(50% - 1.25rem) calc(100% - 1.25rem), 0 calc(100% - 1.25rem)); }
.reveal-ready [data-reveal] { opacity: 0; transform: translateY(24px); transition: opacity .7s ease, transform .7s ease; transition-delay: var(--reveal-delay, 0ms); }
.reveal-ready [data-reveal].is-visible { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.slide { transition: none; }
.slide.is-active { transform: none; }
::view-transition-old(root), ::view-transition-new(root) { animation: none; }
}
</style>
</head>
<body class="bg-paper font-sans font-light text-ink antialiased">
<% active_page = "index" %>
<% left_nav = { "about" => "meet us", "story" => "our story", "services" => "services" } %>
<% right_nav = { "portfolio" => "portfolio", "blog" => "the journal", "contact" => "hire us" } %>
<!-- block:start navbar -->
<header class="absolute inset-x-0 top-0 z-40 text-white">
<nav class="mx-auto flex max-w-6xl items-center justify-between px-5 py-6 md:px-10 lg:grid lg:grid-cols-[1fr_auto_1fr] lg:gap-16" aria-label="Main">
<ul class="hidden items-center justify-end gap-12 text-[13px] lg:flex">
<% left_nav.each do |slug, label| %>
<% active = active_page == slug %>
<li><a href="<%= slug %>.html" <%= 'aria-current="page"'.html_safe if active %> class="border-b pb-1 transition <%= active ? 'border-white' : 'border-transparent text-white/80 hover:text-white' %>"><%= label %></a></li>
<% end %>
</ul>
<a href="index.html" class="grid h-16 w-16 place-items-center rounded-full border border-white/70 font-serif text-2xl italic tracking-tight" aria-label="Emberlyn home">EM</a>
<ul class="hidden items-center gap-12 text-[13px] lg:flex">
<% right_nav.each do |slug, label| %>
<% active = active_page == slug %>
<li><a href="<%= slug %>.html" <%= 'aria-current="page"'.html_safe if active %> class="border-b pb-1 transition <%= active ? 'border-white' : 'border-transparent text-white/80 hover:text-white' %>"><%= label %></a></li>
<% end %>
</ul>
<button type="button" data-menu-toggle aria-controls="mobile-menu" aria-expanded="false" aria-label="Open menu" class="grid h-11 w-11 place-items-center border border-white/60 lg:hidden"><i class="fa-solid fa-bars" aria-hidden="true"></i></button>
</nav>
<div id="mobile-menu" class="hidden px-5 pb-6 lg:hidden">
<ul class="grid gap-1 bg-paper p-4 text-ink shadow-xl">
<% { "index" => "home" }.merge(left_nav).merge(right_nav).each do |slug, label| %>
<% active = active_page == slug %>
<li><a href="<%= slug %>.html" <%= 'aria-current="page"'.html_safe if active %> class="block px-3 py-2.5 font-caps text-xl <%= active ? 'bg-sand font-semibold' : 'hover:bg-sand' %>"><%= label %></a></li>
<% end %>
</ul>
</div>
</header>
<!-- block:end navbar -->
<% slides = [["1460978812857-470ed1c77af0", "Couple kissing on a windy shore, the bride's veil lifting in black and white"], ["1537633552985-df8429e8048b", "Bride and groom embracing by the sea, her long veil trailing on the pebbles"], ["1532712938310-34cb3982ef74", "Newlyweds walking hand in hand across a hilltop at dusk"]] %>
<!-- block:start hero -->
<section class="notch relative h-[92vh] max-h-[52rem] min-h-[34rem] overflow-hidden bg-ink text-white" data-slider aria-roledescription="carousel" aria-label="Featured weddings">
<% slides.each_with_index do |(photo, alt), index| %>
<img src="https://images.unsplash.com/photo-<%= photo %>?auto=format&fit=crop&w=1900&q=80" alt="<%= alt %>" data-slide class="slide absolute inset-0 h-full w-full object-cover grayscale <%= 'is-active' if index.zero? %>" <%= 'aria-hidden="true"'.html_safe unless index.zero? %>>
<% end %>
<div class="absolute inset-0 bg-black/45" aria-hidden="true"></div>
<div class="relative flex h-full flex-col items-center justify-center px-6 text-center">
<p class="text-[11px] uppercase tracking-[0.6em] text-white/85">wedding photography</p>
<h1 class="mt-4 font-serif text-5xl md:text-7xl">Elias Marlowe</h1>
<p class="mt-6 max-w-md text-sm leading-relaxed text-white/85 md:text-base">Keep the brightest days of your life in more than memory. Keep them in print.</p>
<a href="portfolio.html" class="mt-9 border border-white/50 bg-white/15 px-7 py-2.5 text-[11px] uppercase tracking-[0.4em] backdrop-blur transition hover:bg-white hover:text-ink">Discover</a>
</div>
<button type="button" data-slider-prev class="group absolute left-0 top-1/2 hidden w-40 -translate-y-1/2 text-left text-[11px] tracking-widest text-white/80 md:block" aria-label="Previous wedding"><span class="block h-px w-full bg-white/70 transition-all group-hover:w-32"></span><span class="mt-2 block pl-16">Prev</span></button>
<button type="button" data-slider-next class="group absolute right-0 top-1/2 hidden w-40 -translate-y-1/2 text-right text-[11px] tracking-widest text-white/80 md:block" aria-label="Next wedding"><span class="ml-auto block h-px w-full bg-white/70 transition-all group-hover:w-32"></span><span class="mt-2 block pr-16">Next</span></button>
<div class="absolute inset-x-0 bottom-10 flex items-end justify-between px-5 text-[11px] md:px-10">
<div class="flex gap-2" role="group" aria-label="Choose a slide">
<% slides.each_index do |index| %>
<button type="button" data-slider-dot="<%= index %>" aria-label="Show wedding <%= index + 1 %>" aria-pressed="<%= index.zero? %>" class="h-px w-8 bg-white/40 transition aria-pressed:w-14 aria-pressed:bg-white"></button>
<% end %>
</div>
<a href="#welcome" class="hidden text-white/80 md:block">Scroll down</a>
<div class="flex items-center gap-5">
<span class="text-white/80">Follow</span>
<% [["fa-instagram", "Instagram", "https://www.instagram.com"], ["fa-facebook-f", "Facebook", "https://www.facebook.com"], ["fa-pinterest-p", "Pinterest", "https://www.pinterest.com"]].each do |icon, label, url| %>
<a href="<%= url %>" aria-label="<%= label %>" class="text-sm transition hover:text-white/60"><i class="fa-brands <%= icon %>" aria-hidden="true"></i></a>
<% end %>
</div>
</div>
</section>
<!-- block:end hero -->
<!-- block:start about -->
<section id="welcome" class="relative">
<div class="mx-auto h-24 w-px bg-ink/60 md:h-28" aria-hidden="true"></div>
<p class="mx-auto max-w-3xl px-6 pb-20 pt-12 text-center font-caps text-base font-medium uppercase leading-relaxed tracking-[0.25em] md:pb-28 md:text-lg" data-reveal>
We exist to share the joy of a celebration. Every story we tell is shaped around yours: tender or playful, wild or quietly classic. We look after every small detail so you can stay inside the moments that matter.
</p>
<div class="relative">
<div class="absolute inset-x-0 bottom-0 top-28 bg-white md:top-36" aria-hidden="true"></div>
<div class="relative mx-auto grid max-w-6xl gap-10 px-5 pb-20 md:grid-cols-[0.8fr_1fr] md:gap-16 md:px-10 md:pb-28">
<img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=900&q=80" alt="Portrait of photographer Elias Marlowe in black and white" class="aspect-[3/4] w-full object-cover grayscale" loading="lazy" data-reveal>
<div class="md:pt-6">
<h2 class="font-script text-6xl leading-none md:text-8xl">Hi, I'm Elias</h2>
<div class="relative mt-16 border-l border-ink/60 pb-10 pl-8 md:ml-10 md:mt-24 md:pl-12">
<p class="max-w-md text-sm leading-[2] text-ink/80">For ten years I worked in newsrooms, chasing deadlines across three countries. A friend's wedding in Porto changed the plan: I came home with four hundred photos and a new job. Today I photograph around thirty weddings a year, mostly couples who want honest pictures that still feel like them in thirty years.</p>
<a href="contact.html" class="absolute -bottom-5 -left-5 bg-sage px-9 py-3 text-[11px] uppercase tracking-[0.4em] text-white transition hover:bg-sage-deep md:-left-14">Contact</a>
</div>
</div>
</div>
</div>
</section>
<!-- block:end about -->
<% stories = [
["03 · 02 · 2026", "Engagement", "Lena and Tomas, sunset session", "A breezy evening on the cliffs of the Algarve: four hours, one picnic and more than six hundred frames of pure, easy joy.", "1532712938310-34cb3982ef74", "Couple embracing on a hilltop at golden hour", false],
["14 · 06 · 2025", "Wedding", "Amelia and Noah", "A June wedding on a vineyard terrace. They decided to marry three months after meeting, and every photo shows why.", "1519741497674-611481863552", "Bride and groom backlit by the evening sun", true],
["21 · 09 · 2025", "Wedding", "Love is in the air", "I never feel like I'm at work at a wedding. Here's a celebration that turned strangers into one big family by midnight.", "1583939003579-730e3918a45a", "Guests throwing confetti as the couple kisses", true],
["28 · 02 · 2026", "Couples · Engagement", "Inès and Rafael", "What a wonderfully easy-going pair these two are.", "1591604466107-ec97de577aff", "Couple smiling together beside an autumn lake", false]
] %>
<!-- block:start blog -->
<section class="bg-sand">
<div class="mx-auto max-w-6xl px-5 py-20 md:px-10 md:py-28">
<div class="text-center">
<p class="text-[11px] uppercase tracking-[0.5em] text-mute">the journal</p>
<h2 class="mt-3 font-serif text-4xl md:text-5xl">Recent stories</h2>
</div>
<div class="mt-14 grid gap-10 md:grid-cols-2 md:gap-x-16">
<% stories.each_with_index do |(date, category, title, excerpt, photo, alt, tall), index| %>
<article class="group <%= 'md:mt-24' if index.odd? %> <%= 'md:-mt-24' if index == 2 %>" data-reveal style="--reveal-delay: <%= (index % 2) * 120 %>ms">
<a href="amelia-and-noah.html" class="block">
<span class="block overflow-hidden"><img src="https://images.unsplash.com/photo-<%= photo %>?auto=format&fit=crop&w=1000&q=80" alt="<%= alt %>" class="<%= tall ? 'aspect-[4/5]' : 'aspect-[4/3]' %> w-full object-cover transition duration-700 group-hover:scale-105" loading="lazy"></span>
<span class="relative -mt-10 ml-6 block bg-white p-7 shadow-sm md:ml-10">
<span class="flex flex-wrap justify-between gap-2 text-[10px] uppercase tracking-[0.3em] text-mute"><span><%= date %></span><span><b class="font-medium text-ink">categories</b> <%= category %></span></span>
<span class="mt-4 block font-serif text-xl"><%= title %></span>
<span class="mt-3 block text-xs leading-[1.9] text-ink/70"><%= excerpt %></span>
</span>
</a>
</article>
<% end %>
</div>
<div class="mt-16 text-center"><a href="blog.html" class="inline-block bg-sage px-10 py-3 text-[11px] uppercase tracking-[0.4em] text-white transition hover:bg-sage-deep">View more</a></div>
</div>
</section>
<!-- block:end blog -->
<% collections = [["Weddings", "48 stories", "Full days, from getting ready to the last dance", "1511285560929-80b456fea0bc"], ["Elopements", "21 stories", "Just the two of you, somewhere wild", "1537633552985-df8429e8048b"], ["Engagements", "33 stories", "Easy sessions before the big day", "1520854221256-17451cc331bf"], ["Details", "60+ sets", "Rings, flowers, tables and handwritten vows", "1515934751635-c81c6bc9a2d8"]] %>
<!-- block:start gallery -->
<section class="relative bg-white" data-hover-list>
<div class="mx-auto max-w-6xl px-5 py-20 md:px-10 md:py-28">
<div class="flex flex-wrap items-end justify-between gap-6">
<h2 class="font-serif text-4xl md:text-5xl">Collections</h2>
<a href="portfolio.html" class="text-[11px] uppercase tracking-[0.4em] text-mute hover:text-ink">full portfolio</a>
</div>
<ol class="mt-12 border-t border-ink/15">
<% collections.each_with_index do |(title, count, text, photo), index| %>
<li class="border-b border-ink/15" data-hover-item data-image="https://images.unsplash.com/photo-<%= photo %>?auto=format&fit=crop&w=600&q=80">
<a href="portfolio.html" class="group grid items-center gap-4 py-7 md:grid-cols-[4rem_1fr_1fr_auto] md:py-9">
<span class="font-caps text-lg text-mute">0<%= index + 1 %></span>
<span class="font-serif text-3xl transition group-hover:translate-x-2 md:text-4xl"><%= title %></span>
<span class="text-sm text-ink/70"><%= text %></span>
<span class="text-[11px] uppercase tracking-[0.3em] text-mute"><%= count %></span>
<img src="https://images.unsplash.com/photo-<%= photo %>?auto=format&fit=crop&w=600&q=80" alt="" class="aspect-[16/9] w-full object-cover md:hidden" loading="lazy">
</a>
</li>
<% end %>
</ol>
</div>
<img data-hover-preview src="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80" alt="" aria-hidden="true" class="pointer-events-none fixed left-0 top-0 z-30 hidden w-64 opacity-0 shadow-2xl transition-opacity duration-300 md:block">
</section>
<!-- block:end gallery -->
<% months = (0...6).map { |offset| Date.new(2027, 4, 1) >> offset } %>
<% booked = ->(day) { (day.saturday? && (day.day % 3 != 0)) || ((day.day * day.month) % 11).zero? } %>
<!-- block:start contact -->
<section class="bg-paper" data-availability>
<div class="mx-auto grid max-w-6xl gap-12 px-5 py-20 md:px-10 md:py-28 lg:grid-cols-[1fr_1.15fr] lg:items-center">
<div>
<p class="text-[11px] uppercase tracking-[0.5em] text-mute">season 2027</p>
<h2 class="mt-3 font-serif text-4xl leading-tight md:text-5xl">Is your date still free?</h2>
<p class="mt-6 max-w-md text-sm leading-[2] text-ink/75">I take one wedding a day and around thirty a year. Pick your date to see if it's open, then hold it for seven days while we talk.</p>
<div class="mt-10 border-l border-ink/40 pl-6" aria-live="polite">
<p data-availability-summary class="font-serif text-2xl">Choose a day in the calendar</p>
<p data-availability-note class="mt-2 text-sm text-mute">Saturdays go first, so book early for summer.</p>
</div>
<a href="contact.html" data-availability-cta class="mt-8 inline-block bg-sage px-9 py-3 text-[11px] uppercase tracking-[0.4em] text-white transition hover:bg-sage-deep">Hold my date</a>
</div>
<div class="bg-white p-6 shadow-sm md:p-10">
<div class="flex items-center justify-between">
<button type="button" data-month-prev class="grid h-10 w-10 place-items-center border border-ink/20 transition hover:border-ink disabled:opacity-30" aria-label="Previous month"><i class="fa-solid fa-chevron-left text-xs" aria-hidden="true"></i></button>
<p class="font-caps text-2xl font-medium" data-month-label aria-live="polite"><%= months.first.strftime("%B %Y") %></p>
<button type="button" data-month-next class="grid h-10 w-10 place-items-center border border-ink/20 transition hover:border-ink disabled:opacity-30" aria-label="Next month"><i class="fa-solid fa-chevron-right text-xs" aria-hidden="true"></i></button>
</div>
<% months.each_with_index do |month, index| %>
<div data-month="<%= month.strftime("%B %Y") %>" <%= 'hidden' unless index.zero? %> class="mt-8">
<div class="grid grid-cols-7 gap-1 text-center text-[10px] uppercase tracking-[0.2em] text-mute" aria-hidden="true">
<% %w[Mo Tu We Th Fr Sa Su].each do |name| %><span><%= name %></span><% end %>
</div>
<div class="mt-3 grid grid-cols-7 gap-1">
<% (month.cwday - 1).times do %><span aria-hidden="true"></span><% end %>
<% (month..(month.next_month - 1)).each do |day| %>
<% taken = booked.call(day) %>
<button type="button" data-day="<%= day.strftime("%A, %-d %B %Y") %>" <%= 'disabled' if taken %> aria-pressed="false" aria-label="<%= day.strftime("%A %-d %B") %><%= taken ? ', booked' : ', available' %>" class="aspect-square text-sm transition enabled:hover:bg-sand disabled:cursor-not-allowed disabled:text-ink/25 disabled:line-through aria-pressed:bg-ink aria-pressed:text-white <%= 'font-medium' if day.saturday? && !taken %>"><%= day.day %></button>
<% end %>
</div>
</div>
<% end %>
<p class="mt-6 flex flex-wrap gap-6 text-xs text-mute"><span class="flex items-center gap-2"><span class="h-3 w-3 bg-ink"></span>your pick</span><span class="flex items-center gap-2"><span class="text-ink/25 line-through">12</span>booked</span></p>
</div>
</div>
</section>
<!-- block:end contact -->
<% reviews = [
["I genuinely can't find the words for how wonderful Elias was. He put us at ease from our first coffee, and on the day he caught every moment we missed. Our photos are out of this world and tell our story perfectly.", "Jessa & Ethan"],
["We hate being photographed. Somehow we forgot the camera was there within ten minutes, and the pictures look like us on our best day.", "Priya & Daniel"],
["Our album arrived on our first anniversary. We both cried at page three. Worth every cent and then some.", "Marta & Joaquim"]
] %>
<!-- block:start reviews -->
<section class="bg-white" data-carousel aria-roledescription="carousel" aria-label="Kind words from couples">
<div class="mx-auto grid max-w-6xl items-center px-5 py-20 md:grid-cols-[1fr_1.1fr] md:px-10 md:py-28">
<img src="https://images.unsplash.com/photo-1550005809-91ad75fb315f?auto=format&fit=crop&w=1000&q=80" alt="Bride holding a soft peach bouquet beside the groom" class="aspect-[4/5] w-full object-cover md:aspect-[5/6]" loading="lazy">
<div class="relative -mt-16 mx-4 bg-paper px-7 pb-10 pt-6 shadow-sm md:-ml-24 md:mt-24 md:px-12">
<h2 class="-mt-16 text-right font-script text-6xl md:-mt-24 md:text-7xl">Kind words</h2>
<div class="mt-6">
<% reviews.each_with_index do |(quote, couple), index| %>
<figure data-carousel-slide <%= 'hidden' unless index.zero? %> class="text-center">
<span class="font-serif text-4xl text-ink/40" aria-hidden="true">“</span>
<blockquote class="mt-2 text-sm leading-[2] text-ink/80"><%= quote %></blockquote>
<figcaption class="mt-6 text-[11px] uppercase tracking-[0.4em]">– <%= couple %></figcaption>
</figure>
<% end %>
</div>
<div class="mt-8 flex items-center justify-center gap-6">
<button type="button" data-carousel-prev class="grid h-10 w-10 place-items-center transition hover:bg-sand" aria-label="Previous review"><i class="fa-solid fa-caret-left" aria-hidden="true"></i></button>
<span class="h-12 w-px rotate-[25deg] bg-ink/40" aria-hidden="true"></span>
<button type="button" data-carousel-next class="grid h-10 w-10 place-items-center transition hover:bg-sand" aria-label="Next review"><i class="fa-solid fa-caret-right" aria-hidden="true"></i></button>
</div>
<p class="mt-4 text-center text-xs text-mute"><span data-carousel-index>1</span> / <%= reviews.size %></p>
</div>
</div>
</section>
<!-- block:end reviews -->
<!-- block:start footer -->
<footer class="bg-sand">
<ul class="grid grid-cols-3 md:grid-cols-6" aria-label="Recent photos on Instagram">
<% ["1465495976277-4387d4b0b4c6", "1525258946800-98cfd641d0de", "1469371670807-013ccf25f16a", "1507504031003-b417219a0fde", "1519225421980-715cb0215aed", "1494955870715-979ca4f13bf0"].each do |photo| %>
<li><a href="https://www.instagram.com" class="group relative block overflow-hidden" aria-label="Open on Instagram"><img src="https://images.unsplash.com/photo-<%= photo %>?auto=format&fit=crop&w=400&q=80" alt="" class="aspect-square w-full object-cover transition duration-500 group-hover:scale-110" loading="lazy"><span class="absolute inset-0 grid place-items-center bg-ink/40 text-white opacity-0 transition group-hover:opacity-100"><i class="fa-brands fa-instagram text-xl" aria-hidden="true"></i></span></a></li>
<% end %>
</ul>
<div class="mx-auto max-w-6xl px-5 pt-16 md:px-10">
<p class="select-none text-center font-script text-[24vw] leading-[0.9] text-ink md:text-[11rem]" aria-hidden="true">Emberlyn</p>
<div class="mt-10 flex flex-col items-center gap-6 border-t border-ink/15 py-8 text-xs md:flex-row md:justify-between">
<ul class="flex flex-wrap justify-center gap-x-8 gap-y-2 uppercase tracking-[0.3em]">
<% left_nav.merge(right_nav).each do |slug, label| %><li><a href="<%= slug %>.html" class="hover:text-sage"><%= label %></a></li><% end %>
</ul>
<p class="text-mute">© <span data-current-year>2026</span> Emberlyn Studio · Porto & everywhere</p>
</div>
</div>
</footer>
<!-- block:end footer -->
<script>
document.querySelectorAll('[data-menu-toggle]').forEach((button) => {
const menu = document.getElementById(button.getAttribute('aria-controls'));
if (!menu) return;
button.addEventListener('click', () => {
const open = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!open));
menu.classList.toggle('hidden', open);
});
});
document.querySelectorAll('[data-slider]').forEach((slider) => {
const slides = [...slider.querySelectorAll('[data-slide]')];
const dots = [...slider.querySelectorAll('[data-slider-dot]')];
let current = 0;
let timer;
const show = (index) => {
current = (index + slides.length) % slides.length;
slides.forEach((slide, i) => {
slide.classList.toggle('is-active', i === current);
slide.toggleAttribute('aria-hidden', i !== current);
});
dots.forEach((dot, i) => dot.setAttribute('aria-pressed', String(i === current)));
};
const restart = () => {
clearInterval(timer);
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) timer = setInterval(() => show(current + 1), 6000);
};
slider.querySelector('[data-slider-prev]')?.addEventListener('click', () => { show(current - 1); restart(); });
slider.querySelector('[data-slider-next]')?.addEventListener('click', () => { show(current + 1); restart(); });
dots.forEach((dot, i) => dot.addEventListener('click', () => { show(i); restart(); }));
restart();
});
document.querySelectorAll('[data-carousel]').forEach((carousel) => {
const slides = [...carousel.querySelectorAll('[data-carousel-slide]')];
const index = carousel.querySelector('[data-carousel-index]');
let current = 0;
const show = (next) => {
current = (next + slides.length) % slides.length;
slides.forEach((slide, i) => { slide.hidden = i !== current; });
if (index) index.textContent = current + 1;
};
carousel.querySelector('[data-carousel-prev]')?.addEventListener('click', () => show(current - 1));
carousel.querySelector('[data-carousel-next]')?.addEventListener('click', () => show(current + 1));
});
document.querySelectorAll('[data-hover-list]').forEach((list) => {
const preview = list.querySelector('[data-hover-preview]');
if (!preview || !matchMedia('(hover: hover)').matches) return;
list.querySelectorAll('[data-hover-item]').forEach((item) => {
item.addEventListener('mouseenter', () => { preview.src = item.dataset.image; preview.style.opacity = '1'; });
item.addEventListener('mouseleave', () => { preview.style.opacity = '0'; });
item.addEventListener('mousemove', (event) => { preview.style.transform = `translate(${event.clientX + 28}px, ${event.clientY - 90}px) rotate(-3deg)`; });
});
});
document.querySelectorAll('[data-availability]').forEach((root) => {
const months = [...root.querySelectorAll('[data-month]')];
const label = root.querySelector('[data-month-label]');
const prev = root.querySelector('[data-month-prev]');
const next = root.querySelector('[data-month-next]');
const summary = root.querySelector('[data-availability-summary]');
const note = root.querySelector('[data-availability-note]');
let current = 0;
const showMonth = (index) => {
current = Math.max(0, Math.min(months.length - 1, index));
months.forEach((month, i) => { month.hidden = i !== current; });
label.textContent = months[current].dataset.month;
prev.disabled = current === 0;
next.disabled = current === months.length - 1;
};
prev.addEventListener('click', () => showMonth(current - 1));
next.addEventListener('click', () => showMonth(current + 1));
root.addEventListener('click', (event) => {
const day = event.target.closest('[data-day]');
if (!day || day.disabled) return;
root.querySelectorAll('[data-day]').forEach((other) => other.setAttribute('aria-pressed', String(other === day)));
summary.textContent = `${day.dataset.day} is free`;
note.textContent = 'Lovely choice. Hold it now and I will keep it for you for seven days.';
});
showMonth(0);
});
document.querySelectorAll('[data-filter-group]').forEach((group) => {
const items = document.querySelectorAll(group.dataset.filterGroup);
group.querySelectorAll('[data-filter]').forEach((chip) => {
chip.addEventListener('click', () => {
group.querySelectorAll('[data-filter]').forEach((other) => other.setAttribute('aria-pressed', String(other === chip)));
items.forEach((item) => { item.hidden = chip.dataset.filter !== 'all' && item.dataset.role !== chip.dataset.filter; });
});
});
});
document.querySelectorAll('[data-lightbox]').forEach((dialog) => {
const image = dialog.querySelector('img');
const caption = dialog.querySelector('[data-lightbox-caption]');
document.querySelectorAll('[data-lightbox-open]').forEach((button) => {
button.addEventListener('click', () => {
image.src = button.dataset.full;
image.alt = button.querySelector('img')?.alt || '';
caption.textContent = button.dataset.caption;
dialog.showModal();
});
});
dialog.addEventListener('click', (event) => { if (event.target === dialog) dialog.close(); });
});
document.querySelectorAll('[data-contact-form]').forEach((form) => {
form.addEventListener('submit', (event) => {
event.preventDefault();
const status = form.querySelector('[data-form-status]');
if (!form.checkValidity()) {
status.textContent = form.dataset.error;
form.querySelector(':invalid')?.focus();
return;
}
status.textContent = form.dataset.success;
form.reset();
});
});
document.querySelectorAll('[data-current-year]').forEach((el) => { el.textContent = new Date().getFullYear(); });
(() => {
const items = document.querySelectorAll('[data-reveal]');
const skip = navigator.webdriver || !('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!items.length || skip) return;
document.documentElement.classList.add('reveal-ready');
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
});
}, { rootMargin: '0px 0px -10% 0px' });
items.forEach((item) => observer.observe(item));
})();
</script>
</body>
</html>
Unlimited access to all templates
Unlock our entire library of premium UI kits, dashboards, and landing pages. Renews automatically, cancel anytime.
$20
/ 3 months
or $60 / year
Get Unlimited Access