/* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== E D I T Y O U R P H O T O S ================================================================== These are the three big pictures at the very top of the site. Landscape photographs work best -- they are shown wide on a computer and cropped taller on a phone, so keep the important part near the middle. To get a web address for a picture: add an Image element anywhere in Carrd, upload your photograph, then copy the address it gives you and paste it between the quote marks below. Leave a line as "" and the picture that is there now stays. ------------------------------------------------------------------ IF A PICTURE LOOKS BADLY CROPPED ON A PHONE ------------------------------------------------------------------ A phone screen is tall and narrow, so a wide photograph has to lose its left and right edges to fill the frame. By default the middle is kept. If the part that matters is not in the middle, you can say so per slide. Further down this file each slide has a line like: style="--focus-m:50% 50%" The first number is left-to-right, the second is top-to-bottom: 0% = keep the left edge / the top 50% = keep the middle (the default) 100% = keep the right edge / the bottom So a bouquet sitting on the left of the photo wants 20% 50% . Someone's face near the top wants 50% 25% . Only phones use this; the computer version never changes. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ heroBlooms: "", /* slide 1 -- "Made to say something." */ heroExperiences: "", /* slide 2 -- "Gather around flowers." */ heroEvents: "" /* slide 3 -- "Let\'s create something..." */ });
Skip to content
Floral studio · Đà Nẵng · Việt Nam
(function(){ var root=document.getElementById('hera'); if(!root) return; var D=document.documentElement; /* note: the .h-js class and the scroll-reveal engine are owned by 00-HEAD.html, so every embed shares one instance */ var reduce=window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* ---- CAROUSEL ----------------------------------------------- Auto-rotates, but stops for anyone who shows an intention to read: hover, keyboard focus, a tab going to the background, or a reduced-motion preference (which never starts it at all). */ var car=document.getElementById('h-car'), slidesBox=document.getElementById('h-slides'), slides=[].slice.call(slidesBox.querySelectorAll('.h-slide')), dots=[].slice.call(car.querySelectorAll('.h-dot')), playBtn=document.getElementById('h-cplay'), i=0,timer=null,paused=reduce,DUR=6500; function show(n,userDriven){ i=(n+slides.length)%slides.length; slides.forEach(function(s,k){ s.classList.toggle('is-on',k===i); }); dots.forEach(function(d,k){ d.setAttribute('aria-current',k===i?'true':'false'); }); /* only announce when a person moved it; announcing every auto tick would talk over whatever they are actually reading */ slidesBox.setAttribute('aria-live',userDriven?'polite':'off'); } function start(){ if(paused||reduce) return; stop(); timer=setInterval(function(){ show(i+1,false); },DUR); } function stop(){ if(timer){ clearInterval(timer); timer=null; } } function setPaused(p){ paused=p; car.classList.toggle('is-paused',p); playBtn.setAttribute('aria-label',p?'Play slideshow':'Pause slideshow'); if(p) stop(); else start(); } dots.forEach(function(d){ d.addEventListener('click',function(){ show(+d.dataset.go,true); if(!paused){ start(); } }); }); document.getElementById('h-cprev').addEventListener('click',function(){ show(i-1,true); if(!paused){ start(); } }); document.getElementById('h-cnext').addEventListener('click',function(){ show(i+1,true); if(!paused){ start(); } }); playBtn.addEventListener('click',function(){ setPaused(!paused); }); /* hover / focus hold -- never the only way to stop it */ car.addEventListener('mouseenter',function(){ if(!paused) stop(); }); car.addEventListener('mouseleave',function(){ if(!paused) start(); }); car.addEventListener('focusin',function(){ if(!paused) stop(); }); car.addEventListener('focusout',function(){ if(!paused && !car.contains(document.activeElement)) start(); }); document.addEventListener('visibilitychange',function(){ if(document.hidden) stop(); else if(!paused) start(); }); /* arrow keys when the carousel has focus */ car.addEventListener('keydown',function(e){ if(e.key==='ArrowLeft'){ e.preventDefault(); show(i-1,true); } else if(e.key==='ArrowRight'){ e.preventDefault(); show(i+1,true); } }); /* swipe */ var sx=null,sy=null; slidesBox.addEventListener('touchstart',function(e){ sx=e.touches[0].clientX; sy=e.touches[0].clientY; },{passive:true}); slidesBox.addEventListener('touchend',function(e){ if(sx===null) return; var dx=e.changedTouches[0].clientX-sx, dy=e.changedTouches[0].clientY-sy; if(Math.abs(dx)>44 && Math.abs(dx)>Math.abs(dy)){ show(i+(dx<0?1:-1),true); } sx=sy=null; },{passive:true}); if(reduce){ car.classList.add('is-paused'); playBtn.setAttribute('aria-label','Play slideshow'); } show(0,false); start(); })();

Studio

Composed for the moment.

In season, by hand, one at a time.

Signature blooms

Our Bestsellers

A few of the arrangements our customers come back for.

/* -- nothing below here needs editing ------------------------- */ (function(){ var sec = document.getElementById('bestsellers'), vp = document.getElementById('h-bs-vp'), track = document.getElementById('h-bs-track'), count = document.getElementById('h-bs-count'), bar = document.getElementById('h-bs-bar'), prev = document.getElementById('h-bs-prev'), next = document.getElementById('h-bs-next'), foot = sec && sec.querySelector('.h-best-foot'); if(!track) return; var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* THIS COMPONENT HOLDS NO PRODUCT DATA. It asks the data layer for the bestsellers and draws them. It does not know where they came from, and it never will -- which is what lets the product source move later without this file changing. */ var items = []; var page = 0, per = 0, pages = 1, built = false; function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){ return {'&':'\x26amp;','<':'\x26lt;','>':'\x26gt;','"':'\x26quot;'}[c]; }); } var PLUS = '\x3csvg width="15" height="15" viewBox="0 0 15 15" fill="none" ' + 'stroke="currentColor" stroke-width="1.5" aria-hidden="true">' + '\x3cpath d="M7.5 1v13M1 7.5h13" stroke-linecap="round"/>\x3c/svg>'; var TICK = '\x3csvg width="15" height="15" viewBox="0 0 15 15" fill="none" ' + 'stroke="currentColor" stroke-width="1.7" aria-hidden="true">' + '\x3cpath d="M2 7.8 6 11.5 13 3.5" stroke-linecap="round" ' + 'stroke-linejoin="round"/>\x3c/svg>'; /* The card is two real controls side by side, the same shape the catalogue uses: the card opens the piece, the circle drops it straight into the order. Siblings, not nested -- a button inside a button is invalid and sends the click to the wrong one. */ function card(p,n){ var num = ('0'+(n+1)).slice(-2); /* products carry a LIST, images[] -- reading a singular "image" here meant every card fell through to the grey placeholder. */ var pic = (p.images && p.images[0]) || p.image || ''; var shot = pic ? '\x3cimg src="'+esc(pic)+'" alt="'+esc(p.alt||p.name)+'" loading="lazy">' : '\x3cspan class="h-bs-ph">\x3ci aria-hidden="true">\x3c/i>' + '\x3cspan>Bouquet '+num+' \u00b7 3:4\x3c/span>\x3c/span>'; return '\x3cdiv class="h-bs-card h-rev'+(built?' is-in':'')+'"' + ' style="--d:'+((n%8)*55)+'ms">' + '\x3cbutton class="h-bs-open" type="button" data-id="'+esc(p.id)+'"' + ' aria-label="'+esc(p.name)+' \u2014 see details">' + '\x3cspan class="h-bs-plate">' + '\x3cspan class="h-bs-shot'+(built?'':' h-wipe')+'">'+shot+'\x3c/span>' + '\x3cspan class="h-bs-meta">' + '\x3cspan>' + '\x3cspan class="h-bs-num">'+num+'\x3c/span>' + '\x3cspan class="h-bs-name">'+esc(p.name)+'\x3c/span>' + (p.tone ? '\x3cspan class="h-bs-sub">'+esc(p.tone)+'\x3c/span>' : '') + '\x3c/span>' + '\x3c/span>' + '\x3c/span>' + '\x3c/button>' + '\x3cbutton class="h-bs-go" type="button" data-add="'+esc(p.id)+'"' + ' aria-label="Add '+esc(p.name)+' to your order">'+PLUS+'\x3c/button>' + '\x3c/div>'; } /* One handler for the whole strip: the cards are written with innerHTML and rebuilt whenever the number per page changes, so listeners attached to individual cards would not survive. */ function find(id){ var hit = null; items.forEach(function(x){ if(String(x.id) === String(id)) hit = x; }); return hit; } track.addEventListener('click', function(e){ var addBtn = e.target.closest('.h-bs-go'); if(addBtn){ e.preventDefault(); var p = find(addBtn.getAttribute('data-add')); if(p) quickAdd(p, addBtn); return; } var openBtn = e.target.closest('.h-bs-open'); if(openBtn){ var q = find(openBtn.getAttribute('data-id')); /* the product view lives in the Blooms catalogue embed. If that is not on the page, the + still works and this does nothing rather than throwing. */ if(q && typeof window.HERA_openProduct === 'function') window.HERA_openProduct(q); } }); /* Straight into the order, exactly as photographed and one of it -- the same shortcut the catalogue's + gives. Anyone who wants to choose colours or wrapping opens the piece instead. */ function quickAdd(p, btn){ if(!(window.HERA && HERA.bag)) return; HERA.bag.add({ productId: p.id, name: p.name, size: p.size, type: p.type, arranged: p.arranged, image: (p.images && p.images[0]) || p.image || '', price: p.price, priceFrom: p.priceFrom, toneList: [], tone: '', wrapStyle: p.wrapping || '', wrapColour: p.wrapColour || '', qty: 1, card: '', special: '' }); btn.innerHTML = TICK; btn.classList.add('is-added'); setTimeout(function(){ btn.innerHTML = PLUS; btn.classList.remove('is-added'); }, 1400); } /* how many cards belong on a page \u2014 read from CSS so the breakpoints are defined in exactly one place */ function perPage(){ var v = parseInt(getComputedStyle(sec).getPropertyValue('--h-bs-per'),10); return (v && v > 0) ? v : 1; } /* Rebuilt only when the count per page changes -- and never before the products arrive, or the empty page the first resize signal builds would latch this guard shut. */ function build(){ var n = perPage(); if(!items.length || (n === per && built && track.children.length)) return false; var firstItem = page * (per || n); /* keep the reader's place */ per = n; pages = Math.max(1, Math.ceil(items.length / per)); var html = ''; for(var p = 0; p < pages; p++){ html += '\x3cdiv class="h-best-page">'; for(var i = p*per; i < Math.min((p+1)*per, items.length); i++) html += card(items[i], i); html += '\x3c/div>'; } track.innerHTML = html; built = true; page = Math.min(pages - 1, Math.floor(firstItem / per)); return true; } function update(){ track.style.transform = 'translate3d(-' + (page * 100) + '%,0,0)'; count.textContent = ('0'+(page+1)).slice(-2) + ' / ' + ('0'+pages).slice(-2); bar.style.width = (100 / pages) + '%'; bar.style.left = (page * 100 / pages) + '%'; prev.disabled = page <= 0; next.disabled = page >= pages - 1; if(foot) foot.style.visibility = pages > 1 ? '' : 'hidden'; } function go(dir){ build(); /* last line of defence: if every resize signal was missed, correct it here */ var p = Math.max(0, Math.min(pages - 1, page + dir)); if(p === page){ update(); return; } page = p; update(); } prev.addEventListener('click', function(){ go(-1); }); next.addEventListener('click', function(){ go(1); }); vp.addEventListener('keydown', function(e){ if(e.key === 'ArrowLeft'){ e.preventDefault(); go(-1); } else if(e.key === 'ArrowRight'){ e.preventDefault(); go(1); } }); /* swipe -- the track no longer scrolls natively, so this is the only way through the pages on a phone */ var sx = null, sy = null; vp.addEventListener('touchstart', function(e){ sx = e.touches[0].clientX; sy = e.touches[0].clientY; }, {passive:true}); vp.addEventListener('touchend', function(e){ if(sx === null) return; var dx = e.changedTouches[0].clientX - sx, dy = e.changedTouches[0].clientY - sy; if(Math.abs(dx) > 44 && Math.abs(dx) > Math.abs(dy)) go(dx < 0 ? 1 : -1); sx = sy = null; }, {passive:true}); /* Rebuild when the number of cards per page changes. Three signals, because no single one is reliable on its own: a resize, the font finishing loading, and the element's own size changing. */ var rt; function recheck(){ clearTimeout(rt); rt = setTimeout(function(){ if(build()) update(); }, 120); } ['(min-width:640px)','(min-width:1024px)'].forEach(function(q){ var mq = window.matchMedia(q); if(mq.addEventListener) mq.addEventListener('change', recheck); else if(mq.addListener) mq.addListener(recheck); }); if(typeof ResizeObserver === 'function') new ResizeObserver(recheck).observe(vp); window.addEventListener('resize', recheck); /* Ask the data layer for the products, then draw. The callback is what makes a future non-instant source work unchanged: this file already waits to be handed a list rather than reaching for one. */ function onReady(all){ items = HERA.products.bestsellers(all); if(!items.length){ sec.style.display = 'none'; return; } build(); update(); if(window.__heraScan) window.__heraScan(); } /* Config may not have loaded yet -- if HERA.products isn't there, queue this request instead of crashing, and Config runs it for us the moment it's ready. */ if(window.HERA && window.HERA.products && typeof window.HERA.products.ready === 'function'){ HERA.products.ready(onReady); }else{ window.__heraProductsQueue = window.__heraProductsQueue || []; window.__heraProductsQueue.push(onReady); } })();
/* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== E D I T Y O U R P H O T O S ================================================================== The two pictures in the Monthly Flowers section. Both are the same size and both portrait -- taller than they are wide. They want flowers living in a real room, not a product shot. To get a web address for a picture: add an Image element anywhere in Carrd, upload your photograph, then copy the address it gives you and paste it between the quote marks below. Leave a line as "" and the picture that is there now stays. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ monthlyMain: "https://res.cloudinary.com/xf7oayn7/image/upload/f_auto,q_auto/IMG_5197", /* the larger of the two */ monthlyDetail: "https://res.cloudinary.com/xf7oayn7/image/upload/f_auto,q_auto/IMG_4835" /* a close-up of the same setting */ });

Monthly Flowers

Fresh flowers, on repeat.

Seasonal flowers, thoughtfully composed and delivered regularly for your space.

Starting from 350.000đ

Discover Monthly Flowers
In a space · 4:6Detail · 4:6
/* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== E D I T Y O U R P H O T O S ================================================================== Four pictures across three sections. experiences a workshop in progress -- people, hands, a table brands a campaign or activation venues an installation in a hotel or venue proposal the moment itself -- PORTRAIT, 3 wide by 4 tall proposalDetail a detail of the same setting To get a web address for a picture: add an Image element anywhere in Carrd, upload your photograph, then copy the address it gives you and paste it between the quote marks below. Leave a line as "" and the picture that is there now stays. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ experiences: "", brands: "", venues: "", proposal: "", proposalDetail: "" });
Photo 1 · Experiences — workshop / people around flowers

Experiences

Gather around flowers.

Hands-on floral experiences, hosted in the studio and beyond.

Discover experiences

Events & Collaborations

Let's make something people remember.

Floral concepts, installations and creative collaborations for brands, hotels and spaces.

Photo 04 · The proposal · your photograph here

Private Celebrations

For the moments that mean everything.

Proposals, intimate celebrations and floral settings created around your moment.

Plan your moment

Photo 05 · A detail of the setting · your photograph here
/* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== E D I T Y O U R P H O T O S ================================================================== One picture: the two of you in the studio. Landscape crops best, and it wants you working rather than posed -- arranging, tying, mid-conversation. Not a team portrait. To get a web address for a picture: add an Image element anywhere in Carrd, upload your photograph, then copy the address it gives you and paste it between the quote marks below. Leave a line as "" and the picture that is there now stays. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ about: "" });

About Hera

It started with flowers.

Hera is an independent floral studio in Đà Nẵng, shaped by a love of flowers, beautiful spaces and the people we create them for.

Our story

Photo · Hera founders / studio

In the studio — Đà Nẵng

/* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== E D I T Y O U R P H O T O S ================================================================== One picture at the top of the Blooms page. It wants to read as a studio photograph rather than a product shot -- a table being worked on, a wrapped bouquet leaving, an armful of stems. Slightly wider than it is tall. To get a web address for a picture: add an Image element anywhere in Carrd, upload your photograph, then copy the address it gives you and paste it between the quote marks below. Leave a line as "" and the picture that is there now stays. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ bloomsHero: "" });
Skip to content
Floral studio · Đà Nẵng · Việt Nam

Blooms

Find your flowers.

Signature arrangements, seasonal flowers and pieces made for the moment you have in mind.

Explore blooms
Photo · Blooms hero · your photograph here
/* ================================================================== ## S Y S T E M C O D E -- D O N O T E D I T ## ================================================================== Turns the picture at the top of Blooms into a slow slideshow when more than one address has been given. The addresses are yours to set, in 00-IMAGES-02: bloomsHero is the first picture, and HERA_BLOOMS_HERO holds any that follow. With none of those filled in, this does nothing at all and the page keeps the single still picture it has always had. The pictures are stacked in the frame that already exists and cross-faded with opacity, so the reveal animation, the phone crop and the mobile version of bloomsHero all keep working untouched. ================================================================== */ (function(){ var HOLD = 5200; /* how long each picture is held, in thousandths of a second */ function start(){ var frame = document.querySelector('[data-photo="bloomsHero"]'); if(!frame) return false; if(frame.getAttribute('data-rotating')) return true; var extras = (window.HERA_BLOOMS_HERO || []) .map(function(u){ return String(u == null ? '' : u).trim(); }) .filter(function(u){ return !!u; }); if(!extras.length) return true; /* one picture: nothing to do */ /* The first slide is whatever the photo system already placed, so the mobile version of bloomsHero and its crop are inherited rather than reproduced here. If it has not arrived yet, wait. */ var first = frame.querySelector('img'); if(!first) return false; frame.setAttribute('data-rotating', '1'); first.className = (first.className ? first.className + ' ' : '') + 'h-hero-slide is-on'; extras.forEach(function(url){ var im = document.createElement('img'); /* the right size for this frame, not the camera original */ im.src = (window.HERA_SIZED ? window.HERA_SIZED(url, frame) : url); im.alt = ''; /* the first slide carries the description */ im.loading = 'lazy'; im.className = 'h-hero-slide'; frame.appendChild(im); }); var slides = frame.querySelectorAll('.h-hero-slide'); /* a small mark per picture, so it reads as a carousel rather than a picture that changes on its own */ var dots = document.createElement('span'); dots.className = 'h-bl-hero-dots'; dots.setAttribute('aria-hidden', 'true'); for(var d = 0; d < slides.length; d++){ dots.appendChild(document.createElement('i')); } dots.firstChild.className = 'is-on'; frame.appendChild(dots); var marks = dots.querySelectorAll('i'); /* The first picture is fetched at once; the rest are left until the page has settled and then asked for properly. A slide that turns up blank because its picture had not been fetched is far worse than a page that weighs a little more. */ window.setTimeout(function(){ for(var s = 1; s < slides.length; s++) slides[s].loading = 'eager'; }, 1200); /* someone who has asked for less movement gets the first picture and no movement at all, rather than a slideshow that jumps */ if(window.matchMedia('(prefers-reduced-motion: reduce)').matches) return true; var at = 0, moving = false; function go(next){ if(moving || next === at) return; moving = true; var out = slides[at], on = slides[next]; /* the one leaving slides out to the left; the one arriving is placed off to the right first, so it comes in rather than appearing in the middle */ on.classList.remove('is-out'); out.classList.remove('is-on'); out.classList.add('is-out'); on.classList.add('is-on'); marks[at].classList.remove('is-on'); marks[next].classList.add('is-on'); at = next; setTimeout(function(){ out.classList.remove('is-out'); moving = false; }, 950); } setInterval(function(){ go((at + 1) % slides.length); }, HOLD); return true; } /* Carrd loads a section's pictures a moment after its markup, so this tries again briefly rather than giving up on the first look. */ var tries = 0; (function again(){ if(start()) return; if(++tries > 40) return; setTimeout(again, 150); })(); })();
/* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== T H E N O T I C E A B O V E T H E S H O P ================================================================== A single line that sits above the search box on the Blooms page, for anything you want every customer to read before they order -- a holiday rush, a delivery pause, a note that some styles are limited this week. TO TURN IT ON: change show: false to show: true TO TURN IT OFF: change show: true to show: false Nothing else has to change. When it is off the line is not there at all, and the page looks exactly as it does now. TO CHANGE THE WORDS: type over the sentence between the quote marks. Keep it to a sentence or two -- it is a notice, not a paragraph. An apostrophe is fine; type it as \' if you use one. ================================================================== */ window.HERA_NOTICE = { show: true, text: "For holidays, we recommend placing your order at least one week " + "in advance so we have enough time to prepare everything " + "beautifully for you." };
(function(){ var N = window.HERA_NOTICE || {}; var box = document.getElementById('h-bl-notice'); if(!box) return; var words = String(N.text == null ? '' : N.text).trim(); if(N.show !== true || !words) return; /* off: nothing is drawn */ box.innerHTML = '\x3cdiv>' + '\x3ci aria-hidden="true">\x3c/i>' + '\x3cdiv>\x3cb>Please note\x3c/b>' + '\x3cp>' + words.replace(/&/g,'\x26amp;').replace(/\x3c/g,'\x26lt;') + '\x3c/p>' + '\x3c/div>' + '\x3c/div>'; box.hidden = false; })();
Size

The catalogue

Every arrangement is made by hand, so no two are ever identical. Flowers change with the season and with what is good that morning — yours will be at least 70% like the photograph, and we hold its colour, shape and feeling exactly.

Add-ons

Something to go with them.

Extras we can bring in alongside your flowers.

See all add-ons

/* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== E D I T Y O U R P H O T O S ================================================================== One picture, beside Create Your Own. It should look like a conversation rather than a finished bouquet -- a sketch, stems being chosen, a hand pointing at something on a table. Taller than it is wide. To get a web address for a picture: add an Image element anywhere in Carrd, upload your photograph, then copy the address it gives you and paste it between the quote marks below. Leave a line as "" and the picture that is there now stays. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ createYourOwn: "" });

Create your own

Nothing here is quite it?

Tell us the occasion, roughly what you'd like to spend and the feeling you're after. We'll design something from the flowers worth having that week.

  • Occasion
  • Budget
  • Colours
  • Anything specific

Start a custom piece

Photo · Create your own · your photograph here

Monthly Flowers

Fresh flowers, on repeat.

Seasonal flowers, composed and delivered regularly for your space.

Starting from 350.000đ

Discover Monthly Flowers

/* Create Your Own and the Customize it! tab are the same errand, so the button opens the form rather than sending anyone hunting for it. */ (function(){ var a=document.getElementById('h-cyo-go'); if(!a) return; a.addEventListener('click',function(e){ if(typeof window.HERA_openCustomForm==='function'){ e.preventDefault(); window.HERA_openCustomForm(); } }); })();
/* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 1. E V E R Y P R I C E , I N O N E P L A C E ================================================================== Change a price here and it changes everywhere on the page. Nothing below is written into the layout. Leave any of them as "" and that line simply does not appear, so nothing ever reads as an empty or provisional number. fromPer the entry point shown on the hero and the plans oneMonth what a one month plan starts at threeMonth what a three month plan starts at longer what the longer plan says instead of a price inHome the in-home arranging service vase what vase rental says note the small print under the plans ================================================================== */ window.HERA_MF_PRICE = { fromPer: "350k", oneMonth: "", threeMonth: "", longer: "Priced with you", inHome: "From 250,000 VND per session", vase: "By request", note: "Starting price depends on the size of the arrangement, delivery and the plan you choose. Plans can be renewed at the end of a subscription." }; /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 2. Y O U R P H O T O G R A P H S ================================================================== Flowers living in a home, not a bouquet on a white background. The hero especially: a vase on a table in a real room says "every week" far better than a wrapped bouquet does. mfHero the opening picture. A room with flowers in it. mfHome flowers at home, for "your space, your flowers" mfDetail a close crop, used quietly Leave any of them as "" and a labelled space holds its place. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ mfHero: "https://res.cloudinary.com/bayvlj8i/image/upload/f_auto,q_auto/IMG_4581", mfHome: "", mfDetail: "" }); /* 3. The week's arrangements. Five to ten works well. They share one height and keep their own width, so an upright vase and a wide table shot sit together without either being cropped square. src the picture address from Carrd cap a short label, or "" for none */ window.HERA_MF_SAMPLES = [ { src:"https://res.cloudinary.com/xf7oayn7/image/upload/f_auto,q_auto/IMG_1462", cap:"" }, { src:"https://res.cloudinary.com/xf7oayn7/image/upload/f_auto,q_auto/IMG_5800", cap:"" }, { src:"https://res.cloudinary.com/xf7oayn7/image/upload/f_auto,q_auto/IMG_5801", cap:"" }, { src:"https://res.cloudinary.com/xf7oayn7/image/upload/f_auto,q_auto/IMG_5807", cap:"" }, { src:"https://res.cloudinary.com/xf7oayn7/image/upload/f_auto,q_auto/IMG_5805", cap:"" }, { src:"https://res.cloudinary.com/xf7oayn7/image/upload/f_auto,q_auto/IMG_5795", cap:"" }, { src:"https://res.cloudinary.com/xf7oayn7/image/upload/f_auto,q_auto/IMG_5793", cap:"" }, { src:"https://res.cloudinary.com/xf7oayn7/image/upload/f_auto,q_auto/IMG_5799", cap:"" } ]; /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 4. T H E P L A N S ================================================================== no the numeral name the plan's name line one line about who it suits rhythm what the delivery looks like meta the label above the figure, e.g. "Deliveries" value what sits under that label price which price from section 1 to show: "oneMonth", "threeMonth" or "longer". Leave "" for none. cta the words on its link want which option the form should choose when someone arrives from that link. Must match section 5 exactly. ================================================================== */ window.HERA_MF_PLANS = [ { no:"01", name:"One month", line:"A short introduction to the Flower Club.", rhythm:"Weekly delivery for one month.", meta:"Deliveries", value:"4 weekly deliveries", price:"oneMonth", cta:"Choose one month", want:"1 Month" }, { no:"02", name:"Three months", line:"For making fresh flowers part of the routine.", rhythm:"Weekly delivery for three months.", meta:"Deliveries", value:"12 weekly deliveries", price:"threeMonth", cta:"Choose three months", want:"3 Months" }, { no:"03", name:"Longer", line:"For homes, offices, or anyone who wants to keep it going.", rhythm:"A longer or custom subscription.", meta:"Duration", value:"However long suits you", price:"longer", cta:"Ask about a longer plan", want:"Longer / Custom" } ]; /* 5. What the form asks first. The first three must match the "want" lines above exactly, or the links will not preselect anything. */ window.HERA_MF_WANTS = ["1 Month", "3 Months", "Longer / Custom", "Not sure yet"]; /* 6. When deliveries can happen. Both lists are used by the form. */ window.HERA_MF_DAYS = ["Monday","Tuesday","Wednesday","Thursday", "Friday","Saturday","Sunday"]; window.HERA_MF_TIMES = ["10:00 \u2013 12:00", "15:00 \u2013 17:00", "17:00 \u2013 19:00 (weekends)"]; /* 7. What each week feels like. Three at most. */ window.HERA_MF_FEEL = [ { no:"01", t:"Fresh", b:"Chosen from the best of what arrives that week." }, { no:"02", t:"Different", b:"New combinations, new varieties, a changing seasonal mood." }, { no:"03", t:"Easy", b:"A recurring delivery, so the house is refreshed without anyone reordering." } ]; /* 8. How it works. Four steps. */ window.HERA_MF_STEPS = [ { no:"01", t:"Choose your plan", b:"One month, three months, or ask about something longer." }, { no:"02", t:"Tell us your style", b:"Give Hera a direction, or let us surprise you each week." }, { no:"03", t:"Set your delivery day", b:"One fixed day each week, and the time window that suits you." }, { no:"04", t:"We keep it fresh", b:"Hera makes a new mix each week and brings it to your door." } ];

Hera Flower Club

Fresh flowers,
every week.

A changing mix of seasonal flowers delivered to your door, so your space always has something fresh in it.

New flowers. New combinations. A new mood every week.

Start your Flower Club

Plans from one month.

Photo · flowers at home · the opening picture

The weekly ritual

Flowers that change
with the week.

Every week, Hera works with the freshest flowers available from current shipments and seasonal supply. We mix varieties, colours and textures to make something new each time.

Not the same arrangement repeated. A fresh floral moment shaped by what is best that week.

    A week at Hera

    A different arrangement,
    every delivery.

    Each week's flowers depend on the season and what is available. These are examples of Hera's floral direction, not fixed weekly designs.

    Choose your rhythm

    Keep the flowers coming.

      How it works

      Set it once.
      Enjoy it every week.

        Your space.
        Your flowers.

        Custom request

        Tell us the colours, the mood, the shape or the general direction you like, and Hera works to it.

        Your references are a direction rather than a design to copy. What is available changes every week, so the arrangement is interpreted through Hera's style and that week's flowers.

        Surprise me

        Let Hera design each week from whatever is freshest, without a brief to work to.

        Most people who stay a long time end up here. It is the version that brings something home you would not have thought to ask for.

        Your weekly delivery

        Same rhythm.
        Fresh flowers.

        Choose one fixed day each week, and the window that suits you.

        Days

        Times

        Please let Hera know at least one day in advance if a delivery needs to move.

        A little extra

        Need more than delivery?

          Hera Flower Club

          Start your Flower Club.

          Tell us how you'd like your weekly flowers to work. Hera will confirm availability, pricing and your first delivery with you directly.

          var __hb7="\n/* ==================================================================\n Nothing below this line needs editing.\n ================================================================== */\n(function(){\n function esc(s){\n return String(s == null ? '' : s)\n .replace(/&(?!#?\\w+;)/g, '\x26amp;').replace(/\x3c/g, '\x26lt;')\n .replace(/>/g, '\x26gt;').replace(/\"/g, '\x26quot;');\n }\n function copy(s){ return String(s == null ? '' : s); }\n var ARROW = '\x3csvg width=\"17\" height=\"10\" viewBox=\"0 0 17 10\" fill=\"none\" ' +\n 'stroke=\"currentColor\" stroke-width=\"1.1\" aria-hidden=\"true\">' +\n '\x3cpath d=\"M0 5h15M11.5 1 15.5 5l-4 4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\x3c/svg>';\n var REDUCED = window.matchMedia('(prefers-reduced-motion:reduce)').matches;\n var P = window.HERA_MF_PRICE || {};\n\n /* ---- the hero, once ---------------------------------------------- */\n var top_ = document.querySelector('.h-mf-top');\n if(top_){\n if(REDUCED) top_.classList.add('is-in');\n else requestAnimationFrame(function(){\n setTimeout(function(){ top_.classList.add('is-in'); }, 60);\n });\n }\n /* the entry price, shown only when there is one */\n var from = document.getElementById('h-mf-from');\n if(from && String(P.fromPer || '').trim()){\n from.textContent = 'Starting from ' + P.fromPer + '.';\n }\n\n /* ---- 03 what each week feels like -------------------------------- */\n var feel = document.getElementById('h-mf-feel');\n if(feel){\n feel.innerHTML = (window.HERA_MF_FEEL || []).map(function(f, i){\n return '\x3cli class=\"h-mf-feel-i h-rev\" style=\"--d:' + (i * 120) + 'ms\">' +\n '\x3cspan class=\"h-mf-feel-n\" aria-hidden=\"true\">' + copy(f.no) + '\x3c/span>' +\n '\x3cp class=\"h-mf-feel-t\">' + copy(f.t) + '\x3c/p>' +\n '\x3cp class=\"h-mf-feel-b\">' + copy(f.b) + '\x3c/p>\x3c/li>';\n }).join('');\n }\n\n /* ---- 04 the week's arrangements -----------------------------------\n One height, natural widths. Frames hold a minimum width until their\n photograph loads, so the strip can always be scrolled -- otherwise\n the pictures further along could never come into view to load. */\n var gal = document.getElementById('h-mf-gal');\n var SHOTS = (window.HERA_MF_SAMPLES || []).filter(function(s){\n return String(s && s.src || '').trim();\n });\n if(gal){\n if(!SHOTS.length){\n gal.innerHTML = (window.HERA_MF_SAMPLES || []).slice(0, 6).map(function(s, i){\n return '\x3cfigure class=\"h-mf-shot is-empty h-rev\" style=\"--d:' + ((i % 4) * 70) + 'ms\">' +\n '\x3cspan class=\"h-quiet-ph\">\x3cspan>' + esc(s.cap || 'A week at Hera') +\n '\x3c/span>\x3c/span>\x3c/figure>';\n }).join('');\n }else{\n gal.innerHTML = SHOTS.map(function(s, i){\n var cap = String(s.cap || '').trim()\n ? '\x3cfigcaption class=\"h-mf-shot-c\">' + copy(s.cap) + '\x3c/figcaption>' : '';\n return '\x3cfigure class=\"h-mf-shot h-rev\" style=\"--d:' + ((i % 4) * 70) + 'ms\">' +\n '\x3cimg src=\"' + esc(s.src) + '\" alt=\"' + esc(s.cap || 'A Hera weekly arrangement') +\n '\" loading=\"lazy\" draggable=\"false\">' + cap + '\x3c/figure>';\n }).join('');\n }\n dragging(gal);\n }\n\n /* one drag behaviour, used by the strip */\n function dragging(rail){\n var down = false, sx = 0, sl = 0, vel = 0, lx = 0, raf = 0;\n function coast(){\n vel *= 0.94;\n if(Math.abs(vel) < 0.4){ rail.classList.remove('is-drag'); return; }\n rail.scrollLeft -= vel;\n raf = requestAnimationFrame(coast);\n }\n rail.addEventListener('pointerdown', function(e){\n if(e.pointerType === 'touch') return;\n down = true; vel = 0; sx = lx = e.clientX; sl = rail.scrollLeft;\n cancelAnimationFrame(raf);\n rail.classList.add('is-drag');\n rail.setPointerCapture(e.pointerId);\n });\n rail.addEventListener('pointermove', function(e){\n if(!down) return;\n vel = e.clientX - lx; lx = e.clientX;\n rail.scrollLeft = sl - (e.clientX - sx);\n });\n function release(e){\n if(!down) return;\n down = false;\n try{ rail.releasePointerCapture(e.pointerId); }catch(err){}\n if(Math.abs(vel) > 1) raf = requestAnimationFrame(coast);\n else rail.classList.remove('is-drag');\n }\n rail.addEventListener('pointerup', release);\n rail.addEventListener('pointercancel', release);\n rail.addEventListener('keydown', function(e){\n if(e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;\n e.preventDefault();\n rail.scrollLeft += (e.key === 'ArrowRight' ? 1 : -1) * rail.clientWidth * 0.8;\n });\n }\n\n /* ---- 05 the plans -------------------------------------------------- */\n var plans = document.getElementById('h-mf-plans');\n if(plans){\n plans.innerHTML = (window.HERA_MF_PLANS || []).map(function(pl, i){\n var price = String(P[pl.price] || '').trim();\n var priceRow = price\n ? '\x3cp class=\"h-mf-plan-k\">Starting at\x3c/p>\x3cp class=\"h-mf-plan-v\">' + copy(price) + '\x3c/p>'\n : '\x3cp class=\"h-mf-plan-k\">\x26nbsp;\x3c/p>\x3cp class=\"h-mf-plan-v\">\x26nbsp;\x3c/p>';\n return '\x3cli class=\"h-mf-plan-i h-rev\" style=\"--d:' + (i * 110) + 'ms\">' +\n '\x3cspan class=\"h-mf-plan-n\" aria-hidden=\"true\">' + copy(pl.no) + '\x3c/span>' +\n '\x3ch3 class=\"h-mf-plan-t\">' + copy(pl.name) + '\x3c/h3>' +\n '\x3cp class=\"h-mf-plan-d\">' + copy(pl.line) + '\x3c/p>' +\n '\x3cp class=\"h-mf-plan-r\">' + copy(pl.rhythm) + '\x3c/p>' +\n '\x3cp class=\"h-mf-plan-k\">' + copy(pl.meta) + '\x3c/p>' +\n '\x3cp class=\"h-mf-plan-v\">' + copy(pl.value) + '\x3c/p>' +\n priceRow +\n '\x3ca class=\"h-mf-plan-go\" href=\"#flower-club-join\" data-want=\"' + esc(pl.want) + '\">' +\n copy(pl.cta) + ARROW + '\x3c/a>\x3c/li>';\n }).join('');\n }\n var pnote = document.getElementById('h-mf-plan-note');\n if(pnote) pnote.textContent = String(P.note || '');\n\n /* ---- 06 how it works ------------------------------------------------ */\n var steps = document.getElementById('h-mf-steps');\n if(steps){\n steps.innerHTML = (window.HERA_MF_STEPS || []).map(function(s, i){\n return '\x3cli class=\"h-mf-step-i h-rev\" style=\"--d:' + (i * 90) + 'ms\">' +\n '\x3cspan class=\"h-mf-step-n\" aria-hidden=\"true\">' + copy(s.no) + '\x3c/span>' +\n '\x3cp class=\"h-mf-step-t\">' + copy(s.t) + '\x3c/p>' +\n '\x3cp class=\"h-mf-step-b\">' + copy(s.b) + '\x3c/p>\x3c/li>';\n }).join('');\n }\n\n /* ---- 08 the delivery ------------------------------------------------ */\n var days = document.getElementById('h-mf-days');\n if(days) days.textContent = (window.HERA_MF_DAYS || []).join(' \\u00b7 ');\n var times = document.getElementById('h-mf-times');\n if(times) times.textContent = (window.HERA_MF_TIMES || []).join(' \\u00b7 ');\n\n /* ---- 09 the extras --------------------------------------------------- */\n var extras = document.getElementById('h-mf-extras');\n if(extras){\n var rows = [\n ['In-home floral arrangement', String(P.inHome || ''),\n 'Hera comes to you and arranges the flowers in your space, in your own vase.'],\n ['Vase rental', String(P.vase || ''),\n 'Please ask at least three days ahead so Hera can prepare a vase that suits the flowers.']\n ].filter(function(r){ return r[1].trim(); });\n extras.innerHTML = rows.map(function(r, i){\n return '\x3cli class=\"h-mf-extra-i h-rev\" style=\"--d:' + (i * 90) + 'ms\">' +\n '\x3cp class=\"h-mf-extra-t\">' + copy(r[0]) + '\x3c/p>' +\n '\x3cp class=\"h-mf-extra-b\">' + copy(r[2]) + '\x3c/p>' +\n '\x3cp class=\"h-mf-extra-v\">' + copy(r[1]) + '\x3c/p>\x3c/li>';\n }).join('');\n }\n\n /* ---- 10 joining -----------------------------------------------------\n Built rather than written out, so the plans, days and times on the\n page and the ones in the form can never drift apart. */\n var form = document.getElementById('h-mf-form');\n if(!form) return;\n function seg(name, list, id){\n return '\x3cdiv class=\"h-mf-seg\" role=\"group\" id=\"' + id + '\">' +\n list.map(function(o){\n return '\x3clabel class=\"h-mf-seg-o\">\x3cinput type=\"radio\" name=\"' + name +\n '\" value=\"' + esc(o) + '\">\x3cspan>' + esc(o) + '\x3c/span>\x3c/label>';\n }).join('') + '\x3c/div>';\n }\n function field(id, label, req, type, ph, half){\n var mark = req ? ' \x3cspan class=\"h-mf-req\">required\x3c/span>' : '';\n var body = type === 'area'\n ? '\x3ctextarea class=\"h-mf-in h-mf-ta\" id=\"' + id + '\" placeholder=\"' + esc(ph || '') + '\">\x3c/textarea>'\n : '\x3cinput class=\"h-mf-in\" type=\"' + (type || 'text') + '\" id=\"' + id +\n '\" placeholder=\"' + esc(ph || '') + '\">';\n return '\x3clabel class=\"h-mf-f' + (half ? '' : ' is-wide') + '\" data-f=\"' + id + '\">' +\n '\x3cspan class=\"h-mf-lab\">' + label + mark + '\x3c/span>' + body +\n '\x3cspan class=\"h-mf-err\" id=\"' + id + '-e\">\x3c/span>\x3c/label>';\n }\n form.innerHTML =\n '\x3cdiv class=\"h-mf-stg\">\x3cp class=\"h-mf-stg-n\">\x3cspan>01\x3c/span> Your plan\x3c/p>' +\n '\x3cp class=\"h-mf-flab\">Which plan suits you? \x3cspan class=\"h-mf-req\">required\x3c/span>\x3c/p>' +\n seg('mf-plan', window.HERA_MF_WANTS || [], 'mf-plan') +\n '\x3cspan class=\"h-mf-err\" id=\"mf-plan-e\">\x3c/span>\x3c/div>' +\n\n '\x3cdiv class=\"h-mf-stg\">\x3cp class=\"h-mf-stg-n\">\x3cspan>02\x3c/span> How we design it\x3c/p>' +\n '\x3cp class=\"h-mf-flab\">How would you like Hera to design your flowers? \x3cspan class=\"h-mf-req\">required\x3c/span>\x3c/p>' +\n seg('mf-style', ['Custom request', 'Surprise me'], 'mf-style') +\n '\x3cspan class=\"h-mf-err\" id=\"mf-style-e\">\x3c/span>' +\n '\x3cdiv class=\"h-mf-more\" id=\"mf-style-more\" hidden>' +\n '\x3cdiv class=\"h-mf-grid\">' +\n field('mf-notes', 'Tell us about the colours, mood or shape you like', true, 'area',\n 'Soft and pale, deep and warm, tall and sculptural...') +\n field('mf-link', 'A link or reference', true, 'text',\n 'A Pinterest board or an Instagram post') +\n '\x3c/div>' +\n '\x3cp class=\"h-mf-fine h-mf-fine--pale\">Photographs are welcome when Hera follows up ' +\n 'to confirm your plan \x26#8212; this page cannot receive files yet.\x3c/p>' +\n '\x3c/div>\x3c/div>' +\n\n '\x3cdiv class=\"h-mf-stg\">\x3cp class=\"h-mf-stg-n\">\x3cspan>03\x3c/span> Your delivery\x3c/p>' +\n '\x3cp class=\"h-mf-flab\">Which day each week? \x3cspan class=\"h-mf-req\">required\x3c/span>\x3c/p>' +\n seg('mf-day', window.HERA_MF_DAYS || [], 'mf-day') +\n '\x3cspan class=\"h-mf-err\" id=\"mf-day-e\">\x3c/span>' +\n '\x3cp class=\"h-mf-flab h-mf-flab--gap\">And what time? \x3cspan class=\"h-mf-req\">required\x3c/span>\x3c/p>' +\n seg('mf-time', window.HERA_MF_TIMES || [], 'mf-time') +\n '\x3cspan class=\"h-mf-err\" id=\"mf-time-e\">\x3c/span>' +\n '\x3cp class=\"h-mf-fine h-mf-fine--pale\">Please let Hera know at least one day ahead if ' +\n 'a delivery needs to move.\x3c/p>\x3c/div>' +\n\n '\x3cdiv class=\"h-mf-stg\">\x3cp class=\"h-mf-stg-n\">\x3cspan>04\x3c/span> Anything extra\x3c/p>' +\n '\x3cp class=\"h-mf-flab\">Choose any that apply. \x3cspan class=\"h-mf-req\">required\x3c/span>\x3c/p>' +\n '\x3cdiv class=\"h-mf-chips\" role=\"group\" aria-label=\"Optional services\">' +\n ['In-home floral arrangement', 'Vase rental', 'No additional service'].map(function(o){\n return '\x3clabel class=\"h-mf-chip\">\x3cinput type=\"checkbox\" value=\"' + esc(o) +\n '\">\x3cspan>' + esc(o) + '\x3c/span>\x3c/label>';\n }).join('') +\n '\x3c/div>\x3cspan class=\"h-mf-err\" id=\"mf-extra-e\">\x3c/span>\x3c/div>' +\n\n '\x3cdiv class=\"h-mf-stg\">\x3cp class=\"h-mf-stg-n\">\x3cspan>05\x3c/span> Where it goes\x3c/p>' +\n '\x3cdiv class=\"h-mf-grid\">' +\n field('mf-name', 'Full name', true, 'text', '', true) +\n field('mf-phone', 'Phone', true, 'tel', '', true) +\n '\x3c/div>' +\n '\x3cp class=\"h-mf-flab h-mf-flab--gap\">Best way to reach you \x3cspan class=\"h-mf-req\">required\x3c/span>\x3c/p>' +\n seg('mf-reach', ['WhatsApp', 'Zalo', 'Instagram', 'Facebook', 'Phone'], 'mf-reach') +\n '\x3cspan class=\"h-mf-err\" id=\"mf-reach-e\">\x3c/span>' +\n '\x3cdiv class=\"h-mf-grid\">' +\n field('mf-handle', 'Your username or number there', true, 'text', '') +\n field('mf-addr', 'Delivery address', true, 'area', 'Building, street, district, city') +\n field('mf-dnotes', 'Delivery notes', true, 'text',\n 'A gate code, a floor, someone to ask for...') +\n '\x3c/div>\x3c/div>' +\n\n '\x3cdiv class=\"h-mf-stg\">\x3cp class=\"h-mf-stg-n\">\x3cspan>06\x3c/span> You\x3c/p>' +\n '\x3cp class=\"h-mf-flab\">Are these flowers for you? \x3cspan class=\"h-mf-req\">required\x3c/span>\x3c/p>' +\n seg('mf-forme', ['Yes', 'No'], 'mf-forme') +\n '\x3cspan class=\"h-mf-err\" id=\"mf-forme-e\">\x3c/span>' +\n '\x3cdiv class=\"h-mf-more\" id=\"mf-forme-more\" hidden>\x3cdiv class=\"h-mf-grid\">' +\n field('mf-yname', 'Your name', true, 'text";
          __hb7+="', '', true) +\n field('mf-yphone', 'Your phone', true, 'tel', '', true) +\n field('mf-yemail', 'Your email', true, 'email', '') +\n '\x3c/div>\x3c/div>\x3c/div>' +\n\n '\x3cp class=\"h-mf-sum\" id=\"h-mf-sum\" hidden>\x3c/p>' +\n '\x3cdiv class=\"h-mf-send\">' +\n '\x3cbutton class=\"h-mf-btn h-mf-btn--solid\" type=\"submit\">Start my Flower Club' + ARROW + '\x3c/button>' +\n '\x3cp class=\"h-mf-fine h-mf-fine--pale\">Sending this does not confirm a subscription. ' +\n 'Hera will be in touch to confirm availability, pricing and your first delivery.\x3c/p>' +\n '\x3c/div>';\n\n /* the two questions that open something else */\n function watch(name, opensOn, panel){\n document.addEventListener('change', function(e){\n if(!e.target || e.target.name !== name) return;\n var box = document.getElementById(panel);\n if(box) box.hidden = e.target.value !== opensOn;\n var err = document.getElementById(name + '-e');\n if(err) err.textContent = '';\n var grp = document.getElementById(name);\n if(grp) grp.classList.remove('is-bad');\n });\n }\n watch('mf-style', 'Custom request', 'mf-style-more');\n watch('mf-forme', 'No', 'mf-forme-more');\n\n /* arriving from a plan brings its answer along */\n document.addEventListener('click', function(e){\n var a = e.target.closest('a[data-want]');\n if(!a) return;\n var r = form.querySelector('input[name=\"mf-plan\"][value=\"' +\n String(a.dataset.want).replace(/\"/g, '') + '\"]');\n if(r){ r.checked = true; r.dispatchEvent(new Event('change', {bubbles:true})); }\n });\n\n form.addEventListener('submit', function(e){\n e.preventDefault();\n var bad = 0, firstBad = null;\n function need(id, msg){\n var el = document.getElementById(id);\n var box = form.querySelector('[data-f=\"' + id + '\"]');\n var err = document.getElementById(id + '-e');\n if(!el || !box || !err) return;\n box.classList.remove('is-bad'); err.textContent = '';\n var v = (el.value || '').trim();\n var m = '';\n if(!v) m = msg;\n else if(el.type === 'email' && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v))\n m = 'That does not look like an email address.';\n if(m){ box.classList.add('is-bad'); err.textContent = m; bad++;\n if(!firstBad) firstBad = el; }\n }\n function pick(name, msg){\n var grp = document.getElementById(name);\n var err = document.getElementById(name + '-e');\n if(!grp || !err) return null;\n grp.classList.remove('is-bad'); err.textContent = '';\n var on = form.querySelector('input[name=\"' + name + '\"]:checked');\n if(!on){ grp.classList.add('is-bad'); err.textContent = msg; bad++;\n if(!firstBad) firstBad = grp.querySelector('input'); }\n return on ? on.value : null;\n }\n\n pick('mf-plan', 'Please choose a plan.');\n var style = pick('mf-style', 'Please choose one.');\n if(style === 'Custom request'){\n need('mf-notes', 'Please tell us a little here.');\n need('mf-link', 'Please add a link or a reference.');\n }\n pick('mf-day', 'Please choose a day.');\n pick('mf-time', 'Please choose a time.');\n // at least one service, even if that service is none\n var chips = form.querySelectorAll('.h-mf-chip input:checked');\n var chipErr = document.getElementById('mf-extra-e');\n if(chipErr){\n chipErr.textContent = '';\n if(!chips.length){\n chipErr.textContent = 'Please choose one, or choose no additional service.';\n bad++;\n if(!firstBad) firstBad = form.querySelector('.h-mf-chip input');\n }\n }\n need('mf-name', 'Please add a name.');\n need('mf-phone', 'Please add a phone number.');\n pick('mf-reach', 'Please choose one.');\n need('mf-handle', 'Please add the username or number to reach you on.');\n need('mf-addr', 'Please add the delivery address.');\n need('mf-dnotes', 'Please add a note, or write none.');\n var forme = pick('mf-forme', 'Please choose one.');\n if(forme === 'No'){\n need('mf-yname', 'Please add your name.');\n need('mf-yphone', 'Please add your phone number.');\n need('mf-yemail', 'Please add your email address.');\n }\n\n var sum = document.getElementById('h-mf-sum');\n if(bad){\n if(sum){\n sum.hidden = false;\n sum.textContent = bad === 1\n ? 'One answer still needs your attention.'\n : bad + ' answers still need your attention.';\n }\n if(firstBad && firstBad.focus) firstBad.focus();\n return;\n }\n if(sum) sum.hidden = true;\n var host = form.parentElement;\n form.style.transition = 'opacity .26s cubic-bezier(.22,1,.36,1)';\n form.style.opacity = '0';\n setTimeout(function(){\n form.outerHTML =\n '\x3cdiv class=\"h-mf-done\">' +\n '\x3cp class=\"h-mf-eye h-mf-eye--pale\">You\\u2019re in\x3c/p>' +\n '\x3cp class=\"h-mf-done-h\">Fresh flowers are on the way.\x3c/p>' +\n '\x3cp class=\"h-mf-done-p\">Thank you for joining Hera Flower Club. We\\u2019ll be in ' +\n 'touch shortly to confirm your plan and your first delivery.\x3c/p>' +\n '\x3ca class=\"h-mf-done-go\" href=\"#blooms\" data-link=\"blooms\">View Blooms' + ARROW + '\x3c/a>' +\n '\x3cp class=\"h-mf-fine h-mf-fine--pale\">A copy has been emailed to you as well, so you have a record of it.\x3c/p>' +\n '\x3c/div>';\n var d = host.querySelector('.h-mf-done');\n if(d) d.scrollIntoView({behavior:'smooth', block:'center'});\n }, 280);\n });\n\n if(window.__heraApplyPhotos) window.__heraApplyPhotos();\n if(window.__heraScan) window.__heraScan();\n})();\n";(function(){var s=document.createElement("script");s.textContent=__hb7;document.head.appendChild(s);__hb7=null;})();
          Skip to content
          Floral studio · Đà Nẵng · Việt Nam
          /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== E D I T Y O U R P H O T O S Five pictures on this page. Paste a picture address between the quote marks and it appears. Leave a line as "" and the labelled placeholder stays, so you can approve the page now and add photographs whenever they are ready. To get a picture address: add an Image element anywhere in Carrd, upload your photograph, copy the address it gives you, then delete that Image element. hero People around a table of flowers. Very wide -- it runs the full width of the page. publicWork A public workshop in progress. Wide. oneToOne One person, close, hands in the flowers. Upright. group A private group workshop -- several people, one table, clearly enjoying themselves. Landscape. groupDetail A detail from the same session -- hands, a finished bouquet, a glass beside the stems. Square. masterclass Teaching -- a demonstration, a technique. Upright. certificate Your certificate, photographed flat. Landscape -- wider than it is tall. It shows small, so keep it legible. bloomBar One picture of the Bloom Bar. This page only teases it -- the Bloom Bar page carries the full story. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ expHero: "", expPublicWork: "", expOneToOne: "", expGroup: "", expGroupDetail: "", expMasterclass: "", expCertificate: "", expBloomBar: "" });

          Hera Experiences

          Photo · Experiences hero · people around the flowers

          Gather around flowers.

          From a first bouquet to a room full of them — experiences designed for learning, celebrating and creating together.

          Create with Hera

          Four ways to spend time with the flowers — on your own, with your people, or alongside everyone else who turned up that morning.

          Time with the flowers in your hands.

          Photo · Public workshop · a room mid-workshop

          Public Workshops

          Come as you are. Leave with flowers.

          Seasonal and special workshops, hosted in our studio and in places we love around Đà Nẵng. Book a single seat and turn up — everything is waiting for you.

          See what's coming

          Upcoming at Hera

          What's coming.

           

          Photo · 1:1 · hands in the flowers

          1:1 with Hera

          Just you, the flowers, and a little time to create.

          An unhurried private session. We'll take you through the basics — choosing stems, building a shape, tying it off — while you make something of your own. No experience wanted, and no rush.

          See the 1:1 Session

          Private Group Workshops

          Your people. Your flowers. Your time together.

          A floral experience reserved just for your group, guided by Hera and shaped around your occasion. Everyone makes something, and everyone takes it home.

          • Birthdays
          • Friends
          • Bridal groups
          • Team activities
          • Private gatherings

          Plan a private workshop

          Photo · Masterclass · a technique being taughtPhoto · CertificateCertificate

          Masterclasses

          Go deeper into the craft.

          For anyone who wants the technique behind the flowers. Smaller, longer and more focused than a workshop — you'll work through real methods with your hands, at your own pace, until they hold.

          • Focused instruction
          • Floral techniques
          • Guided practice
          • Certificate

          Explore the masterclass

          Photo · The Bloom Bar

          The Bloom Bar

          Rent the bar. Add our florists. Or make it an experience.

          A floral bar brought to your gathering — with as much or as little of Hera alongside it as you like.

          Discover the Bloom Bar

          Not sure which one?

          Tell us who's coming and what you'd like to make.

          Send us the occasion, roughly how many of you there are and when you were thinking. We'll tell you which experience fits — and if none of them quite do, we'll build one that does.

          Ask about an experience

          /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== E D I T Y O U R C A L E N D A R E V E N T S ================================================================== Everything that appears on the calendar lives in this one list. You never have to touch the calendar itself. ------------------------------------------------------------------ TO ADD AN EVENT ------------------------------------------------------------------ Copy one whole block, from its { down to its }, and paste it underneath. Then type over the parts between the quote marks. The calendar puts it on the right day on its own. ------------------------------------------------------------------ TO REMOVE AN EVENT ------------------------------------------------------------------ Delete its whole block, from { to }, including the comma. ------------------------------------------------------------------ TO MARK SOMETHING SOLD OUT ------------------------------------------------------------------ Change its status line to status: "sold", ------------------------------------------------------------------ TO CHANGE THE PICTURE ------------------------------------------------------------------ Upload it in Carrd, copy the address, and paste it next to image. Remember to LEAVE the Image element on the page -- Carrd deletes any picture nothing is using. ------------------------------------------------------------------ TO ADD OR CHANGE THE BOOKING LINK ------------------------------------------------------------------ Paste the address next to link. Leave it "" and no button shows. button lets you rename it -- "Reserve a seat", "Buy tickets". ------------------------------------------------------------------ WHAT EACH LINE MEANS ------------------------------------------------------------------ date ALWAYS written year-month-day with dashes: "2026-09-14" This is the only line that has to be exact -- it is what puts the event on the right square. title what it is called type the small label above the name, e.g. "Public Workshop" time "10:00 - 13:00" location "Hera Studio, Da Nang" summary one line, shown in the list under the calendar details the longer description, shown when it is opened includes what a seat includes. Separate each with a comma: "All flowers, Tools and wrap, Refreshments" price "850.000d" leave "" to say nothing about price status one of exactly four words: "available" normal "few" nearly gone, marked in brass "sold" gone, booking button hidden "coming-soon" visible but not bookable yet image a picture address, or "" for none link where the booking button goes, or "" for no button button what the button says. Leave "" for "Reserve a seat" ================================================================== */ var HERA_EVENTS = [ { date: "", title: "", type: "Public Workshop", time: "", location: "", summary: "", details: "", includes: "", price: "", status: "available", image: "", link: "", button: "" } ];
          /* ================================================================== ## S Y S T E M C O D E -- D O N O T E D I T ## ================================================================== Draws the month grid, moves between months, and opens an event in a panel over the page. Everything it shows comes from HERA_EVENTS above; nothing here needs changing to add, edit or remove a date. ================================================================== */ (function(){ var grid = document.getElementById('h-cal-days'), label = document.getElementById('h-cal-month'), listEl= document.getElementById('h-cal-list'), prev = document.getElementById('h-cal-prev'), next = document.getElementById('h-cal-next'); if(!grid) return; var MONTHS = ['January','February','March','April','May','June', 'July','August','September','October','November','December']; function esc(s){ return String(s == null ? '' : s) .replace(/&/g,'\x26amp;').replace(/\x3c/g,'\x26lt;').replace(/>/g,'\x26gt;').replace(/"/g,'\x26quot;'); } /* only entries with a real date AND a name are shown, so the blank starter block can be left in the list until it is needed */ /* The list used to be typed into this file by hand and read once. It now arrives from the workshops tab of the spreadsheet, which means it can turn up a moment after the page does -- so reading it is a function that can be run again rather than a one-off. */ var ALL = [], BY_DATE = {}; function readEvents(){ var fromSheet = (window.HERA && HERA.workshops && HERA.workshops.events) || null; ALL = (fromSheet || (typeof HERA_EVENTS !== 'undefined' && HERA_EVENTS) || []) .filter(function(e){ return e && /^\d{4}-\d{2}-\d{2}$/.test(String(e.date || '').trim()) && String(e.title || '').trim(); }); BY_DATE = {}; ALL.forEach(function(e){ var k = String(e.date).trim(); (BY_DATE[k] = BY_DATE[k] || []).push(e); }); } readEvents(); function pad(n){ return (n < 10 ? '0' : '') + n; } function key(y,m,d){ return y + '-' + pad(m+1) + '-' + pad(d); } /* open on the month of the soonest event if there is one, so the calendar never lands on an empty month when dates exist */ var today = new Date(); today.setHours(0,0,0,0); var view = new Date(today.getFullYear(), today.getMonth(), 1); var moved = false; /* has the visitor used the arrows yet */ function landOnFirstEvent(){ if(moved) return; /* never yank the month out from under them */ var upcoming = ALL.map(function(e){ return e.date; }).sort() .filter(function(d){ return new Date(d + 'T00:00:00') >= today; })[0]; if(upcoming){ var u = new Date(upcoming + 'T00:00:00'); view = new Date(u.getFullYear(), u.getMonth(), 1); } } landOnFirstEvent(); function statusOf(e){ var s = String(e.status || 'available').toLowerCase(); return ['available','few','sold','coming-soon'].indexOf(s) > -1 ? s : 'available'; } function tagFor(s){ if(s === 'few') return '\x3cspan class="h-cal-tag is-few">A few seats left\x3c/span>'; if(s === 'sold') return '\x3cspan class="h-cal-tag is-sold">Sold out\x3c/span>'; if(s === 'coming-soon') return '\x3cspan class="h-cal-tag is-soon">Coming soon\x3c/span>'; return ''; } function render(){ var y = view.getFullYear(), m = view.getMonth(); label.textContent = MONTHS[m] + ' ' + y; /* JS weeks start on Sunday; Hera's calendar starts on Monday */ var firstDow = (new Date(y, m, 1).getDay() + 6) % 7; var days = new Date(y, m + 1, 0).getDate(); var cells = ''; for(var i = 0; i < firstDow; i++) cells += '\x3cspan class="h-cal-day is-blank">\x3c/span>'; for(var d = 1; d <= days; d++){ var k = key(y, m, d); var evs = BY_DATE[k] || []; var isToday = (new Date(y, m, d)).getTime() === today.getTime(); var cls = 'h-cal-day' + (evs.length ? ' has-ev' : '') + (isToday ? ' is-today' : ''); if(evs.length){ var allGone = evs.every(function(e){ return statusOf(e) === 'sold'; }); if(allGone) cls += ' is-sold'; cells += '\x3cbutton type="button" class="' + cls + '" data-date="' + k + '"' + ' aria-label="' + esc(d + ' ' + MONTHS[m] + ', ' + evs.length + (evs.length === 1 ? ' event' : ' events')) + '">' + '\x3cspan class="h-cal-n">' + d + '\x3c/span>' + '\x3cspan class="h-cal-dot" aria-hidden="true">\x3c/span>\x3c/button>'; }else{ cells += '\x3cspan class="' + cls + '">\x3cspan class="h-cal-n">' + d + '\x3c/span>\x3c/span>'; } } grid.innerHTML = cells; /* the month's events, listed under the grid */ var mine = ALL.filter(function(e){ var dt = new Date(e.date + 'T00:00:00'); return dt.getFullYear() === y && dt.getMonth() === m; }).sort(function(a,b){ return a.date < b.date ? -1 : 1; }); if(!mine.length){ listEl.innerHTML = '\x3cp class="h-cal-none">Nothing listed for ' + MONTHS[m] + '. Use the arrows to look ahead, or get in touch and we will tell you first.\x3c/p>'; }else{ listEl.innerHTML = mine.map(function(e){ var s = statusOf(e); var dt = new Date(e.date + 'T00:00:00'); return '\x3cbutton type="button" class="h-cal-item' + (s === 'sold' ? ' is-sold' : '') + '" data-date="' + esc(e.date) + '" data-title="' + esc(e.title) + '">' + '\x3cspan class="h-cal-item-d">\x3cb>' + dt.getDate() + '\x3c/b>\x3c/span>' + '\x3cspan class="h-cal-item-b">' + (e.type ? '\x3cspan class="h-cal-item-k">' + esc(e.type) + '\x3c/span>' : '') + '\x3cspan class="h-cal-item-t">' + esc(e.title) + '\x3c/span>' + (e.time || e.location ? '\x3cspan class="h-cal-item-m">' + [e.time, e.location].filter(Boolean).map(esc).join(' \x26#183; ') + '\x3c/span>' : '') + '\x3c/span>' + '\x3cspan class="h-cal-item-s">' + tagFor(s) + '\x3c/span>' + '\x3c/button>'; }).join(''); } if(window.__heraScan) window.__heraScan(); } function shift(n){ view = new Date(view.getFullYear(), view.getMonth() + n, 1); render(); } prev.addEventListener('click', function(){ moved = true; shift(-1); }); next.addEventListener('click', function(){ moved = true; shift(1); }); /* ---- the event panel ---------------------------------------- */ var panel = null, lastFocus = null; function buildPanel(){ if(panel) return; panel = document.createElement('div'); panel.className = 'h-evm'; panel.setAttribute('role','dialog'); panel.setAttribute('aria-modal','true'); panel.setAttribute('aria-label','Event details'); panel.innerHTML = '\x3cdiv class="h-evm-scrim" data-shut>\x3c/div>' + '\x3cdiv class="h-evm-panel">' + '\x3cbutton class="h-evm-close" type="button" data-shut aria-label="Close">' + '\x3csvg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" ' + 'stroke-width="1.5" aria-hidden="true">\x3cpath d="M1.5 1.5l12 12M13.5 1.5l-12 12" ' + 'stroke-linecap="round"/>\x3c/svg>\x3c/button>' + '\x3cdiv class="h-evm-body" id="h-evm-body">\x3c/div>' + '\x3c/div>'; document.body.appendChild(panel); panel.addEventListener('click', function(ev){ if(ev.target.closest('[data-shut]')) shut(); }); document.addEventListener('keydown', function(ev){ if(ev.key === 'Escape' && panel.classList.contains('is-open')) shut(); if(ev.key === 'Tab' && panel.classList.contains('is-open')) trap(ev); }); } function trap(ev){ var f = panel.querySelectorAll('button, [href]'); if(!f.length) return; var first = f[0], last = f[f.length - 1]; if(ev.shiftKey && document.activeElement === first){ ev.preventDefault(); last.focus(); } else if(!ev.shiftKey && document.activeElement === last){ ev.preventDefault(); first.focus(); } } function shut(){ if(!panel) return; panel.classList.remove('is-open'); if(window.HERA && HERA.hold) HERA.hold('experience', false); if(lastFocus) lastFocus.focus(); } function open(dateKey, onlyTitle){ buildPanel(); lastFocus = document.activeElement; var evs = (BY_DATE[dateKey] || []).slice(); if(onlyTitle) evs = evs.filter(function(e){ return e.title === onlyTitle; }); if(!evs.length) return; var dt = new Date(dateKey + 'T00:00:00'); var when = dt.getDate() + ' ' + MONTHS[dt.getMonth()] + ' ' + dt.getFullYear(); document.getElementById('h-evm-body').innerHTML = '\x3cp class="h-evm-date">' + esc(when) + '\x3c/p>' + evs.map(function(e){ var s = statusOf(e); var bookable = s !== 'sold' && s !== 'coming-soon' && String(e.link || '').trim(); var inc = String(e.includes || '').split(',') .map(function(x){ return x.trim(); }).filter(Boolean); return '\x3carticle class="h-evm-ev">' + (String(e.image || '').trim() ? '\x3cspan class="h-evm-fig">\x3cimg src="' + esc(e.image) + '" alt="' + esc(e.title) + '">\x3c/span>' : '') + (e.type ? '\x3cp class="h-eyebrow h-xo-eye">' + esc(e.type) + '\x3c/p>' : '') + '\x3ch4 class="h-evm-t">' + esc(e.title) + '\x3c/h4>' + ((e.time || e.location) ? '\x3cp class="h-evm-meta">' + [e.time, e.location].filter(Boolean).map(esc).join(' \x26#183; ') + '\x3c/p>' : '') + (String(e.details || e.summary || '').trim() ? '\x3cp class="h-evm-p">' + esc(e.details || e.summary) + '\x3c/p>' : '') + (inc.length ? '\x3cul class="h-evm-inc">' + inc.map(function(x){ return '\x3cli>' + esc(x) + '\x3c/li>'; }).join('') + '\x3c/ul>' : '') + '\x3cdiv class="h-evm-foot">' + (String(e.price || '').trim() ? '\x3cp class="h-evm-price">' + esc(e.price) + '\x3c/p>' : '') + tagFor(s) + '\x3c/div>' + (bookable ? '\x3cp class="h-evm-cta">\x3ca class="h-btn" href="' + esc(e.link) + '">' + esc(String(e.button || '').trim() || 'Reserve a seat') + '\x3c/a>\x3c/p>' : '') + '\x3c/article>'; }).join(''); panel.classList.add('is-open'); if(window.HERA && HERA.hold) HERA.hold('experience', true); panel.querySelector('.h-evm-close').focus(); } /* Each workshop has a page of its own now, so a click goes there rather than opening a summary over the calendar. The panel is kept for the one case it is still the right answer: a day with more than one thing on, where the visitor has to choose first. */ function goTo(e){ if(!e || !e.link){ return false; } if(window.HERA && HERA.workshops && e.id) HERA.workshops.remember(e.id); window.location.href = e.link; return true; } grid.addEventListener('click', function(ev){ var b = ev.target.closest('.h-cal-day.has-ev'); if(!b) return; var evs = BY_DATE[b.dataset.date] || []; if(evs.length === 1 && goTo(evs[0])) return; open(b.dataset.date); }); listEl.addEventListener('click', function(ev){ var b = ev.target.closest('.h-cal-item'); if(!b) return; var evs = (BY_DATE[b.dataset.date] || []).filter(function(e){ return e.title === b.dataset.title; }); if(evs.length && goTo(evs[0])) return; open(b.dataset.date, b.dataset.title); }); /* the sheet calls this once it has the workshops */ window.__heraCalendarRefresh = function(){ readEvents(); landOnFirstEvent(); render(); }; render(); /* Carrd holds a section's code back until the section is opened, so the workshops can arrive either before this runs or after it. The line above covers "after"; asking for them here covers "before", when the sheet had already answered and there was nothing yet listening to be told. */ if(window.HERA && HERA.workshops && HERA.workshops.ready){ HERA.workshops.ready(function(){ window.__heraCalendarRefresh(); }); } })();

          1:1 Floral Session

          Photo · 1:1 Session · hands in the flowers

          A little time, just for you and the flowers.

          Come spend a few slow hours with us at Hera. Choose a Hera-style bouquet or arrangement you would love to make and we'll guide you through it at your own pace.

          Learn a little about shape, form, flower selection and composition, sip some tea, make something beautiful and enjoy the process.

          Plan My SessionGift a Session

          However you arrive

          Come exactly as you are.

          Never touched flowers before? Perfect. We can start with the basics and guide you through the entire arrangement, from choosing stems to tying it off.

          Know a little already? Tell us what you would like more guidance on. We can spend the time on shape, movement, composition, colour, or simply helping you feel more confident arranging flowers on your own.

          No learning goal at all? That is a perfectly good reason to come. Sometimes you just want to make flowers, drink tea and have a lovely afternoon.

          What's included

          • Private guidance
          • Your chosen Hera style
          • Flowers & materials
          • Tea
          • Your finished arrangement to take home
          Photo · the table, ready

          A gift

          Give someone a few hours with flowers.

          Photo · what they take home

          A 1:1 Floral Session can be gifted for a birthday, a celebration, a thank-you, or simply because you know someone who would love an afternoon surrounded by flowers.

          We can arrange the details with you first and prepare the experience for the person receiving it.

          Gift a Session

          Made around what you want to create.

          Each 1:1 session is planned individually. The fee depends on the floral style, the flowers, the scale and the date you choose.

          Tell us a little about what you would like to make and your experience with flowers, and we'll recommend the session and confirm the fee with you before anything is booked.

          Plan My Session

          Plan your session

          Tell us what you'd love to make.

          There are no wrong answers here — if you are not sure yet, say so and we'll recommend something.

          Preferred contact method
          Is this session for you, or a gift?
          Is your date flexible?
          Your experience with flowers
          What would you like to make?
          What would you like from your session? (choose at least one)

          We'll review your request, recommend the session that suits what you want to make, and confirm availability and the fee with you before anything is booked. A copy of your request is sent to you as well.

          /* ================================================================== ## S Y S T E M C O D E -- D O N O T E D I T ## ================================================================== The 1:1 Floral Session form. Delivery is handled site-wide by 00-CONFIG-08, which reads the answers off the page -- so nothing here posts anything itself. This only does three things: open the two conditional boxes, check the required answers, and swap the form for the thank-you. ================================================================== */ (function(){ var form = document.getElementById('h-ss-form'); if(!form) return; function $(id){ return document.getElementById(id); } /* --- the two boxes that only appear when they are relevant --- */ function sync(){ var ref = $('ss-make-ref').checked, oth = $('ss-make-other').checked; $('ss-ref-f').hidden = !ref; $('ss-other-f').hidden = !oth; } form.addEventListener('change', function(e){ if(e.target && e.target.name === 'ss-make') sync(); }); sync(); /* --- "Gift a Session" arrives with the answer already ticked --- */ document.addEventListener('click', function(e){ var g = e.target && e.target.closest ? e.target.closest('[data-gift]') : null; if(!g) return; var box = $('ss-gift'); if(box){ box.checked = true; box.dispatchEvent(new Event('change', {bubbles:true})); } }); /* --- checking the answers --- */ function bad(el){ var f = el.closest('.h-bl-field'); if(f) f.classList.add('is-bad'); } /* the contact box follows whichever method was chosen */ function syncReach(){ var on = form.querySelector('input[name="ss-contact"]:checked'); var pick = on ? on.value : 'Zalo'; var box = $('ss-handle-f'), lab = $('ss-handle-l'), inp = $('ss-handle'); var wanted = (pick === 'Zalo' || pick === 'Instagram'); box.hidden = !wanted; if(!wanted) return; lab.textContent = pick === 'Zalo' ? 'Your Zalo number' : 'Your Instagram handle'; inp.placeholder = pick === 'Zalo' ? '+84 90 123 4567' : '@yourname'; inp.type = pick === 'Zalo' ? 'tel' : 'text'; } form.addEventListener('change', function(e){ if(e.target && e.target.name === 'ss-contact') syncReach(); }); syncReach(); form.addEventListener('submit', function(e){ e.preventDefault(); var missing = [], first = null; form.querySelectorAll('.is-bad').forEach(function(f){ f.classList.remove('is-bad'); }); function need(id, what){ var el = $(id); if(!el || el.closest('[hidden]')) return; if(!String(el.value || '').trim()){ bad(el); missing.push(what); if(!first) first = el; } } need('ss-name', 'your name'); need('ss-handle', 'your Zalo number or Instagram handle'); need('ss-help', 'what you would like help with'); need('ss-notes', 'anything else we should know'); need('ss-email', 'your email'); need('ss-phone', 'your phone number'); need('ss-date', 'a preferred date'); need('ss-time', 'a time of day'); if($('ss-make-ref').checked) need('ss-ref', 'which Hera arrangement'); if($('ss-make-other').checked) need('ss-other', 'what you would like to make'); /* at least one thing they want from the session */ var wants = form.querySelectorAll('input[name="ss-want"]:checked').length; if(!wants){ var wf = $('ss-wantl').closest('.h-bl-field'); if(wf) wf.classList.add('is-bad'); missing.push('what you would like from your session'); if(!first) first = form.querySelector('input[name="ss-want"]'); } var em = $('ss-email'); if(em.value.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em.value.trim())){ bad(em); missing.push('a valid email address'); if(!first) first = em; } var err = $('h-ss-err'); if(missing.length){ err.hidden = false; err.textContent = 'We still need ' + (missing.length === 1 ? missing[0] : missing.slice(0, -1).join(', ') + ' and ' + missing[missing.length - 1]) + '.'; if(first && first.focus) first.focus(); return; } err.hidden = true; /* A reference so they can quote it back to us. */ var ref = 'HS-' + String(Date.now()).slice(-6); var out = $('h-ss-ref-out'); if(out) out.textContent = 'Your reference: ' + ref; form.hidden = true; $('h-ss-done').hidden = false; $('h-ss-done').scrollIntoView({behavior:'smooth', block:'center'}); }); })();

          Floral Masterclass

          Photo · Masterclass · a demonstration in progress

          Go deeper into the way you make flowers.

          A more focused floral learning experience, built around the skills you actually want to develop.

          Choose a specific bouquet, arrangement, technique or area of floral design you want to understand better. We'll shape the masterclass around your current level, your goals and the time you want to spend learning.

          Build My Masterclass

          Where it begins

          Start with what you want to learn.

          There is no set curriculum here that everybody has to work through. One person arrives wanting to know exactly how we build a particular bouquet. Another wants to understand movement, structure, mechanics, colour, flower selection, proportion, finishing — or how to look at a reference and translate it into an arrangement of their own.

          So we ask first. We want to understand where you are now and what you want to be able to do, and then we recommend a structure that gets you there.

          Photo · the detail being worked
          • 01A particular arrangementTake one Hera bouquet or arrangement apart and rebuild it properly.
          • 02TechniqueHand-tying, mechanics, structure, and the finishing that holds it all.
          • 03CompositionShape, movement, proportion — why an arrangement reads the way it does.
          • 04Colour & selectionChoosing flowers that work together, and knowing what to leave out.

          Format

          • 1:1 or private small group
          • One day or multi-day
          • Technique-focused
          • Arrangement-focused
          • Tailored to your level
          Photo · the finished piece

          The fee

          Your masterclass is built around you.

          Which is exactly why there is no single price on this page. Two people can ask for a masterclass and need completely different things.

          What the fee depends on

          • Your current skill level
          • What you want to be able to do by the end
          • The arrangement or style you want to work on
          • The flowers and materials it needs
          • How many people are learning
          • How many sessions, and how long

          Send us your enquiry first. We'll talk it through with you, then propose the format and the fee before anything is confirmed.

          Tell Us What You Want to Learn

          Build your masterclass

          What do you want to be able to do?

          The more you tell us here, the more useful our recommendation will be. If you are not sure about the format yet, leave it to us.

          Preferred contact method
          Is this for
          Your current floral experience
          What would you like to learn? (choose at least one)

          We'll review your experience level and what you want to achieve, then come back with a recommended masterclass structure and the fee. A copy of your request is sent to you as well.

          /* ================================================================== ## S Y S T E M C O D E -- D O N O T E D I T ## ================================================================== The Masterclass form. Delivery is handled site-wide by 00-CONFIG-08, so nothing here posts anything itself. This opens the participant-count box when it is relevant, checks the answers Hera cannot quote without, and swaps in the thank-you. ================================================================== */ (function(){ var form = document.getElementById('h-mcf-form'); if(!form) return; function $(id){ return document.getElementById(id); } /* how many people is only asked when it is not just them */ function sync(){ var many = $('mc-fmt-2').checked || $('mc-fmt-g').checked; $('mc-count-f').hidden = !many; } form.addEventListener('change', function(e){ if(e.target && e.target.name === 'mc-format') sync(); }); sync(); /* the contact box follows whichever method was chosen */ function syncReach(){ var on = form.querySelector('input[name="mc-contact"]:checked'); var pick = on ? on.value : 'Zalo'; var box = $('mc-handle-f'), lab = $('mc-handle-l'), inp = $('mc-handle'); var wanted = (pick === 'Zalo' || pick === 'Instagram'); box.hidden = !wanted; if(!wanted) return; lab.textContent = pick === 'Zalo' ? 'Your Zalo number' : 'Your Instagram handle'; inp.placeholder = pick === 'Zalo' ? '+84 90 123 4567' : '@yourname'; inp.type = pick === 'Zalo' ? 'tel' : 'text'; } form.addEventListener('change', function(e){ if(e.target && e.target.name === 'mc-contact') syncReach(); }); syncReach(); form.addEventListener('submit', function(e){ e.preventDefault(); var missing = [], first = null; form.querySelectorAll('.is-bad').forEach(function(f){ f.classList.remove('is-bad'); }); function need(id, what){ var el = $(id); if(!el || el.closest('[hidden]')) return; if(!String(el.value || '').trim()){ var f = el.closest('.h-bl-field'); if(f) f.classList.add('is-bad'); missing.push(what); if(!first) first = el; } } need('mc-name', 'your name'); need('mc-handle', 'your Zalo number or Instagram handle'); need('mc-email', 'your email'); need('mc-phone', 'your phone number'); need('mc-date', 'a preferred start date'); need('mc-len', 'how much time you would like'); need('mc-want', 'what you want to learn'); need('mc-goal', 'what you would like to be able to do'); need('mc-ref', 'a reference'); need('mc-notes', 'anything else we should know'); if($('mc-fmt-2').checked || $('mc-fmt-g').checked) need('mc-count', 'how many people'); /* at least one learning area */ var picked = form.querySelectorAll('input[name="mc-learn"]:checked').length; if(!picked){ var lf = $('mc-learnl').closest('.h-bl-field'); if(lf) lf.classList.add('is-bad'); missing.push('at least one learning area ticked'); if(!first) first = form.querySelector('input[name="mc-learn"]'); } var em = $('mc-email'); if(em.value.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em.value.trim())){ var f2 = em.closest('.h-bl-field'); if(f2) f2.classList.add('is-bad'); missing.push('a valid email address'); if(!first) first = em; } var err = $('h-mcf-err'); if(missing.length){ err.hidden = false; err.textContent = 'We still need ' + (missing.length === 1 ? missing[0] : missing.slice(0, -1).join(', ') + ' and ' + missing[missing.length - 1]) + '.'; if(first && first.focus) first.focus(); return; } err.hidden = true; var ref = 'MC-' + String(Date.now()).slice(-6); var out = $('h-mcf-ref-out'); if(out) out.textContent = 'Your reference: ' + ref; form.hidden = true; $('h-mcf-done').hidden = false; $('h-mcf-done').scrollIntoView({behavior:'smooth', block:'center'}); }); })();

          Private Group Workshop

          Photo · Private group · a table of people making flowers

          Celebrate with flowers.

          Bring your people together around a table full of flowers.

          From birthdays and bridal gatherings to team days, private celebrations, or simply an excuse to spend time together — we'll shape a floral workshop around your group, your occasion and the way you want it to feel.

          Plan Our Workshop

          The shape of the day

          Your people. Your place. Your flowers.

          Almost everything about a private workshop is yours to decide. Tell us what you have in mind and we'll build it around you.

          • Your groupHowever many of you there are
          • The occasionOr no occasion at all
          • WhereAt Hera, or somewhere that suits you
          • Floral styleThe colours and the feeling you imagine
          • What everyone makesBouquets, vases, baskets, something seasonal
          • The paceUnhurried, and built around the room

          Sometimes the occasion is simply being together.

          Photo · the room, mid-workshop

          Where it happens

          Private workshops can be hosted at Hera, or at another suitable venue or location depending on your group and what the setup needs. Tell us where you are imagining it and we'll let you know what's possible.

          The quote

          Priced around your group and your concept.

          Every group is a different size, in a different place, making something different. So we quote each one properly rather than publishing a per-head number that would be wrong for most of you.

          What the quote depends on

          • How many guests are coming
          • The floral concept and style
          • The flowers and materials it needs
          • Where it is being held
          • The setup the location requires

          Send us the details and we'll come back to you with a concept and a quote for the whole gathering.

          Tell Us About Your Group

          Plan your workshop

          Tell us about your group.

          Even rough answers are useful — numbers and dates can move later.

          Preferred contact method
          What are we celebrating?
          Is the date flexible?
          Where would you like to host it?
          What would you like everyone to make?

          We'll look at your group size, the concept, the location and the date, then come back to talk it through and give you a quote. A copy of your request is sent to you as well.

          /* ================================================================== ## S Y S T E M C O D E -- D O N O T E D I T ## ================================================================== The Private Group Workshop form. Delivery is handled site-wide by 00-CONFIG-08, so nothing here posts anything itself. This opens the two conditional blocks -- the occasion box and the venue pair -- checks the answers, and swaps in the thank-you. ================================================================== */ (function(){ var form = document.getElementById('h-gwf-form'); if(!form) return; function $(id){ return document.getElementById(id); } /* The venue pair is only asked when it is not at Hera, and not when they have not decided yet -- there is nothing to type in either case. */ function sync(){ $('gw-occ-f').hidden = !$('gw-occ-other').checked; var away = !$('gw-where-hera').checked && !$('gw-where-uns').checked; $('gw-venue-f').hidden = !away; } form.addEventListener('change', function(e){ if(e.target && (e.target.name === 'gw-where' || e.target.name === 'gw-occ')) sync(); }); sync(); /* the contact box follows whichever method was chosen */ function syncReach(){ var on = form.querySelector('input[name="gw-contact"]:checked'); var pick = on ? on.value : 'Zalo'; var box = $('gw-handle-f'), lab = $('gw-handle-l'), inp = $('gw-handle'); var wanted = (pick === 'Zalo' || pick === 'Instagram'); box.hidden = !wanted; if(!wanted) return; lab.textContent = pick === 'Zalo' ? 'Your Zalo number' : 'Your Instagram handle'; inp.placeholder = pick === 'Zalo' ? '+84 90 123 4567' : '@yourname'; inp.type = pick === 'Zalo' ? 'tel' : 'text'; } form.addEventListener('change', function(e){ if(e.target && e.target.name === 'gw-contact') syncReach(); }); syncReach(); form.addEventListener('submit', function(e){ e.preventDefault(); var missing = [], first = null; form.querySelectorAll('.is-bad').forEach(function(f){ f.classList.remove('is-bad'); }); function need(id, what){ var el = $(id); if(!el || el.closest('[hidden]')) return; if(!String(el.value || '').trim()){ var f = el.closest('.h-bl-field'); if(f) f.classList.add('is-bad'); missing.push(what); if(!first) first = el; } } need('gw-name', 'your name'); need('gw-handle', 'your Zalo number or Instagram handle'); need('gw-email', 'your email'); need('gw-phone', 'your phone number'); need('gw-guests', 'roughly how many guests'); need('gw-date', 'a preferred date'); need('gw-time', 'a preferred time'); need('gw-about', 'a little about the occasion'); need('gw-mood', 'the style or mood'); need('gw-ref', 'a reference'); need('gw-special', 'any special requests'); if($('gw-occ-other').checked) need('gw-occ-txt', 'the occasion'); if(!$('gw-venue-f').hidden){ need('gw-venue', 'the venue name'); need('gw-area', 'the address or area'); } var em = $('gw-email'); if(em.value.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em.value.trim())){ var f2 = em.closest('.h-bl-field'); if(f2) f2.classList.add('is-bad'); missing.push('a valid email address'); if(!first) first = em; } var err = $('h-gwf-err'); if(missing.length){ err.hidden = false; err.textContent = 'We still need ' + (missing.length === 1 ? missing[0] : missing.slice(0, -1).join(', ') + ' and ' + missing[missing.length - 1]) + '.'; if(first && first.focus) first.focus(); return; } err.hidden = true; var ref = 'GW-' + String(Date.now()).slice(-6); var out = $('h-gwf-ref-out'); if(out) out.textContent = 'Your reference: ' + ref; form.hidden = true; $('h-gwf-done').hidden = false; $('h-gwf-done').scrollIntoView({behavior:'smooth', block:'center'}); }); })();
          /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 1. Y O U R P H O T O G R A P H S ================================================================== This page is deliberately spare with pictures: one hero, one editorial photograph, and a small strip. That restraint is what makes it read as an experience rather than a brochure, so please resist adding more frames. bxHero THE OPENING. Your strongest Bloom Bar photograph, with people in it. Wide. Choose one where the flowers and the guests both survive a wide crop. bxMoment THE ONE EDITORIAL PICTURE, further down. Wide, calm, and different from the hero. Upload each in Carrd, copy the address, paste it between the quote marks, and LEAVE the Image element on the page -- Carrd deletes any picture that nothing is using. Leave either as "" and a quiet labelled space holds its place. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ bxHero: "", bxMoment: "" }); /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 2. W H E R E T O C R O P O N A P H O N E ================================================================== A phone is tall and narrow, so a wide photograph loses its left and right edges. By default the middle is kept, which can cut off whoever is standing to one side. first number = left to right 0% left, 50% middle, 100% right second number = top to bottom 0% top, 50% middle, 100% bottom ================================================================== */ window.HERA_BX_CROP = { bxHero: "50% 50%", bxMoment: "50% 50%" }; /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 3. T H E S T R I P ================================================================== A few moments from around the table. Six to ten works well. They all share one height and keep their own natural width, so an upright photograph and a wide one sit together without either being cropped into a square. src the picture address from Carrd cap a short label, or "" for none. Keep them to two words. ================================================================== */ window.HERA_BX_RAIL = [ { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/LF_01264", cap:"01" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4613", cap:"02" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/LF_01313", cap:"03" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/LF_01358_2", cap:"04" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4612", cap:"05" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4614", cap:"06" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4615", cap:"07" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/LF_01253", cap:"08" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4616", cap:"09" } ]; /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 4. T H E T H R E E B L O O M B A R S ================================================================== Three levels of the same experience. Guests always choose their own flowers; what changes is how much help they get afterwards. no the numeral name the title. Use a \x3cbr> to break it over two lines. kind the one-line descriptor body what it is, in two sentences has what is included. One line each. tag SELF-SERVE / FLORIST-ASSISTED / FULL-SERVICE note the sentence under that tag cta the words on its link want which option the form should choose when someone arrives from that link. Must match section 5 exactly. flag a small label above the column, or "" for none NOTE ON PRICING: no prices appear on this page. If you ever want a line about it, the Details section further down is where it belongs. ================================================================== */ window.HERA_BX_LEVELS = [ { no:"01", name:"Mini\x26nbsp;Bloom", kind:"The self-serve experience.", body:"A curated flower bar, prepared and left for your guests. They choose what they love and make their own bouquet, in their own time.", has:["Seasonal flower selection", "Focal flowers", "Fillers and greenery", "Ribbon and wrapping materials", "Flower quantity calculated by guest count", "Bloom Bar prepared for self-service"], tag:"Self-serve", note:"No florist stays at the bar during the event. Guests select and create their own bouquets.", cta:"Inquire about Mini", want:"Mini Bloom", flag:"" }, { no:"02", name:"Signature\x3cbr>Bloom\x26nbsp;Bar", kind:"The florist-assisted experience.", body:"Guests choose the flowers they love from the bar. A Hera florist then wraps each guest's own selection into a finished bouquet to take home.", has:["Premium seasonal flowers", "Curated flower selection", "Hera wrapping and ribbons", "Flower buckets and display", "Setup and styling", "Flower quantity calculated by guest count", "On-site florist wrapping"], tag:"Florist-assisted", note:"Guests choose their own flowers; the florist wraps that selection. How many florists are needed is set by guest count and the event.", cta:"Inquire about Signature", want:"Signature Bloom Bar", flag:"Most popular" }, { no:"03", name:"Luxe\x3cbr>Bloom\x26nbsp;Bar", kind:"The elevated experience.", body:"A more elevated version: premium blooms, a wider selection, a styled bar and Hera florists with your guests throughout.", has:["Premium imported blooms", "Expanded flower selection", "Custom colour palette available", "Styled Bloom Bar setup", "Signature Hera packaging", "Setup and styling", "Flower quantity calculated by guest count", "On-site florist wrapping and assistance"], tag:"Full-service", note:"Guests still choose their own flowers. How many florists are needed is set by guest count, the size of the event and what it asks for.", cta:"Inquire about Luxe", want:"Luxe Bloom Bar", flag:"" } ]; /* 5. What the form asks first. The first three must match the "want" lines above exactly, or the links will not preselect anything. */ window.HERA_BX_WANTS = ["Mini Bloom", "Signature Bloom Bar", "Luxe Bloom Bar", "Not sure yet"]; /* 6. What is shaped around your event. Four at most. */ window.HERA_BX_SHAPED = [ { t:"Guest count", b:"Flower quantities and staffing are planned around the number of guests." }, { t:"Flowers", b:"Seasonal availability decides the exact varieties on the bar." }, { t:"Colour", b:"A Bloom Bar can be curated around the mood and palette of your event." }, { t:"Experience", b:"From entirely self-serve to fully florist-assisted, choose the level that suits the gathering." } ]; /* 7. The practical things. Add or remove a row freely. */ window.HERA_BX_DETAILS = [ { t:"Minimum", b:"Bloom Bar is designed for a minimum of 15 guests." }, { t:"Flower quantity", b:"The amount of flowers is calculated according to guest count and the experience chosen." }, { t:"Florist staffing", b:"Signature and Luxe include florist support. How many florists are needed is determined by guest count and the event." }, { t:"Delivery", b:"Delivery is quoted separately and is not part of the package." }, { t:"Customisation", b:"Every package can be shaped around your event, palette and requirements." }, { t:"Seasonality", b:"Flower selection and pricing move with the season and the flower market." }, { t:"Pricing", b:"Shared on inquiry, once we know the guest count and the shape of the event." } ];
          Photo · Bloom Bar · the opening picture

          Hera Experiences

          Bloom Bar

          Build your own bouquet experience.

          Curated blooms for events, weddings, birthdays, cafes, brand activations and intimate gatherings.

          Discover Bloom Bar

          The experience

          A flower bar,
          made to be yours.

          Bloom Bar is Hera's build-your-own bouquet experience. We curate a selection of seasonal flowers around your occasion, then invite your guests to step up, choose what they love and leave with flowers of their own.

          From intimate gatherings to weddings and brand activations, each Bloom Bar is created around the people, space and mood of the event.

          The process

          How it works.

          1. We curate

            Flowers are selected around your event, the season, the guest count and the experience you choose.

          2. They choose

            Guests step up to the bar and choose the flowers that speak to them.

          3. They take it home

            Depending on the Bloom Bar you choose, guests make the bouquet themselves or a Hera florist wraps their selection.

          Photo · guests choosing flowers

          Choose what you love.

          We'll take care of the rest.

          Choose your experience

          Three ways to Bloom.

          Every Bloom Bar begins with the same idea: beautiful flowers, and something your guests get to make their own.

            The difference

            Self-serve

            Mini Bloom

            You choose.
            You arrange.
            You take it home.

            Hera prepares the flowers and the bar before the event. Guests enjoy it on their own, selecting and arranging their own flowers.

            No florist is stationed at the bar.

            Florist-assisted

            Signature + Luxe

            You choose.
            We wrap.

            Guests choose the flowers they want from the bar. A Hera florist then wraps those selections into finished bouquets to take home.

            Staffing is set by guest count, so the bar keeps moving.

            No two Bloom Bars
            need to look the same.

            The flowers, the quantities, the palette and the level of florist support are shaped around your guest count, your occasion and the experience you choose.

              Bloom Bar in motion

              A few moments from around the table.

              The details

              A few things to know.

                They choose the flowers.They leave with the memory.

                Bloom Bar

                Bring the flower bar
                to your next gathering.

                Tell us a little about your event and we'll help you choose the Bloom Bar experience that fits.

                Which Bloom Bar are you thinking about?

                Ask us a question
                A var __hb6="\n/* ==================================================================\n Nothing below this line needs editing.\n ================================================================== */\n(function(){\n function esc(s){\n return String(s == null ? '' : s)\n .replace(/&(?!#?\\w+;)/g, '\x26amp;').replace(/\x3c/g, '\x26lt;')\n .replace(/>/g, '\x26gt;').replace(/\"/g, '\x26quot;');\n }\n function copy(s){ return String(s == null ? '' : s); }\n var ARROW = '\x3csvg width=\"17\" height=\"10\" viewBox=\"0 0 17 10\" fill=\"none\" ' +\n 'stroke=\"currentColor\" stroke-width=\"1.1\" aria-hidden=\"true\">' +\n '\x3cpath d=\"M0 5h15M11.5 1 15.5 5l-4 4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\x3c/svg>';\n var REDUCED = window.matchMedia('(prefers-reduced-motion:reduce)').matches;\n\n var LEVELS = window.HERA_BX_LEVELS || [];\n\n /* ---- 05 the three Bloom Bars -------------------------------------\n Three columns on fine rules. Signature carries a small brass label\n because it is the one most people choose, and nothing more. */\n var lv = document.getElementById('h-bx-levels');\n if(lv){\n lv.innerHTML = LEVELS.map(function(p, i){\n var flag = String(p.flag || '').trim()\n ? '\x3cspan class=\"h-bx-lv-flag\">' + copy(p.flag) + '\x3c/span>' : '';\n var has = (p.has || []).map(function(h){\n return '\x3cli>' + copy(h) + '\x3c/li>'; }).join('');\n return '\x3cli class=\"h-bx-lv-i h-rev' + (flag ? ' is-lead' : '') +\n '\" style=\"--d:' + (i * 110) + 'ms\">' +\n flag +\n '\x3cspan class=\"h-bx-lv-n\" aria-hidden=\"true\">' + copy(p.no) + '\x3c/span>' +\n '\x3ch3 class=\"h-bx-lv-t\">' + copy(p.name) + '\x3c/h3>' +\n '\x3cp class=\"h-bx-lv-k\">' + copy(p.kind) + '\x3c/p>' +\n '\x3cp class=\"h-bx-lv-b\">' + copy(p.body) + '\x3c/p>' +\n '\x3cul class=\"h-bx-lv-l\">' + has + '\x3c/ul>' +\n '\x3cp class=\"h-bx-lv-tag\">' + copy(p.tag) + '\x3c/p>' +\n '\x3cp class=\"h-bx-lv-note\">' + copy(p.note) + '\x3c/p>' +\n '\x3ca class=\"h-bx-lv-go\" href=\"#bloom-bar-request\" data-want=\"' + esc(p.want) + '\">' +\n copy(p.cta) + ARROW + '\x3c/a>' +\n '\x3c/li>';\n }).join('');\n }\n\n /* ---- 07 shaped around the event ----------------------------------- */\n var shaped = document.getElementById('h-bx-shaped');\n if(shaped){\n shaped.innerHTML = (window.HERA_BX_SHAPED || []).map(function(s, i){\n return '\x3cli class=\"h-bx-shape-i h-rev\" style=\"--d:' + (i * 70) + 'ms\">' +\n '\x3cp class=\"h-bx-shape-t\">' + copy(s.t) + '\x3c/p>' +\n '\x3cp class=\"h-bx-shape-b\">' + copy(s.b) + '\x3c/p>\x3c/li>';\n }).join('');\n }\n\n /* ---- 09 the details ------------------------------------------------ */\n var det = document.getElementById('h-bx-details');\n if(det){\n det.innerHTML = (window.HERA_BX_DETAILS || []).map(function(d, i){\n return '\x3cli class=\"h-bx-det-i h-rev\" style=\"--d:' + ((i % 4) * 60) + 'ms\">' +\n '\x3cp class=\"h-bx-det-t\">' + copy(d.t) + '\x3c/p>' +\n '\x3cp class=\"h-bx-det-b\">' + copy(d.b) + '\x3c/p>\x3c/li>';\n }).join('');\n }\n\n /* ---- 07 the strip ------------------------------------------------\n One height, natural widths. Drifts very slowly on its own until\n the moment anyone touches it, and then never again. */\n var rail = document.getElementById('h-bx-rail');\n var SHOTS = (window.HERA_BX_RAIL || []).filter(function(s){\n return String(s && s.src || '').trim();\n });\n if(rail){\n if(!SHOTS.length){\n /* nothing to show yet: hold the space with quiet labelled frames\n rather than collapsing the section out of the page */\n rail.innerHTML = (window.HERA_BX_RAIL || []).slice(0, 6).map(function(s, i){\n return '\x3cfigure class=\"h-bx-shot is-empty h-rev\" style=\"--d:' + ((i % 4) * 70) + 'ms\">' +\n '\x3cspan class=\"h-quiet-ph\">\x3cspan>' + esc(s.cap || 'Bloom Bar') + '\x3c/span>\x3c/span>' +\n '\x3c/figure>';\n }).join('');\n }else{\n rail.innerHTML = SHOTS.map(function(s, i){\n var cap = String(s.cap || '').trim()\n ? '\x3cfigcaption class=\"h-bx-shot-c\">' + copy(s.cap) + '\x3c/figcaption>' : '';\n return '\x3cfigure class=\"h-bx-shot h-rev\" style=\"--d:' + ((i % 4) * 70) + 'ms\">' +\n '\x3cimg src=\"' + esc(s.src) + '\" alt=\"' + esc(s.cap || 'A Hera Bloom Bar') +\n '\" loading=\"lazy\" draggable=\"false\">' + cap + '\x3c/figure>';\n }).join('');\n }\n\n var touched = false, drift = 0;\n function stopDrift(){\n touched = true;\n cancelAnimationFrame(drift);\n }\n ['pointerdown','wheel','touchstart','keydown'].forEach(function(ev){\n rail.addEventListener(ev, stopDrift, {passive:true});\n });\n\n /* the drift: about six pixels a second, and it gives up at the end\n rather than looping back to the start */\n if(!REDUCED && SHOTS.length > 2){\n var last = 0;\n var run = function(now){\n if(touched) return;\n if(!last) last = now;\n var step = (now - last) * 0.006;\n last = now;\n var max = rail.scrollWidth - rail.clientWidth;\n if(rail.scrollLeft >= max - 1) return;\n rail.scrollLeft += step;\n drift = requestAnimationFrame(run);\n };\n var io = new IntersectionObserver(function(en){\n en.forEach(function(e){\n if(e.isIntersecting && !touched){ last = 0; drift = requestAnimationFrame(run); }\n else cancelAnimationFrame(drift);\n });\n }, {threshold:0.35});\n io.observe(rail);\n }\n\n /* dragging with a mouse; a trackpad and a phone already scroll */\n var down = false, sx = 0, sl = 0, vel = 0, lx = 0, glide = 0;\n function coast(){\n vel *= 0.94;\n if(Math.abs(vel) < 0.4){ rail.classList.remove('is-drag'); return; }\n rail.scrollLeft -= vel;\n glide = requestAnimationFrame(coast);\n }\n rail.addEventListener('pointerdown', function(e){\n if(e.pointerType === 'touch') return;\n down = true; vel = 0; sx = lx = e.clientX; sl = rail.scrollLeft;\n cancelAnimationFrame(glide);\n rail.classList.add('is-drag');\n rail.setPointerCapture(e.pointerId);\n });\n rail.addEventListener('pointermove', function(e){\n if(!down) return;\n vel = e.clientX - lx; lx = e.clientX;\n rail.scrollLeft = sl - (e.clientX - sx);\n });\n function release(e){\n if(!down) return;\n down = false;\n try{ rail.releasePointerCapture(e.pointerId); }catch(err){}\n if(Math.abs(vel) > 1) glide = requestAnimationFrame(coast);\n else rail.classList.remove('is-drag');\n }\n rail.addEventListener('pointerup', release);\n rail.addEventListener('pointercancel', release);\n\n function ends(){\n var max = rail.scrollWidth - rail.clientWidth;\n var p = document.getElementById('h-bx-prev'),\n n = document.getElementById('h-bx-next');\n if(p) p.disabled = rail.scrollLeft <= 2;\n if(n) n.disabled = rail.scrollLeft >= max - 2;\n }\n function glideTo(target){\n var from = rail.scrollLeft;\n var max = rail.scrollWidth - rail.clientWidth;\n target = Math.max(0, Math.min(target, max));\n var dist = target - from;\n if(Math.abs(dist) < 1) return;\n if(REDUCED){ rail.scrollLeft = target; ends(); return; }\n cancelAnimationFrame(glide);\n var t0 = 0, ms = 460, ran = false;\n glide = requestAnimationFrame(function frame(now){\n ran = true;\n if(!t0) t0 = now;\n var p = Math.min(1, (now - t0) / ms);\n rail.scrollLeft = from + dist * (1 - Math.pow(1 - p, 3));\n ends();\n if(p < 1) glide = requestAnimationFrame(frame);\n });\n /* animation frames are withheld from a hidden tab, and a press\n that moves nothing reads as a broken control */\n setTimeout(function(){\n if(ran) return;\n cancelAnimationFrame(glide);\n rail.scrollLeft = target; ends();\n }, 90);\n }\n function step(dir){ stopDrift(); glideTo(rail.scrollLeft + dir * rail.clientWidth * 0.8); }\n var pv = document.getElementById('h-bx-prev'),\n nx = document.getElementById('h-bx-next');\n if(pv) pv.addEventListener('click', function(){ step(-1); });\n if(nx) nx.addEventListener('click', function(){ step(1); });\n rail.addEventListener('scroll', ends, {passive:true});\n window.addEventListener('resize', ends);\n rail.addEventListener('keydown', function(e){\n if(e.key === 'ArrowRight'){ e.preventDefault(); step(1); }\n if(e.key === 'ArrowLeft'){ e.preventDefault(); step(-1); }\n });\n ends();\n }\n\n /* ---- 10 the inquiry ----------------------------------------------- */\n var want = '';\n var wantBox = document.getElementById('h-bx-want');\n if(wantBox){\n wantBox.innerHTML = (window.HERA_BX_WANTS || []).map(function(w){\n return '\x3clabel class=\"h-bx-seg-o\">' +\n '\x3cinput type=\"radio\" name=\"h-bx-want\" value=\"' + esc(w) + '\">' +\n '\x3cspan>' + esc(w) + '\x3c/span>\x3c/label>';\n }).join('');\n wantBox.addEventListener('change', function(e){\n if(!e.target || e.target.type !== 'radio') return;\n want = e.target.value;\n document.getElementById('h-bx-want-e').textContent = '';\n wantBox.classList.remove('is-bad');\n });\n }\n /* every link on the page arrives with its answer already given */\n document.addEventListener('click', function(e){\n var a = e.target.closest('a[data-want]');\n if(!a || !wantBox) return;\n var r = wantBox.querySelector('input[value=\"' + String(a.dataset.want).replace(/\"/g, '') + '\"]');\n if(!r) return;\n r.checked = true;\n r.dispatchEvent(new Event('change', {bubbles:true}));\n });\n\n var form = document.getElementById('h-bx-form');\n if(form){\n /* every answer is needed: a Bloom Bar cannot be quoted without the\n date, the place and the guest count, and the palette shapes what\n we buy */\n var NEED = [\n {id:'bx-name', msg:'Please add your name.'},\n {id:'bx-type', msg:'Please choose one.'},\n {id:'bx-email', msg:'Please add an email address.', email:true},\n {id:'bx-phone', msg:'Please add a phone or WhatsApp number.'},\n {id:'bx-date', msg:'Please add a date.'},\n {id:'bx-where', msg:'Please add a location.'},\n {id:'bx-pax', msg:'Please add a rough number of guests.'},\n {id:'bx-palette', msg:'Please tell us a colour or a mood.'},\n {id:'bx-idea', msg:'Please tell us a little here.'}\n ];\n form.addEventListener('submit', function(e){\n e.preventDefault();\n var bad = 0, firstBad = null;\n NEED.forEach(function(n){\n var el = document.getElementById(n.id);\n var box = form.querySelector('[data-f=\"' + n.id + '\"]');\n var err = document.getElementById(n.id + '-e');\n if(!el || !box || !err) return;\n box.classList.remove('is-bad'); err.textContent = '';\n var v = (el.value || '').trim();\n var msg = '';\n if(!v) msg = n.msg;\n else if(n.email && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v))\n msg = 'That does not look like an email address.';\n if(msg){\n box.classList.add('is-bad'); err.textContent = msg;\n bad++; if(!firstBad) firstBad = el;\n }\n });\n var we = document.getElementById('h-bx-want-e');\n if(we){\n we.textContent = '';\n wantBox.classList.remove('is-bad');\n if(!want){\n we.textContent = 'Please choose one.';\n wantBox.classList.add('is-bad');\n bad++;\n if(!firstBad) firstBad = wantBox.querySelector('input');\n }\n }\n var sum = document.getElementById('h-bx-sum');\n if(bad){\n if(sum){\n sum.hidden = false;\n sum.textContent = bad === 1\n ? 'One answer still needs your attention.'\n : bad + ' answers still need your attention.';\n }\n if(firstBad && firstBad.focus) firstBad.focus();\n return;\n }\n if(sum) sum.hidden = true;\n var host = form.parentElement;\n form.style.transition = 'opacity .26s cubic-bezier(.22,1,.36,1)';\n form.style.opacity = '0';\n setTimeout(function(){\n form.outerHTML =\n '\x3cdiv class=\"h-bx-done\">' +\n '\x3cp class=\"h-bx-eye h-bx-eye--pale\">Received\x3c/p>' +\n '\x3cp class=\"h-bx-done-h\">It\\u2019s in our hands.\x3c/p>' +\n '\x3cp class=\"h-bx-done-p\">Thank you for thinking of Hera. We\\u2019ll read the ' +\n 'details and get back to you directly.\x3c/p>' +\n '\x3cp class=\"h-bx-fine h-bx-fine--pale\">A copy has been emailed to you as well, so you have a record of it.\x3c/p>' +\n '\x3c/div>';\n var d = host.querySelector('.h-bx-done');\n if(d) d.scrollIntoView({";
                __hb6+="behavior:'smooth', block:'center'});\n }, 280);\n });\n }\n\n /* ---- phone crops --------------------------------------------------- */\n var CROP = window.HERA_BX_CROP || {};\n Object.keys(CROP).forEach(function(k){\n var v = String(CROP[k] || '').trim();\n if(!v) return;\n var els = document.querySelectorAll('[data-photo=\"' + k + '\"]');\n for(var i = 0; i < els.length; i++) els[i].style.setProperty('--focus-m', v);\n });\n\n if(window.__heraApplyPhotos) window.__heraApplyPhotos();\n if(window.__heraScan) window.__heraScan();\n})();\n";(function(){var s=document.createElement("script");s.textContent=__hb6;document.head.appendChild(s);__hb6=null;})();
                Skip to content
                Floral studio · Đà Nẵng · Việt Nam
                /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== E D I T Y O U R P H O T O S ================================================================== Two photographs, and they carry the whole page. Choose them for MOOD as much as for content -- a visitor should feel the difference between the two halves before reading a word. evPrivate Romantic, intimate, personal. A proposal setting, a table laid for two, candlelight. Warm. evBrands Polished and at scale. An installation in a hotel or venue, a brand activation, something built. Upload each in Carrd, copy the address, and paste it between the quote marks. LEAVE the Image element on the page afterwards -- Carrd deletes any picture that nothing is using. Leave either as "" and a labelled placeholder stays in its place, so the page works before the photography exists. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ evPrivate: "", evBrands: "" }); /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== W H E R E T O C R O P O N A P H O N E ================================================================== A phone is tall and narrow, so each photograph loses its left and right edges. By default the middle is kept -- which can cut off whoever is standing to one side. first number = left to right 0% left, 50% middle, 100% right second number = top to bottom 0% top, 50% middle, 100% bottom So if the couple sit to the right of the frame, try "70% 50%". The computer version is wide enough to show the whole picture and never uses these, so changing them is completely safe. ================================================================== */ (function(){ var CROP = { evPrivate: "50% 50%", evBrands: "50% 50%" }; /* --- nothing below this line needs editing --- */ function apply(){ Object.keys(CROP).forEach(function(k){ var v = String(CROP[k] || '').trim(); if(!v) return; var els = document.querySelectorAll('[data-photo="' + k + '"]'); for(var i = 0; i < els.length; i++) els[i].style.setProperty('--focus-m', v); }); } if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', apply); else apply(); new MutationObserver(apply).observe(document.documentElement, {childList:true, subtree:true}); })(); /* ============================================================ EVENTS -- TWO DOORS, NOT ONE LONG PAGE Events opens on the choice and nothing else. Private Celebrations and Work With Hera are each a long read, and stacking both behind the chooser meant a visitor scrolled through all of one to reach the other. Both branches stay in the page -- they are only hidden -- so every link that already points at #private or #brands still works, and the section keeps a single Carrd Section Break. Nothing here needs a new Section, an extra embed, or any change in Carrd. ============================================================ */ (function(){ var BRANCHES = ['private','brands']; /* Each branch is written across several Carrd embeds, not one -- Private is 4, Brands is 5, because the writing and the code both ran past what a single embed can hold. Carrd wraps every embed in its own box, so a branch is really a RUN of those boxes: from the one holding its own id, up to wherever the next branch's box begins (or the end of the Section, for the last branch). Finding that run by position, once, is what lets show() below hide or reveal a whole branch with one line, however many embeds it grew into later. */ function boxOf(id){ var el = document.getElementById(id); var box = el && el.closest('.embed-component'); return box || null; } var chooser, blocks = {}, started = false; var ARROW = '\x3csvg width="17" height="10" viewBox="0 0 17 10" fill="none" ' + 'stroke="currentColor" stroke-width="1.3" aria-hidden="true">' + '\x3cpath d="M17 5H2M5.5 1 1.5 5l4 4" stroke-linecap="round" ' + 'stroke-linejoin="round"/>\x3c/svg>'; /* one way back to the choice, at the top of each branch's first box */ function addBackLinks(){ BRANCHES.forEach(function(k){ var run = blocks[k]; if(!run || !run.length) return; var first = run[0]; if(first.querySelector('.h-ev4-back')) return; var back = document.createElement('button'); back.type = 'button'; back.className = 'h-ev4-back'; back.innerHTML = ARROW + '\x3cspan>All events\x3c/span>'; back.addEventListener('click', function(){ if(location.hash) history.replaceState(null, '', location.pathname + location.search); show(null); chooser.scrollIntoView({block:'start'}); }); first.insertBefore(back, first.firstChild); }); } function show(which){ chooser.style.display = which ? 'none' : ''; BRANCHES.forEach(function(k){ var run = blocks[k]; if(!run) return; var on = which === k; for(var i = 0; i < run.length; i++) run[i].style.display = on ? '' : 'none'; }); } function findInBranch(key, id){ var run = blocks[key]; if(!run) return null; for(var i = 0; i < run.length; i++){ var hit = run[i].querySelector('#' + (window.CSS && CSS.escape ? CSS.escape(id) : id)); if(hit) return hit; } return null; } /* Arriving from somewhere else -- the homepage's "Plan your proposal", the footer, or a pasted address. */ function fromHash(){ var h = (location.hash || '').replace(/^#/,''); if(BRANCHES.indexOf(h) > -1){ show(h); return; } /* an id that lives inside one of the branches still has to open it -- and be scrolled to, since it was display:none at the moment the browser tried its own jump and so was skipped. */ for(var i = 0; i < BRANCHES.length; i++){ var hit = h && findInBranch(BRANCHES[i], h); if(hit){ show(BRANCHES[i]); requestAnimationFrame(function(){ hit.scrollIntoView({block:'start'}); }); return; } } show(null); } /* Carrd adds each embed in turn, so the two branches are still on their way when this file runs. Wait for them rather than giving up -- and stop watching once they arrive. */ function boot(){ if(started) return true; chooser = document.getElementById('events-choose'); if(!chooser) return false; var section = chooser.closest('[id$="-section"]'); if(!section) return false; var kids = Array.prototype.slice.call(section.children); var starts = BRANCHES.map(function(k){ var box = boxOf(k); return {key:k, i: box ? kids.indexOf(box) : -1}; }); if(starts.some(function(s){ return s.i < 0; })) return false; starts.sort(function(a,b){ return a.i - b.i; }); starts.forEach(function(s, idx){ var end = idx + 1 < starts.length ? starts[idx+1].i : kids.length; blocks[s.key] = kids.slice(s.i, end); }); started = true; addBackLinks(); /* Opening a branch from somewhere else on the site. The two branches start closed, so a link arriving from another page -- "Plan your moment" on the homepage, "Plan an event" on the contact page -- has to say which one to open before the thing it points at can be scrolled to. This is how the shared link handler in 00-CONFIG asks for that, by the id it is heading for. Answering true means the branch is now open. */ window.HERA_reveal = function(id){ if(!id || !started) return false; if(BRANCHES.indexOf(id) > -1){ show(id); return true; } for(var i = 0; i < BRANCHES.length; i++){ if(findInBranch(BRANCHES[i], id)){ show(BRANCHES[i]); return true; } } return false; }; /* Opening a branch -- from the chooser's own two doors, or from a link anywhere else on the site that points at one of them. This listens on the document in the CAPTURE phase, and that is the whole point of it. Neither branch is a Carrd Section, so when Carrd's own click handler sees "#private" it looks for a Section by that name, finds none, and sends the visitor to the Home page instead. Preventing the default is not enough to stop it: Carrd does not rely on the browser following the link, it navigates itself, in code. The only way to stop that is to take the click before Carrd's handler ever sees it -- which is what capturing on the document does, since capture runs ahead of every ordinary listener no matter which was attached first. The address bar is deliberately left saying "#events", because "#private" is an address Carrd cannot restore on a reload. */ document.addEventListener('click', function(e){ var a = e.target.closest ? e.target.closest('a[href^="#"]') : null; if(!a) return; var key = a.getAttribute('href').slice(1); if(BRANCHES.indexOf(key) < 0) return; e.preventDefault(); e.stopImmediatePropagation(); /* Coming from another Section -- Events has to be on screen before there is a branch to open. "#events" is a real Section Break, so Carrd handles that part quite happily. */ var section = chooser.closest('[id$="-section"]'); if(section && !section.classList.contains('active')){ location.hash = section.id.replace(/-section$/, ''); setTimeout(function(){ show(key); var later = document.getElementById(key); if(later) later.scrollIntoView({block:'start'}); }, 260); return; } show(key); var t = document.getElementById(key); if(t) t.scrollIntoView({block:'start'}); }, true); window.addEventListener('hashchange', fromHash); fromHash(); return true; } if(!boot()){ var mo = new MutationObserver(function(){ if(boot()) mo.disconnect(); }); mo.observe(document.documentElement, {childList:true, subtree:true}); window.addEventListener('load', function(){ if(boot() && mo) mo.disconnect(); }); } })();
                /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== E D I T Y O U R C E L E B R A T I O N S ================================================================== Two things live here: the one shared gallery at the top of this page, and the list of occasions someone can choose from further down. They are not tied to each other any more -- every occasion shows the same photographs, so there is nothing to fill in per occasion, and a category with no dedicated photos of its own no longer looks empty next to one that has plenty. ------------------------------------------------------------------ THE GALLERY ------------------------------------------------------------------ ADD A PHOTOGRAPH add one more line to HERA_CEL_GALLERY, below REMOVE A PHOTOGRAPH delete its line REORDER PHOTOGRAPHS move the lines up or down As many or as few as you like; one is fine. Upload each in Carrd, copy the address, and paste it between the quote marks. LEAVE the Image element on the page afterwards -- Carrd deletes any picture that nothing is using. ------------------------------------------------------------------ THE OCCASIONS ------------------------------------------------------------------ ADD AN OCCASION copy a whole { ... }, block and type over it REMOVE AN OCCASION delete its whole block REORDER OCCASIONS move a whole block up or down REWORD ANYTHING type over the words between the quote marks tab the short name on the button name the same occasion, as it appears in the form's own list req which of the STANDARD questions must be answered for this occasion. Any of: date place pax need vision images budget contact "contact" means the name and the handle together. Anything not listed here stays optional. ask the few extra questions the form adds for THIS occasion: label the question type "text" a single line "choice" pick one "multi" pick any number "long" a paragraph options only for "choice" and "multi" req add req:true to make it required Choosing a button takes someone straight to the request form, with that occasion already selected -- there is nothing else to click through first. ================================================================== */ var HERA_CEL_GALLERY = [ "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4337_2", "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4330_2", "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/Screenshot_2026-08-31_at_3.18.41_PM", "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/Screenshot_2026-08-31_at_3.22.56_PM", "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_8415", "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4341", "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_8554", "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4513", "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/Screenshot_2026-09-01_at_1.25.17_PM", "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4729", "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4730" ]; var HERA_CELEBRATIONS = [ { tab:"Proposals", name:"Proposals", req:"date, place, need, vision, budget, contact", ask:[ {label:"Roughly what time of day?", type:"text"}, {label:"Indoors or outdoors?", type:"choice", options:"Indoors, Outdoors, Not sure yet"}, {label:"Anything we should know about the surprise?", type:"long"} ] }, { tab:"Weddings", name:"Weddings", req:"date, place, need, vision, budget, contact", ask:[ {label:"Which parts are we creating for?", type:"multi", req:true, options:"Ceremony, Reception, Both, Not decided yet"}, {label:"Anything else about the day?", type:"long"} ] }, { tab:"Birthdays", name:"Birthdays", req:"date, place, need, vision, budget, contact", ask:[ {label:"Anything about the person it is for?", type:"long"} ] }, { tab:"Private Dinners", name:"Private Dinners", req:"date, place, pax, need, budget, contact", ask:[ {label:"Anything else about the evening?", type:"long"} ] }, { tab:"Event Decor", name:"Event Decor", req:"date, place, pax, need, vision, budget, contact", ask:[ {label:"What is the occasion?", type:"text", req:true}, {label:"Anything else we should know?", type:"long"} ] }, { tab:"Photoshoot Setups", name:"Photoshoot Setups", req:"date, place, need, vision, budget, contact", ask:[ {label:"What is the shoot for?", type:"text", req:true}, {label:"How long is the shoot?", type:"text"}, {label:"Anything else about the direction?", type:"long"} ] }, { tab:"Create Your Own", name:"Something else entirely?", req:"date, place, need, budget, contact", ask:[ {label:"What are we creating?", type:"text", req:true}, {label:"Tell us what you have in mind", type:"long"} ] } ]; /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== The picture at the very top of this page, and where to crop the photographs on a phone. "gallery" here is the crop for every photograph in HERA_CEL_GALLERY above -- one setting for all of them, since they now share one row rather than one each. first number = left to right 0% left, 50% middle, 100% right second number = top to bottom 0% top, 50% middle, 100% bottom Only phones use the crop setting; the computer version is unaffected, so changing it is completely safe. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ celHero: "" }); window.HERA_CEL_CROP = { celHero: "50% 50%", gallery: "50% 50%" };

                Private Celebrations

                Made around your moment.

                Weddings, proposals, dinners and everything that doesn't need a name.

                Photo · Private celebrations · the opening picture

                What are we creating?

                Send us your idea

                Tell us what you're imagining.

                Nothing here is a package. Send us the occasion and how you want it to feel, and we build around that.

                01 About the occasion

                02 What do you need from Hera?

                Choose as many as apply. Most people need some of it, not all.

                03 What you're imagining

                Show us what you're imagining (optional)

                Inspiration, screenshots, venue photographs -- anything that helps us see the direction.

                No images chosen

                04 Your information

                We read every one ourselves and reply within two working days.

                /* ================================================================== ## S Y S T E M C O D E -- D O N O T E D I T ## ================================================================== Makes the email address above required. The rest of this form is checked by the script that draws it, which is stored in a compiled form and is not edited by hand. So rather than unpicking that, this adds the one extra check on its own -- in the capture phase, which runs before the form's own handler, and it reuses the form's existing error line so a missing email reads exactly like every other missing answer. ================================================================== */ (function(){ var form = document.getElementById('h-pcf'); if(!form) return; form.addEventListener('submit', function(ev){ var el = document.getElementById('h-pcf-email'); if(!el) return; var box = el.closest('.h-bl-field'); var v = String(el.value || '').trim(); var ok = v && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); if(ok){ if(box) box.classList.remove('is-bad'); return; } if(box) box.classList.add('is-bad'); var err = document.getElementById('h-pcf-err'); if(err){ err.hidden = false; err.textContent = v ? 'That does not look like an email address.' : 'We still need an email address, so we can send you a copy of your request.'; } ev.preventDefault(); ev.stopPropagation(); if(el.focus) el.focus(); }, true); })();
                var __hb4="\n/* ==================================================================\n ## S Y S T E M C O D E -- D O N O T E D I T ##\n ==================================================================\n Draws the shared gallery once from HERA_CEL_GALLERY, the tabs from\n HERA_CELEBRATIONS, and the form from whichever tab is chosen.\n Adding a photograph or an occasion up there is the only thing\n needed -- nothing here has to change for it to appear.\n ================================================================== */\n(function(){\n var tabs = document.getElementById('h-pc-tabs'),\n gal = document.getElementById('h-pc-gal'),\n form = document.getElementById('h-pcf'),\n sel = document.getElementById('h-pcf-type'),\n askEl= document.getElementById('h-pcf-ask'),\n need = document.getElementById('h-pcf-need'),\n err = document.getElementById('h-pcf-err');\n if(!tabs || !form) return;\n\n var LIST = (typeof HERA_CELEBRATIONS !== 'undefined' && HERA_CELEBRATIONS) || [];\n if(!LIST.length) return;\n var at = 0;\n\n /* what a client can ask Hera for -- the same everywhere, because\n it is about involvement rather than about the occasion */\n var NEEDS = ['Planning','Coordination','Florals','Decor and styling',\n 'Setup and execution','Full creative direction',\"I'm not sure yet\"];\n\n function esc(s){\n return String(s == null ? '' : s)\n .replace(/&/g,'\x26amp;').replace(/\x3c/g,'\x26lt;').replace(/>/g,'\x26gt;').replace(/\"/g,'\x26quot;');\n }\n function list(s){\n return String(s || '').split(',').map(function(x){ return x.trim(); }).filter(Boolean);\n }\n function num(i){ return (i + 1 < 10 ? '0' : '') + (i + 1); }\n\n /* ---- tabs ---------------------------------------------------- */\n tabs.innerHTML = LIST.map(function(c, i){\n return '\x3cbutton class=\"h-pick-tab\" type=\"button\" role=\"tab\" data-i=\"' + i + '\"' +\n ' aria-selected=\"' + (i === 0 ? 'true' : 'false') + '\">' +\n '\x3cspan class=\"h-pick-tab-n\">' + num(i) + '\x3c/span>' +\n '\x3cspan class=\"h-pick-tab-t\">' + esc(c.tab) + '\x3c/span>\x3c/button>';\n }).join('');\n\n /* ---- the gallery --------------------------------------------\n One shared row, painted once. It no longer changes when a\n different occasion is chosen below it. Empty entries are\n skipped, so a half-filled gallery still looks deliberate. */\n function paintGallery(){\n var shots = (window.HERA_CEL_GALLERY || []).map(function(u){ return String(u || '').trim(); });\n var filled = shots.filter(Boolean);\n var crop = (window.HERA_CEL_CROP || {}).gallery || '50% 50%';\n\n if(!filled.length){ gal.innerHTML = ''; return; }\n\n var slides = filled.map(function(u, i){\n return '\x3cdiv class=\"h-gal-item\" style=\"--focus-m:' + esc(crop) + '\">' +\n '\x3cimg src=\"' + esc(u) + '\" alt=\"A Hera private celebration\" loading=\"lazy\">' +\n '\x3c/div>';\n }).join('');\n\n gal.innerHTML =\n '\x3cdiv class=\"h-gal-rail\" id=\"h-gal-rail\">' + slides + '\x3c/div>' +\n (filled.length > 1\n ? '\x3cdiv class=\"h-gal-nav\">' +\n '\x3cbutton class=\"h-cbtn\" type=\"button\" data-gal=\"-1\" aria-label=\"Previous photograph\">' +\n '\x3csvg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" aria-hidden=\"true\">\x3cpath d=\"M15 5l-7 7 7 7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\x3c/svg>\x3c/button>' +\n '\x3cbutton class=\"h-cbtn\" type=\"button\" data-gal=\"1\" aria-label=\"Next photograph\">' +\n '\x3csvg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" aria-hidden=\"true\">\x3cpath d=\"M9 5l7 7-7 7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\x3c/svg>\x3c/button>' +\n '\x3c/div>'\n : '');\n }\n\n gal.addEventListener('click', function(e){\n var b = e.target.closest('[data-gal]');\n if(!b) return;\n var rail = document.getElementById('h-gal-rail');\n var first = rail && rail.querySelector('.h-gal-item');\n if(!rail || !first) return;\n var step = first.getBoundingClientRect().width + 14;\n rail.scrollBy({left: step * (+b.dataset.gal), behavior:'smooth'});\n });\n\n /* ---- the form ------------------------------------------------ */\n need.innerHTML = NEEDS.map(function(n, i){\n return '\x3clabel class=\"h-bl-chip\">\x3cinput type=\"checkbox\" value=\"' + esc(n) + '\">' +\n '\x3cspan>' + esc(n) + '\x3c/span>\x3c/label>';\n }).join('');\n\n sel.innerHTML = LIST.map(function(c, i){\n return '\x3coption value=\"' + i + '\">' + esc(c.name) + '\x3c/option>';\n }).join('');\n\n function paintAsk(){\n var qs = LIST[at].ask || [];\n askEl.innerHTML = qs.map(function(q, i){\n var id = 'h-pcf-q' + i;\n /* only OPTIONAL questions carry a note; a required one is simply\n the plain question, which is the quieter of the two */\n var lab = '\x3cspan>' + esc(q.label) +\n (q.req ? '' : ' \x3cspan class=\"h-bl-req\">(optional)\x3c/span>') + '\x3c/span>';\n if(q.type === 'long'){\n return '\x3clabel class=\"h-bl-field\" data-ask' + (q.req ? ' data-req' : '') + '>' + lab +\n '\x3ctextarea class=\"h-bl-ta\" id=\"' + id + '\">\x3c/textarea>\x3c/label>';\n }\n if(q.type === 'choice' || q.type === 'multi'){\n var opts = list(q.options), multi = q.type === 'multi';\n return '\x3cdiv class=\"h-bl-field\" data-ask' + (q.req ? ' data-req' : '') + '>' + lab +\n '\x3cdiv class=\"h-bl-chips\" role=\"group\">' + opts.map(function(o, j){\n return '\x3clabel class=\"h-bl-chip\">\x3cinput type=\"' +\n (multi ? 'checkbox' : 'radio') + '\"' +\n (multi ? '' : ' name=\"' + id + '\"') +\n ' value=\"' + esc(o) + '\"' + (!multi && j === 0 ? ' checked' : '') + '>' +\n '\x3cspan>' + esc(o) + '\x3c/span>\x3c/label>';\n }).join('') + '\x3c/div>\x3c/div>';\n }\n return '\x3clabel class=\"h-bl-field\" data-ask' + (q.req ? ' data-req' : '') + '>' + lab +\n '\x3cinput class=\"h-bl-in\" type=\"text\" id=\"' + id + '\">\x3c/label>';\n }).join('');\n }\n\n /* Which of the standard questions this occasion insists on. The\n labels are relabelled rather than just checked, so somebody can\n see what is required before they try to send it. */\n /* ==================================================================\n ## H E R A O W N E R -- S A F E T O E D I T ##\n ==================================================================\n The only three ways Hera is reached. The form asks which one the\n client prefers, then asks for THAT one -- so nobody is made to\n hand over an email address they never check.\n\n To change what a question says, edit the words in quote marks:\n label the wording above the box\n ph the faint example inside the empty box\n missing how it is named if they forget to fill it in\n ================================================================== */\n var HANDLE = {\n WhatsApp: {label:'WhatsApp number', ph:'+84 90 123 4567', type:'tel',\n ac:'tel', missing:'a WhatsApp number'},\n Zalo: {label:'Zalo number', ph:'+84 90 123 4567', type:'tel',\n ac:'tel', missing:'a Zalo number'},\n Telegram: {label:'Telegram username', ph:'@username', type:'text',\n ac:'off', missing:'a Telegram username'},\n Facebook: {label:'Facebook name or link', ph:'facebook.com/yourname',\n type:'text', ac:'off', missing:'a Facebook name or link'},\n Instagram: {label:'Instagram handle', ph:'@username', type:'text',\n ac:'off', missing:'an Instagram handle'}\n };\n\n /* --- nothing below this line needs editing --- */\n function channel(){\n var s = document.getElementById('h-pcf-how');\n var v = s ? s.value : 'WhatsApp';\n return HANDLE[v] ? v : 'WhatsApp';\n }\n /* the one box under the choice becomes whichever was chosen */\n function syncHandle(){\n var c = HANDLE[channel()],\n l = document.getElementById('h-pcf-handle-l'),\n i = document.getElementById('h-pcf-handle');\n if(!l || !i) return;\n l.textContent = c.label;\n i.placeholder = c.ph;\n i.type = c.type;\n i.setAttribute('autocomplete', c.ac);\n }\n\n var STD = {\n date: {el:'h-pcf-date', field:null, label:'a date'},\n place: {el:'h-pcf-place', field:'h-pcf-place-f', label:'a location'},\n pax: {el:'h-pcf-pax', field:null, label:'how many people'},\n vision:{el:'h-pcf-vision', field:null, label:'a little about what you are imagining'},\n budget:{el:'h-pcf-budget', field:null, label:'an approximate budget'}\n };\n\n function markRequired(){\n var req = String(LIST[at].req || '').split(',').map(function(x){\n return x.trim(); }).filter(Boolean);\n var isReq = {};\n req.forEach(function(k){ isReq[k] = true; });\n\n Object.keys(STD).forEach(function(k){\n var conf = STD[k];\n var input = document.getElementById(conf.el);\n if(!input) return;\n var field = conf.field ? document.getElementById(conf.field)\n : input.closest('.h-bl-field');\n if(!field) return;\n var span = field.querySelector(':scope > span');\n if(!span) return;\n var base = span.textContent.replace(/\\s*\\(optional\\)\\s*$/, '')\n .replace(/\\s*\\(if relevant\\)\\s*$/, '')\n .replace(/\\s*\\(a range is fine\\)\\s*$/, '')\n .replace(/\\s*\\(if you know\\)\\s*$/, '').trim();\n field.dataset.req = isReq[k] ? '1' : '';\n span.innerHTML = esc(base) +\n (isReq[k] ? '' : ' \x3cspan class=\"h-bl-req\">(optional)\x3c/span>');\n });\n\n /* the needs list and the pictures are groups, not single fields */\n var nf = document.getElement";
                __hb4+="ById('h-pcf-need-f');\n if(nf) nf.dataset.req = isReq.need ? '1' : '';\n var upf = document.getElementById('h-pcf-up');\n if(upf) upf.closest('.h-bl-field').dataset.req = isReq.images ? '1' : '';\n var upSpan = document.querySelector('#h-pcf-up')\n && document.querySelector('#h-pcf-up').closest('.h-bl-field').querySelector(':scope > span');\n if(upSpan){\n upSpan.innerHTML = \"Show us what you're imagining\" +\n (isReq.images ? '' : ' \x3cspan class=\"h-bl-req\">(optional)\x3c/span>');\n }\n }\n\n /* On a phone the seven categories are wider than the screen, so the\n strip scrolls sideways. Two things follow from that, and without\n them the categories are effectively hidden:\n\n showTab brings the chosen one into the middle. Otherwise\n choosing from the dropdown highlights a tab that is\n somewhere off the side of the screen.\n edges fades whichever end has more beyond it. A phone draws\n no scrollbar, so nothing else tells a visitor that\n four more categories exist past the edge. */\n function showTab(){\n var b = tabs.children[at];\n if(!b || tabs.scrollWidth <= tabs.clientWidth + 1) return;\n var want = b.offsetLeft - (tabs.clientWidth - b.offsetWidth) / 2;\n var max = tabs.scrollWidth - tabs.clientWidth;\n want = Math.max(0, Math.min(want, max));\n /* ease if the browser will, but never at the cost of moving at all:\n if nothing has happened by the next frame, jump straight there. */\n var from = tabs.scrollLeft;\n try{ tabs.scrollTo({left:want, behavior:'smooth'}); }\n catch(e){ tabs.scrollLeft = want; }\n setTimeout(function(){\n if(Math.abs(tabs.scrollLeft - from) < 2 && Math.abs(want - from) > 2){\n tabs.scrollLeft = want;\n }\n edges();\n }, 220);\n }\n function edges(){\n var more = tabs.scrollWidth > tabs.clientWidth + 1;\n tabs.classList.toggle('can-l', more && tabs.scrollLeft > 4);\n tabs.classList.toggle('can-r', more &&\n tabs.scrollLeft < tabs.scrollWidth - tabs.clientWidth - 4);\n }\n tabs.addEventListener('scroll', edges, {passive:true});\n window.addEventListener('resize', edges);\n\n function go(i, moveTo){\n at = i;\n sel.value = String(i);\n [].forEach.call(tabs.children, function(b, k){\n b.setAttribute('aria-selected', k === at ? 'true' : 'false');\n });\n paintAsk(); markRequired();\n showTab(); edges();\n if(window.__heraScan) window.__heraScan();\n if(moveTo){\n var t = document.getElementById('plan-your-moment');\n if(t) t.scrollIntoView({behavior:'smooth', block:'start'});\n }\n }\n\n /* Choosing a button IS the action now -- there is no separate\n panel with its own \"send us your idea\" link to click through\n first, so a click goes straight to the form. Arrow keys only\n move the selection, the way arrow keys in any other tablist\n do; only an actual choice sends someone down the page. */\n tabs.addEventListener('click', function(e){\n var b = e.target.closest('.h-pick-tab');\n if(b) go(+b.dataset.i, true);\n });\n tabs.addEventListener('keydown', function(e){\n if(e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;\n e.preventDefault();\n go((at + (e.key === 'ArrowRight' ? 1 : LIST.length - 1)) % LIST.length, false);\n tabs.children[at].focus();\n });\n sel.addEventListener('change', function(){ go(+sel.value, false); });\n\n /* ---- reference images ---------------------------------------\n The pictures are held and shown back so the visitor can see what\n they picked. They are NOT uploaded anywhere: this form has no\n delivery address yet, and pretending otherwise would silently\n lose them. The note under the button says so. */\n var picked = [];\n var fileIn = document.getElementById('h-pcf-files'),\n pickBtn= document.getElementById('h-pcf-pick'),\n countEl= document.getElementById('h-pcf-count'),\n thumbs = document.getElementById('h-pcf-thumbs');\n\n pickBtn.addEventListener('click', function(){ fileIn.click(); });\n\n fileIn.addEventListener('change', function(){\n [].forEach.call(fileIn.files, function(f){\n if(!/^image\\//.test(f.type)) return;\n if(picked.length >= 8) return; /* a sane ceiling */\n picked.push(f);\n });\n fileIn.value = '';\n paintThumbs();\n });\n\n function paintThumbs(){\n countEl.textContent = picked.length\n ? picked.length + (picked.length === 1 ? ' image chosen' : ' images chosen')\n : 'No images chosen';\n thumbs.innerHTML = '';\n picked.forEach(function(f, i){\n var cell = document.createElement('div');\n cell.className = 'h-up-thumb';\n var img = document.createElement('img');\n img.alt = f.name;\n img.src = URL.createObjectURL(f);\n img.onload = function(){ URL.revokeObjectURL(img.src); };\n var x = document.createElement('button');\n x.type = 'button'; x.className = 'h-up-x';\n x.setAttribute('aria-label', 'Remove ' + f.name);\n x.textContent = '\u00d7';\n x.addEventListener('click', function(){ picked.splice(i, 1); paintThumbs(); });\n cell.appendChild(img); cell.appendChild(x);\n thumbs.appendChild(cell);\n });\n }\n\n /* ---- sending ------------------------------------------------- */\n function bad(el, on){\n var f = el.closest('.h-bl-field');\n if(f) f.classList.toggle('is-bad', !!on);\n }\n form.addEventListener('submit', function(e){\n e.preventDefault();\n\n var req = String(LIST[at].req || '').split(',').map(function(x){\n return x.trim(); }).filter(Boolean);\n var isReq = {};\n req.forEach(function(k){ isReq[k] = true; });\n\n var missing = [];\n var firstBad = null;\n function fail(field, what, focusEl){\n if(field) field.classList.add('is-bad');\n missing.push(what);\n if(!firstBad) firstBad = focusEl || (field && field.querySelector('input,textarea,select'));\n }\n function ok(field){ if(field) field.classList.remove('is-bad'); }\n\n /* the standard questions, only where this occasion asks for them */\n Object.keys(STD).forEach(function(k){\n var input = document.getElementById(STD[k].el);\n if(!input) return;\n var field = STD[k].field ? document.getElementById(STD[k].field)\n : input.closest('.h-bl-field');\n ok(field);\n if(isReq[k] && !input.value.trim()) fail(field, STD[k].label, input);\n });\n\n /* what they need from us -- a tick or their own words counts */\n var nf = document.getElementById('h-pcf-need-f');\n ok(nf);\n if(isReq.need){\n var ticked = document.querySelectorAll('#h-pcf-need input:checked').length;\n var typed = document.getElementById('h-pcf-need-other').value.trim();\n if(!ticked && !typed) fail(nf, 'what you need from us', null);\n }\n\n /* reference pictures */\n var upField = document.getElementById('h-pcf-up').closest('.h-bl-field');\n ok(upField);\n if(isReq.images && !picked.length) fail(upField, 'at least one picture', null);\n\n /* the extra questions this occasion marks as required */\n askEl.querySelectorAll('[data-req]').forEach(function(f){\n f.classList.remove('is-bad');\n var box = f.querySelector('input[type=checkbox],input[type=radio]');\n var free = f.querySelector('input[type=text],textarea');\n var answered = box ? !!f.querySelector('input:checked')\n : !!(free && free.value.trim());\n if(!answered){\n var lab = f.querySelector(':scope > span');\n /* the label is a question, so its punctuation has to come off\n before it is dropped into a sentence */\n var asWords = lab\n ? lab.textContent.replace(/\\s*\\(optional\\)\\s*$/,'')\n .replace(/[?.:]+\\s*$/,'').trim().toLowerCase()\n : 'one of the questions above';\n fail(f, asWords, free);\n }\n });\n\n /* Your details are always required. The email address included:\n the five ways to reach you above are all apps, none of which\n can be emailed, so without it the copy of this request cannot\n be sent back to whoever wrote it. */\n var name = document.getElementById('h-pcf-name'),\n handle = document.getElementById('h-pcf-handle'),\n email = document.getElementById('h-pcf-email');\n var typed = email.value.trim();\n [[name,'your name',!!name.value.trim()],\n [handle, HANDLE[channel()].missing, !!handle.value.trim()],\n [email, typed ? 'an email address that looks complete'\n : 'an email address, so we can send you a copy',\n /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(typed)]\n ].forEach(function(row){\n var field = row[0].closest('.h-bl-field');\n ok(field);\n if(!row[2]) fail(field, row[1], row[0]);\n });\n\n if(missing.length){\n err.hidden = false;\n err.textContent = 'We still need ' + (missing.length === 1 ? missing[0]\n : missing.slice(0, -1).join(', ') + ' and ' + missing[missing.length - 1]) + '.';\n if(firstBad && firstBad.focus) firstBad.focus();\n else (form.querySelector('.is-bad') || form).scrollIntoView({behavior:'smooth', block:'center'});\n return;\n }\n err.hidden = true;\n\n form.innerHTML =\n '\x3cdiv class=\"h-req-done\">' +\n '\x3ch3 class=\"h-req-done-h\">Thank you \x26#8212; we have your request.\x3c/h3>' +\n '\x3cp class=\"h-xo-p\">We read every one ourselves. Expect a reply within two ' +\n 'working days, usually with a few questions and a first idea of what we ' +\n 'would make.\x3c/p>' +\n '\x3cp class=\"h-req-note\">A copy has been emailed to you as well, so you have a record of it.\x3c/p>' +\n '\x3c/div>';\n form.scrollIntoView({behavior:'smooth', block:'center'});\n });\n\n var howSel = document.getElementById('h-pcf-how');\n if(howSel) howSel.addEventListener('change', syncHandle);\n syncHandle();\n\n paintGallery();\n go(0, false);\n})();\n"; (function(){var s=document.createElement("script");s.textContent=__hb4;document.head.appendChild(s);__hb4=null;})();
                /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 1. T H E B I G P H O T O G R A P H S ================================================================== The pictures that carry the page. Choose them for scale and polish -- a brand manager is deciding here whether Hera can work at their level. whHero the opening picture. Wide. whSay the full-width picture before the form. Wide, quiet; it sits behind navy, so anything busy disappears. Then one MAIN and one DETAIL picture for each way of working. The detail is the small square that overlaps the corner of the main one -- a close crop works far better here than another wide shot. Leave a detail blank and the main picture simply stands alone. whHost / whHostDetail a public experience in a venue whPartner / whPartnerDetail a brand's product in a Hera room whHire / whHireDetail a private activation or launch Upload each in Carrd, copy the address, paste it between the quote marks, and LEAVE the Image element on the page -- Carrd deletes any picture that nothing is using. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ whHero: "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/TTZ_1345", whSay: "", whHost: "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/TTZ_1144", whHostDetail: "", whPartner: "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4636", whPartnerDetail: "", whHire: "https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/LF_01289_2", whHireDetail: "" }); /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 2. T H E T H R E E W A Y S T O W O R K ================================================================== Each one becomes a chapter in the big split composition: choosing it on the right changes the photograph on the left. no the small numeral name the large editorial line in the index kind who it is for. One short line, in ordinary sentence case. core the three words. Kept short on purpose. body two or three sentences. What it actually is. points the supporting detail. Set as quiet lines, not a table. Leave [] for none. cta the words on its link want which option the form should choose when someone arrives from that link. Must match a name in section 5 exactly. photo which picture from section 1 is the main one detail which picture is the small overlapping square. "" for none. crop where to hold the main picture if it is being cut awkwardly. "50% 50%" is the middle; raise the first number to keep more of the right-hand side. ================================================================== */ window.HERA_WAYS = [ { no:"01", name:"Host with us.", kind:"For hotels, resorts, cafes and venues.", core:"Public · Ticketed · Co-branded", body:"Your space opens its doors and Hera brings the floral experience to it. We develop the concept, lead the flowers and launch it with you — a public event both names are proud to put their name to.", points:["You provide the space and the hospitality", "Hera develops and leads the floral experience", "We promote and co-launch it together"], cta:"Host an experience", want:"Host With Us", photo:"whHost", detail:"whHostDetail", crop:"50% 50%" }, { no:"02", name:"Partner with us.", kind:"For lifestyle, beauty, F&B and wellness brands.", core:"Sponsor · Experience · Exposure", body:"Your product becomes part of the experience rather than a logo beside it. Guests meet it while their hands are busy, in a room they want to photograph — which is a very different kind of introduction.", points:["Product integration and sampling", "Gifting and sponsorship", "Content and exposure made together"], cta:"Partner on an experience", want:"Partner With Us", photo:"whPartner", detail:"whPartnerDetail", crop:"50% 45%" }, { no:"03", name:"Hire us.", kind:"For brands, hotels and private campaigns.", core:"Private · Custom · Branded", body:"A floral experience built around your brief, your guests and nobody else's. Hera takes the vision and makes the room it happens in — from the first idea to the last stem in place.", points:["Brand activations, launches and campaign events", "Creator and KOC experiences", "Installations, custom workshops and guest experiences"], cta:"Start a project", want:"Hire Us", photo:"whHire", detail:"whHireDetail", crop:"50% 40%" } ]; /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 3. S E L E C T E D C O L L A B O R A T I O N S ================================================================== The horizontal gallery. Add a line for each photograph; the order here is the order in the strip. Five pictures work, so do thirty -- the strip simply gets longer. src the picture address from Carrd alt what the picture shows, for anyone who cannot see it partner the partner's name, if they have agreed to be named type the kind of collaboration year "2025", or leave "" cap a short line if the picture needs one shape how it sits in the strip. This is what stops the gallery looking like a row of identical stamps, so vary it deliberately: "wide" a broad landscape moment "" an ordinary landscape "portrait" an upright picture "detail" a small square close-up A good rhythm: wide, portrait, ordinary, detail, portrait, wide. Delete a line and the picture goes. Leave the list empty and the whole section disappears rather than showing gaps. ================================================================== */ window.HERA_WORK = [ { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4646", alt:"", partner:"", type:"", year:"", cap:"", shape:"wide" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/IMG_4656", alt:"", partner:"", type:"", year:"", cap:"", shape:"portrait" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/TTZ_1204", alt:"", partner:"", type:"", year:"", cap:"", shape:"" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/TTZ_1509", alt:"", partner:"", type:"", year:"", cap:"", shape:"wide" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/LF_01434", alt:"", partner:"", type:"", year:"", cap:"", shape:"wide" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/LF_01435_2", alt:"", partner:"", type:"", year:"", cap:"", shape:"wide" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/TTZ_1661", alt:"", partner:"", type:"", year:"", cap:"", shape:"wide" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/LF_01377_3", alt:"", partner:"", type:"", year:"", cap:"", shape:"portrait" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/Screenshot_2026-08-31_at_9.31.31_PM", alt:"", partner:"", type:"", year:"", cap:"", shape:"portrait" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/LF_01393", alt:"", partner:"", type:"", year:"", cap:"", shape:"portrait" }, { src:"https://res.cloudinary.com/vpq4svls/image/upload/f_auto,q_auto/LF_01345_2", alt:"", partner:"", type:"", year:"", cap:"", shape:"portrait" }, { src:"", alt:"", partner:"", type:"", year:"", cap:"", shape:"portrait" }, ]; /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 4. C A S E S T U D I E S ================================================================== Selected collaborations told properly. EMPTY ON PURPOSE -- while this list is empty the section stays off the page entirely, so nothing looks unfinished. Add your first one and it appears. Copy this between the [ ] to add one: { partner:"The Anam", kind:"Host with us", made:"A monthly floral table for hotel guests.", felt:"Guests left with the arrangement they made themselves.", shots:["", "", ""] }, Only add partners who have agreed to be named. ================================================================== */ window.HERA_CASES = [ ]; /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 5. T H E F O U R R O U T E S ================================================================== What the form asks in "What brings you here". The first three must match the "want" lines in section 2 exactly, or the links from the three ways will not pick anything. ================================================================== */ window.HERA_WANTS = ["Host With Us", "Partner With Us", "Hire Us", "Not sure yet"];
                Photo · the opening picture

                Work With Hera

                Let's create something worth sharing.

                Hera works with hotels, resorts, venues and brands to make floral experiences people travel for, talk about and photograph. Not a backdrop at the edge of the room — the reason they came.

                Work With Hera

                Three ways to work together.

                Choose the one that sounds like you.

                  Photo · the chosen way of working

                  The short version

                  Not sure where you fit?

                    Your idea. Our flowers.
                    Let's see where it goes.

                    Start here

                    Have something in mind?

                    Tell us what you're working on. We'll figure out where Hera fits.

                    Every one of these is read by us, not by a form. Expect a reply within two working days.

                    01 What brings you here

                    02 Who you are

                    03 What you're planning

                    04 Anything to show us

                    Moodboards, briefs, decks, venue photographs (optional)

                    Whatever you already have. Nothing needs to be finished.

                    No files chosen
                    var __hb5="\n/* ==================================================================\n Nothing below this line needs editing.\n ================================================================== */\n(function(){\n function esc(s){\n return String(s == null ? '' : s)\n .replace(/&(?!#?\\w+;)/g, '\x26amp;').replace(/\x3c/g, '\x26lt;')\n .replace(/>/g, '\x26gt;').replace(/\"/g, '\x26quot;');\n }\n /* the copy above is written with a few entities of its own, so it is\n trusted as typed rather than escaped a second time */\n function copy(s){ return String(s == null ? '' : s); }\n function photo(key){\n return String((window.HERA_PHOTOS || {})[key] || '').trim();\n }\n var ARROW = '\x3csvg width=\"16\" height=\"9\" viewBox=\"0 0 17 10\" fill=\"none\" ' +\n 'stroke=\"currentColor\" stroke-width=\"1.1\" aria-hidden=\"true\">' +\n '\x3cpath d=\"M0 5h15M11.5 1 15.5 5l-4 4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\x3c/svg>';\n\n var WAYS = window.HERA_WAYS || [];\n var WORK = window.HERA_WORK || [];\n var CASES = window.HERA_CASES || [];\n var WANTS = window.HERA_WANTS || [];\n\n /* =================================================================\n THE THREE WAYS\n ================================================================= */\n var opts = document.getElementById('h-wh-opts');\n var stage = document.getElementById('h-wh-stage');\n var det = document.getElementById('h-wh-det');\n var count = document.getElementById('h-wh-count');\n var at = 0;\n\n if(opts && WAYS.length){\n opts.innerHTML = WAYS.map(function(w, i){\n return '\x3cli class=\"h-wh-opt\" data-i=\"' + i + '\" role=\"presentation\">' +\n '\x3cbutton class=\"h-wh-opt-btn\" type=\"button\" role=\"tab\" ' +\n 'aria-selected=\"false\" aria-controls=\"h-wh-panel\">' +\n '\x3cspan class=\"h-wh-opt-no\">' + copy(w.no) + '\x3c/span>' +\n '\x3cspan class=\"h-wh-opt-name\">' + copy(w.name) + '\x3c/span>' +\n '\x3c/button>\x3c/li>';\n }).join('');\n\n /* The words live in one panel under the picture rather than inside\n each option, so the three can sit side by side across the page. */\n var panel = document.getElementById('h-wh-panel');\n function panelHtml(w){\n var pts = (w.points || []).length\n ? '\x3cul class=\"h-wh-panel-pts\">' + w.points.map(function(p){\n return '\x3cli>' + copy(p) + '\x3c/li>'; }).join('') + '\x3c/ul>' : '';\n return '\x3cp class=\"h-wh-panel-kind\">' + copy(w.kind) + '\x3c/p>' +\n '\x3cp class=\"h-wh-panel-core\">' + copy(w.core) + '\x3c/p>' +\n '\x3cp class=\"h-wh-panel-b\">' + copy(w.body) + '\x3c/p>' + pts +\n '\x3ca class=\"h-wh-panel-cta\" href=\"#work-with-hera\" ' +\n 'data-want=\"' + esc(w.want) + '\">\x3cspan>' + copy(w.cta) + '\x3c/span>' +\n ARROW + '\x3c/a>';\n }\n var fading = 0;\n function setPanel(w, animate){\n if(!panel) return;\n if(!animate){ panel.innerHTML = panelHtml(w); return; }\n panel.classList.add('is-out');\n clearTimeout(fading);\n fading = setTimeout(function(){\n panel.innerHTML = panelHtml(w);\n panel.classList.remove('is-out');\n }, 240);\n }\n\n /* Two \x3cimg> layers so a change is a cross-fade rather than a blink\n while the next file downloads. */\n /* These two load eagerly on purpose. They are the page's main\n photograph, and a lazy image that is below the fold never loads\n at all -- so its onload never fires, and the picture simply never\n changed when a different way of working was chosen. Only the\n chosen photograph is ever given an address, so this still loads\n one picture, not three. */\n var layers = [document.createElement('img'), document.createElement('img')];\n layers.forEach(function(im){\n im.alt = ''; im.loading = 'eager'; im.decoding = 'async';\n stage.insertBefore(im, stage.firstChild);\n });\n var dLayers = [document.createElement('img'), document.createElement('img')];\n dLayers.forEach(function(im){\n im.alt = ''; im.loading = 'eager'; im.decoding = 'async'; det.appendChild(im);\n });\n\n /* Which layer is currently showing, and a token for the most recent\n request. Both matter: the visible layer must only change once the\n new picture has actually decoded, and a quick second choice has to\n be able to overtake a first one that is still downloading. Advancing\n the bookkeeping at request time instead let the next choice write\n over the layer that was still on screen, so the photograph ran a\n step behind whichever way was chosen. */\n var main = {at:0, tok:0};\n var side = {at:0, tok:0};\n var asked = ''; /* the address most recently requested */\n var started = false; /* the first paint should not fade in */\n\n function swap(pair, st, url, crop){\n if(!url){\n pair[0].classList.remove('is-on'); pair[1].classList.remove('is-on');\n st.tok++;\n return;\n }\n var cur = pair[st.at];\n if(cur.getAttribute('src') === url){ cur.classList.add('is-on'); st.tok++; return; }\n var nextI = st.at ? 0 : 1;\n var im = pair[nextI];\n var mine = ++st.tok;\n if(crop) im.style.objectPosition = crop;\n function show(){\n if(mine !== st.tok) return; /* a newer choice has overtaken this */\n cur.classList.remove('is-on');\n im.classList.add('is-on');\n st.at = nextI;\n }\n im.onload = show;\n im.onerror = function(){}; /* leave the previous picture up */\n if(im.getAttribute('src') !== url) im.setAttribute('src', url);\n if(im.complete && im.naturalWidth) show(); /* already in the cache */\n }\n\n function paint(){\n var w = WAYS[at] || {};\n asked = photo(w.photo);\n swap(layers, main, asked, w.crop);\n var dUrl = photo(w.detail);\n det.hidden = !dUrl;\n swap(dLayers, side, dUrl, '');\n var ph = stage.querySelector('.h-quiet-ph');\n if(ph) ph.style.display = photo(w.photo) ? 'none' : '';\n var tally = w.no + ' / ' + String(WAYS.length).replace(/^(\\d)$/, '0$1');\n /* only write when it actually changes -- this text sits inside the\n tree the observer below is watching, and rewriting it every time\n made the two call each other without end */\n if(count && count.textContent !== tally) count.textContent = tally;\n [].forEach.call(opts.children, function(li, i){\n var on = i === at;\n li.classList.toggle('is-on', on);\n li.querySelector('.h-wh-opt-btn').setAttribute('aria-selected', on ? 'true' : 'false');\n });\n setPanel(w, started);\n started = true;\n }\n opts.addEventListener('click', function(e){\n var btn = e.target.closest('.h-wh-opt-btn');\n if(!btn) return;\n at = +btn.parentElement.dataset.i;\n paint();\n });\n opts.addEventListener('keydown', function(e){\n if(e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;\n e.preventDefault();\n at = (at + (e.key === 'ArrowRight' ? 1 : WAYS.length - 1)) % WAYS.length;\n paint();\n opts.children[at].querySelector('.h-wh-opt-btn').focus();\n });\n paint();\n /* pictures can arrive after this runs, when Carrd adds the embed\n that carries them */\n /* Carrd can add the embed carrying the photographs after this one, so\n the page watches for it. It compares against the last address it\n ASKED for, not the one on screen: a picture still downloading would\n otherwise look like a picture that never arrived, and paint again\n on every mutation -- including the ones paint itself causes. */\n new MutationObserver(function(){\n var want = photo(WAYS[at].photo);\n if(want && want !== asked) paint();\n }).observe(document.documentElement, {childList:true, subtree:true});\n }\n\n /* =================================================================\n WHERE YOU FIT -- the same three routes, said the short way\n ================================================================= */\n var fit = document.getElementById('h-wh-fit');\n var ASKS = [\n \"Have a beautiful space?\",\n \"Have a product you want people to discover?\",\n \"Want an experience created for your brand?\"\n ];\n if(fit && WAYS.length){\n fit.innerHTML = WAYS.map(function(w, i){\n return '\x3cli class=\"h-wh-fit-i h-rev\" style=\"--d:' + (i * 70) + 'ms\">' +\n '\x3ca class=\"h-wh-fit-a\" href=\"#work-with-hera\" data-want=\"' + esc(w.want) + '\">' +\n '\x3cspan class=\"h-wh-fit-q\">' + copy(ASKS[i] || '') + '\x3c/span>' +\n '\x3cspan class=\"h-wh-fit-r\">' + copy(w.name).replace(/\\.$/, '') + ARROW + '\x3c/span>' +\n '\x3c/a>\x3c/li>';\n }).join('');\n }\n\n /* =================================================================\n THE FILM STRIP\n ================================================================= */\n var reel = document.getElementById('h-wh-reel');\n var reelSec = document.getElementById('h-wh-work-s";
                    __hb5+="ec');\n var shots = WORK.filter(function(s){ return String(s && s.src || '').trim(); });\n\n if(reel && reelSec && shots.length){\n reelSec.hidden = false;\n reel.innerHTML = shots.map(function(s, i){\n var shape = String(s.shape || '').trim();\n var meta = [s.type, s.year].filter(function(x){ return String(x || '').trim(); })\n .join(' \x26#183; ');\n var title = String(s.partner || '').trim()\n ? 'Hera \x3cspan aria-hidden=\"true\">\x26#215;\x3c/span> ' + copy(s.partner)\n : copy(s.cap);\n var cap = (title || meta)\n ? '\x3cspan class=\"h-wh-fr-cap\">' +\n (title ? '\x3cspan class=\"h-wh-fr-t\">' + title + '\x3c/span>' : '') +\n (meta ? '\x3cspan class=\"h-wh-fr-m\">' + meta + '\x3c/span>' : '') +\n '\x3c/span>' : '';\n return '\x3cbutton class=\"h-wh-fr' + (shape ? ' is-' + esc(shape) : '') +\n '\" type=\"button\" data-i=\"' + i + '\" aria-label=\"Open photograph\">' +\n '\x3cimg src=\"' + esc(s.src) + '\" alt=\"' + esc(s.alt || s.cap || 'Hera collaboration') +\n '\" loading=\"lazy\" draggable=\"false\">' + cap + '\x3c/button>';\n }).join('');\n\n /* -- drag, with the weight of a real object --------------------\n Native scrolling already carries momentum on a trackpad and a\n phone; this adds it for a mouse, which otherwise stops dead. */\n var bar = document.getElementById('h-wh-bar');\n var down = false, startX = 0, startL = 0, moved = 0, vel = 0, lastX = 0, raf = 0;\n\n function progress(){\n if(!bar) return;\n var max = reel.scrollWidth - reel.clientWidth;\n var frac = reel.clientWidth / reel.scrollWidth;\n bar.style.width = Math.max(8, frac * 100) + '%';\n var travel = max > 0 ? reel.scrollLeft / max : 0;\n bar.style.transform = 'translateX(' + (travel * ((1 / frac) - 1) * 100) + '%)';\n var p = document.getElementById('h-wh-prev'), n = document.getElementById('h-wh-next');\n if(p) p.disabled = reel.scrollLeft <= 2;\n if(n) n.disabled = reel.scrollLeft >= max - 2;\n }\n reel.addEventListener('scroll', progress, {passive:true});\n window.addEventListener('resize', progress);\n\n function glide(){\n vel *= 0.94;\n if(Math.abs(vel) < 0.4){ reel.classList.remove('is-drag'); progress(); return; }\n reel.scrollLeft -= vel;\n progress();\n raf = requestAnimationFrame(glide);\n }\n reel.addEventListener('pointerdown', function(e){\n if(e.pointerType === 'touch') return; /* the phone does its own */\n down = true; moved = 0; vel = 0;\n startX = lastX = e.clientX; startL = reel.scrollLeft;\n cancelAnimationFrame(raf);\n reel.classList.add('is-drag');\n reel.setPointerCapture(e.pointerId);\n });\n reel.addEventListener('pointermove', function(e){\n if(!down) return;\n var dx = e.clientX - startX;\n moved = Math.abs(dx);\n vel = e.clientX - lastX; lastX = e.clientX;\n reel.scrollLeft = startL - dx;\n progress();\n });\n function release(e){\n if(!down) return;\n down = false;\n try{ reel.releasePointerCapture(e.pointerId); }catch(err){}\n if(Math.abs(vel) > 1) raf = requestAnimationFrame(glide);\n else reel.classList.remove('is-drag');\n }\n reel.addEventListener('pointerup', release);\n reel.addEventListener('pointercancel', release);\n\n /* Scroll-snap cancels a smooth programmatic scroll outright in some\n browsers -- scrollBy simply does nothing and reports no error --\n so the easing is done here, with snap held off for the duration. */\n var slide = 0;\n function glideTo(target){\n var from = reel.scrollLeft;\n var max = reel.scrollWidth - reel.clientWidth;\n target = Math.max(0, Math.min(target, max));\n var dist = target - from;\n if(Math.abs(dist) < 1) return;\n if(window.matchMedia('(prefers-reduced-motion:reduce)').matches){\n reel.scrollLeft = target; return;\n }\n cancelAnimationFrame(slide);\n reel.style.scrollSnapType = 'none';\n var t0 = 0, ms = 520, ran = false;\n slide = requestAnimationFrame(function frame(now){\n ran = true;\n if(!t0) t0 = now;\n var p = Math.min(1, (now - t0) / ms);\n reel.scrollLeft = from + dist * (1 - Math.pow(1 - p, 3));\n progress();\n if(p < 1) slide = requestAnimationFrame(frame);\n else reel.style.scrollSnapType = '';\n });\n /* Animation frames are withheld from a hidden or throttled tab, and\n a press that moves nothing at all reads as a broken control. If\n no frame has arrived shortly after the press, go straight there. */\n setTimeout(function(){\n if(ran) return;\n cancelAnimationFrame(slide);\n reel.scrollLeft = target;\n reel.style.scrollSnapType = '';\n progress();\n }, 90);\n }\n /* Steps one photograph at a time, landing each one exactly where the\n first one sits at rest -- so the strip always lines up with the\n heading above it rather than drifting by whatever the last frame\n happened to be wide. */\n function stops(){\n var list = reel.querySelectorAll('.h-wh-fr');\n var base = reel.getBoundingClientRect().left, cur = reel.scrollLeft, out = [];\n for(var i = 0; i < list.length; i++){\n out.push(Math.round(list[i].getBoundingClientRect().left - base + cur));\n }\n return out;\n }\n function step(dir){\n var pos = stops();\n if(!pos.length) return;\n var origin = pos[0] - 0; /* where frame one rests */\n var cur = reel.scrollLeft, target = null, i;\n if(dir > 0){\n for(i = 0; i < pos.length; i++){\n if(pos[i] - origin > cur + 4){ target = pos[i] - origin; break; }\n }\n if(target === null) target = reel.scrollWidth;\n }else{\n for(i = pos.length - 1; i >= 0; i--){\n if(pos[i] - origin < cur - 4){ target = pos[i] - origin; break; }\n }\n if(target === null) target = 0;\n }\n glideTo(target);\n }\n var pv = document.getElementById('h-wh-prev'), nx = document.getElementById('h-wh-next');\n if(pv) pv.addEventListener('click', function(){ step(-1); });\n if(nx) nx.addEventListener('click', function(){ step(1); });\n reel.addEventListener('keydown', function(e){\n if(e.key === 'ArrowRight'){ e.preventDefault(); step(1); }\n if(e.key === 'ArrowLeft'){ e.preventDefault(); step(-1); }\n });\n\n /* -- the lightbox ---------------------------------------------- */\n var lb = document.getElementById('h-wh-lb'),\n lbImg = document.getElementById('h-wh-lb-img'),\n lbT = document.getElementById('h-wh-lb-t');\n var lbAt = 0, lastFocus = null;\n\n function lbPaint(){\n var s = shots[lbAt] || {};\n lbImg.setAttribute('src', String(s.src || ''));\n lbImg.alt = String(s.alt || s.cap || '');\n var title = String(s.partner || '').trim()\n ? 'Hera \x3cspan aria-hidden=\"true\">\x26#215;\x3c/span> ' + copy(s.partner)\n : copy(s.cap);\n var meta = [s.type, s.year].filter(function(x){ return String(x || '').trim(); })\n .join(' \x26#183; ');\n lbT.innerHTML = (title || '') +\n (meta ? '\x3cspan class=\"h-wh-lb-m\">' + meta + '\x3c/span>' : '');\n }\n function lbOpen(i){\n lbAt = i; lastFocus = document.activeElement;\n lb.hidden = false;\n lbPaint();\n requestAnimationFrame(function(){ lb.classList.add('is-on'); });\n if(window.HERA && HERA.hold) HERA.hold('brands-lightbox', true);\n document.getElementById('h-wh-lb-x').focus();\n }\n function lbClose(){\n lb.classList.remove('is-on');\n if(window.HERA && HERA.hold) HERA.hold('brands-lightbox', false);\n setTimeout(function(){ lb.hidden = true; }, 400);\n if(lastFocus && lastFocus.focus) lastFocus.focus();\n }\n function lbGo(d){ lbAt = (lbAt + d + shots.length) % shots.length; lbPaint(); }\n\n reel.addEventListener('click', function(e){\n var fr = e.target.closest('.h-wh-fr');\n if(!fr) return;\n if(moved > 6) return; /* that was a drag, not a click */\n lbOpen(+fr.dataset.i);\n });\n document.getElementById('h-wh-lb-x').addEventListener('click', lbClose);\n document.getElementById('h-wh-lb-p').addEventListener('click', function(){ lbGo(-1); });\n document.getElementById('h-wh-lb-n').addEventListener('click', function(){ lbGo(1); });\n lb.addEventListener('click', function(e){ if(e.target === lb) lbClose(); });\n document.addEventListener('keydown', function(e){\n if(lb.hidden) return;\n if(e.key === 'Escape') lbClose();\n if(e.key === 'ArrowRight') lbGo(1);\n if(e.key === 'ArrowLeft') lbGo(-1);\n });\n progress();\n }\n\n /* =================================================================\n CASE STUDIES -- nothing entered, nothing shown\n ================================================================= */\n var caseBox = document.getElementById('h-wh-cases');";
                    __hb5+="\n var caseSec = document.getElementById('h-wh-cases-sec');\n if(caseBox && caseSec){\n var real = CASES.filter(function(c){ return String(c && c.partner || '').trim(); });\n if(real.length){\n caseSec.hidden = false;\n caseBox.innerHTML = real.map(function(c, i){\n var pics = (c.shots || []).filter(function(u){ return String(u || '').trim(); });\n var strip = pics.length\n ? '\x3cdiv class=\"h-wh-case-shots\">' + pics.slice(0, 4).map(function(u){\n return '\x3cimg src=\"' + esc(u) + '\" alt=\"' + esc(c.partner) +\n '\" loading=\"lazy\">'; }).join('') + '\x3c/div>' : '';\n var row = function(label, val){\n return String(val || '').trim()\n ? '\x3cdiv class=\"h-wh-case-row\">\x3cdt>' + esc(label) + '\x3c/dt>\x3cdd>' +\n copy(val) + '\x3c/dd>\x3c/div>' : '';\n };\n return '\x3carticle class=\"h-wh-case h-rev\" style=\"--d:' + (i * 80) + 'ms\">' +\n '\x3cp class=\"h-wh-case-t\">Hera \x3cspan aria-hidden=\"true\">\x26#215;\x3c/span> ' +\n copy(c.partner) + '\x3c/p>' +\n '\x3cdl class=\"h-wh-case-d\">' + row('Collaboration', c.kind) +\n row('What we made', c.made) + row('What guests found', c.felt) + '\x3c/dl>' +\n strip + '\x3c/article>';\n }).join('');\n }\n }\n\n /* =================================================================\n THE FORM\n ================================================================= */\n var form = document.getElementById('h-whf');\n if(!form) return;\n var err = document.getElementById('h-whf-err');\n var wantBox = document.getElementById('h-whf-want');\n var want = '';\n\n if(wantBox){\n wantBox.innerHTML = WANTS.map(function(w){\n return '\x3clabel class=\"h-wh-seg-o\">' +\n '\x3cinput type=\"radio\" name=\"h-whf-want\" value=\"' + esc(w) + '\">' +\n '\x3cspan>' + esc(w) + '\x3c/span>\x3c/label>';\n }).join('');\n wantBox.addEventListener('change', function(e){\n if(!e.target || e.target.type !== 'radio') return;\n want = e.target.value;\n var f = document.getElementById('h-whf-want-f');\n if(f) f.classList.remove('is-bad');\n });\n }\n /* every route on the page lands here with its answer already given */\n function choose(w){\n if(!wantBox) return;\n var r = wantBox.querySelector('input[value=\"' + String(w).replace(/\"/g, '') + '\"]');\n if(!r) return;\n r.checked = true;\n r.dispatchEvent(new Event('change', {bubbles:true}));\n }\n document.addEventListener('click', function(e){\n var a = e.target.closest('a[data-want]');\n if(!a) return;\n choose(a.dataset.want);\n });\n\n /* ==================================================================\n ## H E R A O W N E R -- S A F E T O E D I T ##\n ==================================================================\n The three ways Hera is reached. The form asks which one they\n prefer, then asks for THAT one, so nobody hands over a number\n they never answer.\n\n label the wording above the box\n ph the faint example inside the empty box\n missing how it is named if they forget to fill it in\n ================================================================== */\n var HANDLE = {\n WhatsApp: {label:'WhatsApp number', ph:'+84 90 123 4567', type:'tel',\n ac:'tel', missing:'a WhatsApp number'},\n Zalo: {label:'Zalo number', ph:'+84 90 123 4567', type:'tel',\n ac:'tel', missing:'a Zalo number'},\n Telegram: {label:'Telegram username', ph:'@username', type:'text',\n ac:'off', missing:'a Telegram username'}\n };\n\n /* --- nothing below this line needs editing --- */\n function channel(){\n var s = document.getElementById('h-whf-how');\n var v = s ? s.value : 'WhatsApp';\n return HANDLE[v] ? v : 'WhatsApp';\n }\n function syncHandle(){\n var c = HANDLE[channel()],\n l = document.getElementById('h-whf-handle-l'),\n i = document.getElementById('h-whf-handle');\n if(!l || !i) return;\n l.textContent = c.label;\n i.placeholder = c.ph;\n i.type = c.type;\n i.setAttribute('autocomplete', c.ac);\n }\n var howSel = document.getElementById('h-whf-how');\n if(howSel) howSel.addEventListener('change', syncHandle);\n syncHandle();\n\n /* ---- files ---------------------------------------------------- */\n var fileIn = document.getElementById('h-whf-files'),\n pick = document.getElementById('h-whf-pick'),\n countF = document.getElementById('h-whf-count'),\n thumbs = document.getElementById('h-whf-thumbs'),\n picked = [], CAP = 8;\n if(pick && fileIn){\n pick.addEventListener('click', function(){ fileIn.click(); });\n fileIn.addEventListener('change', function(){\n [].forEach.call(fileIn.files, function(f){ if(picked.length < CAP) picked.push(f); });\n fileIn.value = '';\n paintFiles();\n });\n }\n function paintFiles(){\n if(!countF || !thumbs) return;\n countF.textContent = picked.length\n ? picked.length + (picked.length === 1 ? ' file chosen' : ' files chosen') +\n (picked.length >= CAP ? ' (the most we can take)' : '')\n : 'No files chosen';\n thumbs.innerHTML = '';\n picked.forEach(function(f, i){\n var cell = document.createElement('div');\n cell.className = 'h-up-thumb';\n if(/^image\\//.test(f.type)){\n var img = document.createElement('img');\n img.alt = f.name; img.src = URL.createObjectURL(f);\n img.onload = function(){ URL.revokeObjectURL(img.src); };\n cell.appendChild(img);\n }else{\n var doc = document.createElement('span');\n doc.className = 'h-up-doc';\n doc.textContent = f.name.replace(/^(.{16}).+(\\.[a-z0-9]+)$/i, '$1...$2');\n cell.appendChild(doc);\n }\n var x = document.createElement('button');\n x.type = 'button'; x.className = 'h-up-x';\n x.setAttribute('aria-label', 'Remove ' + f.name);\n x.textContent = '\\u00d7';\n x.addEventListener('click', function(){ picked.splice(i, 1); paintFiles(); });\n cell.appendChild(x);\n thumbs.appendChild(cell);\n });\n }\n\n /* ---- sending --------------------------------------------------- */\n form.addEventListener('submit', function(e){\n e.preventDefault();\n var missing = [], firstBad = null;\n function fail(field, words){\n if(field){\n field.classList.add('is-bad');\n if(!firstBad) firstBad = field.querySelector('input,textarea') || field;\n }\n missing.push(words);\n }\n function ok(field){ if(field) field.classList.remove('is-bad'); }\n\n var g = function(id){ return document.getElementById(id); };\n var email = g('h-whf-email');\n var wantF = g('h-whf-want-f');\n\n /* Everything about who you are is required, and so are the four\n things we cannot quote without: what, when, where and roughly\n how much. Guest count and the attachments stay optional. */\n [[g('h-whf-co'), 'your company or brand', !!g('h-whf-co').value.trim()],\n [g('h-whf-name'), 'your name', !!g('h-whf-name').value.trim()],\n [g('h-whf-role'), 'your role', !!g('h-whf-role').value.trim()],\n [g('h-whf-site'), 'a website or Instagram', !!g('h-whf-site').value.trim()],\n [email, 'an email we can reply to',\n /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email.value.trim())],\n [g('h-whf-handle'), HANDLE[channel()].missing, !!g('h-whf-handle').value.trim()],\n [g('h-whf-brief'), 'a little about what you are planning', !!g('h-whf-brief').value.trim()],\n [g('h-whf-when'), 'when it is', !!g('h-whf-when').value.trim()],\n [g('h-whf-where'), 'where it is', !!g('h-whf-where').value.trim()],\n [g('h-whf-budget'), 'an approximate budget', !!g('h-whf-budget').value.trim()]\n ].forEach(function(row){\n var field = row[0].closest('.h-bl-field');\n ok(field);\n if(!row[2]) fail(field, row[1]);\n });\n ok(wantF);\n if(!want) fail(wantF, 'what brings you here');\n\n if(missing.length){\n err.hidden = false;\n err.textContent = 'We still need ' + (missing.length === 1 ? missing[0]\n : missing.slice(0, -1).join(', ') + ' and ' + missing[missing.length - 1]) + '.';\n if(firstBad && firstBad.focus) firstBad.focus();\n else (form.querySelector('.is-bad') || form).scrollIntoView({behavior:'smooth', block:'center'});\n return;\n }\n err.hidden = true;\n form.innerHTML =\n '\x3cdiv class=\"h-req-done\">' +\n '\x3ch3 class=\"h-req-done-h\">Thank you \x26#8212; we have your note.\x3c/h3>' +\n '\x3cp class=\"h-xo-p\">We read these ourselves. Expect a reply within two ' +\n 'working days, usually with a few questions and an idea of where ' +\n 'Hera could fit.\x3c/p>' +\n '\x3cp class=\"h-req-note\">A copy has been emailed to you as well, so you have a record of it.\x3c/p>' +\n '\x3c/div>';\n form.scrollIntoView({behavior:'smooth', block:'center'});\n });\n\n if(window.__heraScan) window.__heraScan();\n})();\n"; (function(){var s=document.createElement("script");s.textContent=__hb5;document.head.appendChild(s);__hb5=null;})();
                    /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 1. Y O U R P H O T O G R A P H S ================================================================== This page wants MORE PEOPLE AND PROCESS than finished bouquets. A visitor is deciding whether Hera is real and capable, and hands at work say that better than another perfect arrangement. abHero the opening picture. Wide. NOT a bouquet on a table -- an installation, a room being built, a workshop in progress, the team at work. abDetail a floral close-up. The smallest scale of the work. abLarge the largest thing Hera has made. An installation or a whole space. abGuests people inside a finished Hera space, enjoying it. abTeamPeople THE TEAM. A HORIZONTAL photograph with the people in it. It fills half the studio section on its own, so it wants to be a wide shot rather than an upright one. abTeam1 another moment of the team, for the filmstrip abTeam2 a third, for the filmstrip abProcess hands, stems, tools -- the work happening abStudio the studio itself abEvent an event set up and ready abWorkshop a workshop or floral experience Upload each in Carrd, copy the address, paste it between the quote marks, and LEAVE the Image element on the page -- Carrd deletes any picture that nothing is using. Leave any of them as "" and a quiet labelled space holds its place. The page works before the photography exists. ================================================================== */ window.HERA_PHOTOS = window.HERA_PHOTOS || {}; (function(m){ Object.keys(m).forEach(function(k){ var v = String(m[k] == null ? '' : m[k]).trim(); if(v || !window.HERA_PHOTOS[k]) window.HERA_PHOTOS[k] = m[k]; }); })({ abHero: "", abDetail: "", abLarge: "", abGuests: "", abTeamPeople: "", abTeam1: "", abTeam2: "", abProcess: "", abStudio: "", abEvent: "", abWorkshop: "" }); /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 2. W H E R E T O C R O P O N A P H O N E ================================================================== A phone is tall and narrow, so a wide photograph loses its left and right edges. By default the middle is kept, which can cut off whoever is standing to one side. first number = left to right 0% left, 50% middle, 100% right second number = top to bottom 0% top, 50% middle, 100% bottom Only phones use these. Changing them cannot affect a computer. ================================================================== */ window.HERA_ABOUT_CROP = { abHero: "50% 50%", abLarge: "50% 50%", abEvent: "50% 50%" }; /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== 3. T H E W O R D S Y O U C H A N G E ================================================================== Three short lists. Everything else on the page is ordinary words you can type over in the markup below. ================================================================== */ /* How Hera works. Four lines at most -- this section is meant to be nearly empty. A fifth line turns it into a list of values. */ window.HERA_ABOUT_APPROACH = [ { t:"Seasonal by nature", b:"We design with what is at its best that week, so no two arrangements are ever identical." }, { t:"Composed, not copied", b:"We work from tone, shape and feeling rather than reproducing a picture stem for stem." }, { t:"Built around the moment", b:"The occasion decides the scale, the palette and how the room should feel to walk into." } ]; /* The range. Short lines, deliberately unpunctuated by explanation -- the point is how far apart the first and last one are. */ window.HERA_ABOUT_RANGE = [ "A bouquet.", "A proposal.", "A dinner.", "A wedding.", "A workshop.", "A brand experience.", "A space transformed by flowers." ]; /* The three worlds. Keep each line to one sentence. "go" is the address it leads to. */ window.HERA_ABOUT_INDEX = [ { name:"Blooms", line:"Bouquets and arrangements for everyday and meaningful moments.", go:"#blooms", link:"blooms" }, { name:"Experiences", line:"Workshops, masterclasses, the Bloom Bar and public floral experiences.", go:"#experiences", link:"experiences" }, { name:"Events", line:"Private celebrations, hotels, brands and creative collaborations.", go:"#events", link:"events" } ]; /* The filmstrip further down the page. Six to ten small photographs in one row that you drag sideways. They all share the same height, and each keeps its own natural width, so uprights and landscapes sit together without any of them being cropped to a square. Add a line to add a picture. Remove one to take it out. There is no layout to get wrong. Just the names of the pictures, in the order you want them. */ /* Left empty on purpose -- only three photos are in use on this page (abHero, abEvent, abWorkshop), and this strip further down would otherwise show two of them a second time. Add names back in here if a scrolling strip of studio photos is ever wanted. */ window.HERA_ABOUT_STRIP = [];
                    Photo · About · the opening picture

                    About Hera

                    We do flowers differently.

                    Hera is a Vietnamese-American floral studio in Đà Nẵng, creating distinctive florals and the experiences around them.

                    Our difference

                    More than the flowers.

                    For us, flowers are usually where the idea begins, not where it ends.

                    Hera approaches each project as a complete experience: the flowers, the space, the people, the details and how everything comes together on the day. From a single arrangement to a proposal, event or brand experience, we move between floral studio, creative partner and execution team depending on what the work calls for.

                    Photo · an event, set

                    Perspective

                    Rooted here.
                    Shaped by more than one place.

                    Hera was created in Đà Nẵng and shaped by both Vietnamese and Western ideas of flowers, of design, and of how people gather and celebrate. We don't choose between them. We take what interests us from both and interpret it through Hera.

                    Behind the work

                    Creative enough to imagine it.Experienced enough to make it happen.

                    Beautiful is only part of the job. Timing, vendors, logistics, communication and execution matter just as much. Years across florals, events and project coordination allow Hera to carry an idea from the first conversation through the final detail.

                    The studio

                    The people behind it.

                    Hera is a small studio, and that is deliberate. The people who take your first call are the people cutting stems the morning of, and standing in the room while it is being built.

                    Photo · the team · horizontal

                    Our approach

                    Made with intention.

                        Different scale. Same intention.

                        What Hera creates

                          Flowers are temporary.
                          The feeling shouldn't be.

                          Let's create something worth sharing.

                          /* ================================================================== Nothing below this line needs editing. Builds the four lists from the data above. Each one disappears cleanly if its list is emptied, so the page can never show a heading with nothing under it. ================================================================== */ (function(){ function esc(s){ return String(s == null ? '' : s) .replace(/&(?!#?\w+;)/g, '\x26amp;').replace(/\x3c/g, '\x26lt;') .replace(/>/g, '\x26gt;').replace(/"/g, '\x26quot;'); } /* the copy above is written with a few entities of its own, so it is trusted as typed rather than escaped a second time */ function copy(s){ return String(s == null ? '' : s); } function photo(key){ return String((window.HERA_PHOTOS || {})[key] || '').trim(); } var ARROW = '\x3csvg width="17" height="10" viewBox="0 0 17 10" fill="none" ' + 'stroke="currentColor" stroke-width="1.1" aria-hidden="true">' + '\x3cpath d="M0 5h15M11.5 1 15.5 5l-4 4" stroke-linecap="round" stroke-linejoin="round"/>\x3c/svg>'; function fill(id, list, draw, hideSection){ var box = document.getElementById(id); if(!box) return; var rows = (list || []).filter(Boolean); if(!rows.length){ var sec = box.closest('section'); if(hideSection && sec) sec.hidden = true; return; } box.innerHTML = rows.map(draw).join(''); } /* ---- how we work ------------------------------------------------ */ fill('h-abp-how', window.HERA_ABOUT_APPROACH, function(a, i){ return '\x3cli class="h-abp-how-i h-rev" style="--d:' + (i * 80) + 'ms">' + '\x3cp class="h-abp-how-t">' + copy(a.t) + '\x3c/p>' + '\x3cp class="h-abp-how-b">' + copy(a.b) + '\x3c/p>\x3c/li>'; }, true); /* ---- the range -------------------------------------------------- Each line steps a little further in than the one above, so the list reads as a widening scale rather than a menu. */ fill('h-abp-range', window.HERA_ABOUT_RANGE, function(line, i){ return '\x3cli class="h-abp-range-i h-rev" style="--d:' + (i * 70) + 'ms;--step:' + i + '">' + copy(line) + '\x3c/li>'; }, true); /* ---- the three worlds ------------------------------------------- */ fill('h-abp-idx', window.HERA_ABOUT_INDEX, function(w, i){ return '\x3cli class="h-abp-idx-i h-rev" style="--d:' + (i * 80) + 'ms">' + '\x3ca class="h-abp-idx-a" href="' + esc(w.go || '#') + '"' + (w.link ? ' data-link="' + esc(w.link) + '"' : '') + '>' + '\x3cspan class="h-abp-idx-n">' + copy(w.name) + '\x3c/span>' + '\x3cspan class="h-abp-idx-l">' + copy(w.line) + '\x3c/span>' + '\x3cspan class="h-abp-idx-go" aria-hidden="true">' + ARROW + '\x3c/span>' + '\x3c/a>\x3c/li>'; }, true); /* ---- the filmstrip ---------------------------------------------- One row, one height, natural widths. Dragging is added for a mouse -- a trackpad and a phone already carry their own momentum. */ var strip = document.getElementById('h-abp-strip'); var STRIP = (window.HERA_ABOUT_STRIP || []).filter(Boolean); if(strip && STRIP.length){ strip.innerHTML = STRIP.map(function(key, i){ return '\x3cfigure class="h-abp-strip-i h-rev" style="--d:' + ((i % 4) * 70) + 'ms" data-photo="' + esc(key) + '" data-alt="From the Hera studio">' + '\x3cspan class="h-quiet-ph">\x3cspan>From the studio\x3c/span>\x3c/span>\x3c/figure>'; }).join(''); var down = false, startX = 0, startL = 0, vel = 0, lastX = 0, raf = 0, slide = 0; function glide(){ vel *= 0.94; if(Math.abs(vel) < 0.4){ strip.classList.remove('is-drag'); return; } strip.scrollLeft -= vel; raf = requestAnimationFrame(glide); } strip.addEventListener('pointerdown', function(e){ if(e.pointerType === 'touch') return; /* the phone does its own */ down = true; vel = 0; startX = lastX = e.clientX; startL = strip.scrollLeft; cancelAnimationFrame(raf); strip.classList.add('is-drag'); strip.setPointerCapture(e.pointerId); }); strip.addEventListener('pointermove', function(e){ if(!down) return; vel = e.clientX - lastX; lastX = e.clientX; strip.scrollLeft = startL - (e.clientX - startX); }); function release(e){ if(!down) return; down = false; try{ strip.releasePointerCapture(e.pointerId); }catch(err){} if(Math.abs(vel) > 1) raf = requestAnimationFrame(glide); else strip.classList.remove('is-drag'); } strip.addEventListener('pointerup', release); strip.addEventListener('pointercancel', release); function ends(){ var max = strip.scrollWidth - strip.clientWidth; var p = document.getElementById('h-abp-prev'), n = document.getElementById('h-abp-next'); if(p) p.disabled = strip.scrollLeft <= 2; if(n) n.disabled = strip.scrollLeft >= max - 2; } /* Scroll-snap and a smooth programmatic scroll cancel each other in some browsers, and animation frames are withheld from a hidden tab. Ease when we can, arrive either way. */ function glideTo(target){ var from = strip.scrollLeft; var max = strip.scrollWidth - strip.clientWidth; target = Math.max(0, Math.min(target, max)); var dist = target - from; if(Math.abs(dist) < 1) return; if(window.matchMedia('(prefers-reduced-motion:reduce)').matches){ strip.scrollLeft = target; ends(); return; } cancelAnimationFrame(slide); var t0 = 0, ms = 460, ran = false; slide = requestAnimationFrame(function frame(now){ ran = true; if(!t0) t0 = now; var p = Math.min(1, (now - t0) / ms); strip.scrollLeft = from + dist * (1 - Math.pow(1 - p, 3)); ends(); if(p < 1) slide = requestAnimationFrame(frame); }); setTimeout(function(){ if(ran) return; cancelAnimationFrame(slide); strip.scrollLeft = target; ends(); }, 90); } function step(dir){ glideTo(strip.scrollLeft + dir * strip.clientWidth * 0.8); } var pv = document.getElementById('h-abp-prev'), nx = document.getElementById('h-abp-next'); if(pv) pv.addEventListener('click', function(){ step(-1); }); if(nx) nx.addEventListener('click', function(){ step(1); }); strip.addEventListener('scroll', ends, {passive:true}); window.addEventListener('resize', ends); strip.addEventListener('keydown', function(e){ if(e.key === 'ArrowRight'){ e.preventDefault(); step(1); } if(e.key === 'ArrowLeft'){ e.preventDefault(); step(-1); } }); ends(); } /* The drifting bands are the botanical pattern used as a mask. If that picture has not been uploaded yet the mask is "none", which does not hide the band -- it shows the whole blue rectangle instead. So the bands only appear once there is a pattern to cut them from. */ function bands(){ var art = getComputedStyle(document.documentElement) .getPropertyValue('--h-toile').trim(); var on = art && art !== 'none'; var els = document.querySelectorAll('.h-abp .h-toile'); for(var i = 0; i < els.length; i++){ els[i].style.display = on ? '' : 'none'; } } bands(); new MutationObserver(bands).observe(document.documentElement, {childList:true, subtree:true}); /* ---- phone crops ------------------------------------------------ */ function applyCrop(){ var crop = window.HERA_ABOUT_CROP || {}; Object.keys(crop).forEach(function(key){ var v = String(crop[key] || '').trim(); if(!v) return; var frames = document.querySelectorAll('[data-photo="' + key + '"]'); for(var i = 0; i < frames.length; i++) frames[i].style.setProperty('--focus-m', v); }); } applyCrop(); /* the sections are built here, so the shared code has to be told to look again for photographs and for things to reveal */ if(window.__heraApplyPhotos) window.__heraApplyPhotos(); if(window.__heraScan) window.__heraScan(); })();
                          /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== H O W T O R E A C H H E R A ================================================================== Fill these in and the row appears on the page. Leave one empty and that row is simply not shown, so nothing ever reads "coming soon". wa the WhatsApp number, digits only, country code first e.g. "84901234567". The page builds the link. waShow how it should read on screen, e.g. "+84 90 123 4567" ig the Instagram handle WITHOUT the @ email the address people should write to studio where the studio is ================================================================== */ window.HERA_REACH = { wa: "", waShow: "", ig: "herafloral.co", email: "", studio: "\u0110\u00e0 N\u1eb5ng, Vietnam" };

                          Contact

                          Tell us whatthe moment calls for.

                          Tell us who you are, what it's about and what you need from us. We read every note ourselves and reply directly.

                          Send a note

                          What can we do?

                          Tell us who you are and what you need, and we'll take it from there.

                          If it is none of these, write to us below.

                          Separate inquiries.

                            We'll take it from here.

                            Hera Floral · Đà Nẵng

                            /* ================================================================== Nothing below this line needs editing. ================================================================== */ (function(){ function esc(s){ return String(s == null ? '' : s) .replace(/&(?!#?\w+;)/g, '\x26amp;').replace(/\x3c/g, '\x26lt;') .replace(/>/g, '\x26gt;').replace(/"/g, '\x26quot;'); } function copy(s){ return String(s == null ? '' : s); } var ARROW = '\x3csvg width="17" height="10" viewBox="0 0 17 10" fill="none" ' + 'stroke="currentColor" stroke-width="1.1" aria-hidden="true">' + '\x3cpath d="M0 5h15M11.5 1 15.5 5l-4 4" stroke-linecap="round" stroke-linejoin="round"/>\x3c/svg>'; /* ---- the note ---------------------------------------------------- Each message sits under the field it belongs to, never in a box at the top that leaves you hunting for what is wrong. */ var form = document.getElementById('h-ct-form'); if(form){ /* ================================================================== ## H E R A O W N E R -- S A F E T O E D I T ## ================================================================== The three ways Hera is reached. The form asks which one they prefer and then asks for that one, so nobody hands over a number they never answer. ================================================================== */ var HANDLE = { WhatsApp: {label:'WhatsApp number', ph:'+84 90 123 4567', type:'tel', ac:'tel', missing:'a WhatsApp number'}, Zalo: {label:'Zalo number', ph:'+84 90 123 4567', type:'tel', ac:'tel', missing:'a Zalo number'}, Instagram: {label:'Instagram handle', ph:'@username', type:'text', ac:'off', missing:'an Instagram handle'} }; /* --- nothing below this line needs editing --- */ function channel(){ var s = document.getElementById('ct-how'); var v = s ? s.value : 'WhatsApp'; return HANDLE[v] ? v : 'WhatsApp'; } function syncHandle(){ var c = HANDLE[channel()], l = document.getElementById('ct-reach-l'), i = document.getElementById('ct-reach'); if(!l || !i) return; l.textContent = c.label; i.placeholder = c.ph; i.type = c.type; i.setAttribute('autocomplete', c.ac); } var how = document.getElementById('ct-how'); if(how) how.addEventListener('change', syncHandle); syncHandle(); function ASKS(){ return [ {id:'ct-name', msg:'Please add your name.'}, {id:'ct-reach', msg:'Please add ' + HANDLE[channel()].missing + '.'}, {id:'ct-email', msg:'Please add an email address.', email:true}, {id:'ct-why', msg:'Please tell us what it is about.'}, {id:'ct-need', msg:'Please tell us a little here.'} ]; } form.addEventListener('submit', function(e){ e.preventDefault(); var bad = 0, firstBad = null; ASKS().forEach(function(a){ var el = document.getElementById(a.id); var box = form.querySelector('[data-f="' + a.id + '"]'); var err = document.getElementById(a.id + '-e'); if(!el || !box || !err) return; box.classList.remove('is-bad'); err.textContent = ''; var v = (el.value || '').trim(); var msg = ''; if(!v) msg = a.msg; else if(a.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) msg = 'That does not look like an email address.'; if(!msg) return; box.classList.add('is-bad'); err.textContent = msg; bad++; if(!firstBad) firstBad = el; }); var sum = document.getElementById('h-ct-sum'); if(bad){ if(sum){ sum.hidden = false; sum.textContent = bad === 1 ? 'One answer still needs your attention.' : bad + ' answers still need your attention.'; } if(firstBad && firstBad.focus) firstBad.focus(); return; } if(sum) sum.hidden = true; var host = form.parentElement; form.style.transition = 'opacity .26s cubic-bezier(.22,1,.36,1)'; form.style.opacity = '0'; setTimeout(function(){ form.outerHTML = '\x3cdiv class="h-ct-done">' + '\x3cp class="h-ct-eye">Received\x3c/p>' + '\x3cp class="h-ct-done-h">It\u2019s in our hands.\x3c/p>' + '\x3cp class="h-ct-done-p">Thank you for thinking of Hera. We\u2019ll read the ' + 'details and get back to you directly.\x3c/p>' + '\x3ca class="h-ct-done-go" href="#top" data-link="home">Back to Hera' + ARROW + '\x3c/a>' + '\x3cp class="h-ct-after">A copy has been emailed to you as well, ' + 'so you have a record of it.\x3c/p>' + '\x3c/div>'; var done = host.querySelector('.h-ct-done'); if(done) done.scrollIntoView({behavior:'smooth', block:'center'}); }, 280); }); } /* ---- how to reach Hera ------------------------------------------- A row appears only when there is something real to put in it. */ var R = window.HERA_REACH || {}; var reach = document.getElementById('h-ct-reach'); if(reach){ var lines = []; if(String(R.wa || '').trim()){ lines.push(['WhatsApp', copy(R.waShow || R.wa), 'https://wa.me/' + String(R.wa).replace(/[^0-9]/g, '')]); } if(String(R.ig || '').trim()){ var h = String(R.ig).replace(/^@/, ''); lines.push(['Instagram', '@' + copy(h), 'https://instagram.com/' + h]); } if(String(R.email || '').trim()){ lines.push(['Email', copy(R.email), 'mailto:' + String(R.email).trim()]); } if(String(R.studio || '').trim()) lines.push(['Studio', copy(R.studio), '']); reach.innerHTML = lines.length ? lines.map(function(row, i){ var inner = '\x3cspan class="h-ct-reach-k">' + esc(row[0]) + '\x3c/span>' + '\x3cspan class="h-ct-reach-v">' + row[1] + '\x3c/span>'; return '\x3cli class="h-ct-reach-i h-rev" style="--d:' + (i * 70) + 'ms">' + (row[2] ? '\x3ca class="h-ct-reach-a" href="' + esc(row[2]) + '"' + (/^https/.test(row[2]) ? ' target="_blank" rel="noopener"' : '') + '>' + inner + '\x3cspan class="h-ct-reach-go" aria-hidden="true">' + ARROW + '\x3c/span>\x3c/a>' : '\x3cspan class="h-ct-reach-a is-flat">' + inner + '\x3c/span>') + '\x3c/li>'; }).join('') : '\x3cli class="h-ct-reach-i">\x3cspan class="h-ct-reach-a is-flat">' + '\x3cspan class="h-ct-reach-k">Contact\x3c/span>' + '\x3cspan class="h-ct-reach-v">To be added\x3c/span>\x3c/span>\x3c/li>'; } if(window.__heraScan) window.__heraScan(); })();
                            Skip to content
                            Floral studio · Đà Nẵng · Việt Nam

                            Your order

                            Almost yours.

                            Review your flowers and leave us the details. We'll be in touch to confirm availability, delivery and payment.

                            01

                            Your flowers

                            02

                            Your details

                            Which apps can we message you on? (choose any)Pick the ones you actually use — we'll only ask for those. We have your email and phone number already.
                            03

                            When do you need it?

                            Requested date
                            Requested time
                            What time exactly?
                            Timing is a request rather than a promise — we'll confirm it with you.
                            04

                            How would you like to receive it?

                            We'll confirm the address and the delivery fee with you when we reply — there's nothing more to fill in here.

                            05

                            Who are the flowers for? optional

                            Are you the recipient?
                            06

                            Anything else we should know? optional

                            Roughly what would you like to spend? (a range is fine)
                            07

                            Before you send

                            No payment is taken now. Hera will contact you to confirm flower availability, final pricing, delivery and payment.

                            Request received.
                            We'll be in touch soon.

                            Your flowers aren't confirmed just yet. Hera will contact you using your preferred method to confirm availability, timing, delivery and payment.

                            var __hb3="\n/* ============================================================\n REVIEW ORDER -- nothing to edit in here.\n\n The words are in the markup above. The lists of choices come\n from Settings -> Code -> Head, so the contact methods, time\n windows and wrapping colours are the same ones used elsewhere\n on the site.\n\n >> NOTHING IS SENT ANYWHERE YET. sendRequest() at the bottom\n assembles the complete order and hands it to one function.\n That function is the only thing that changes once you have\n decided where requests should go.\n ============================================================ */\n(function(){\n var form = document.getElementById('h-rv-form');\n if(!form || !window.HERA || !HERA.bag) return;\n\n var F = (typeof HERA_FORM !== 'undefined' && HERA_FORM) || {};\n var CFG = (typeof HERA_BAG !== 'undefined' && HERA_BAG) || {};\n var BAGNAME = HERA.bag.name;\n\n function esc(s){\n return String(s == null ? '' : s)\n .replace(/&/g,'\x26amp;').replace(/\x3c/g,'\x26lt;').replace(/>/g,'\x26gt;').replace(/\"/g,'\x26quot;');\n }\n function $(id){ return document.getElementById(id); }\n function val(id){ var e = $(id); return e ? String(e.value || '').trim() : ''; }\n\n /* ---------- 01 \\u00b7 what is in the bag ------------------------- */\n function money(n){\n return String(n).replace(/\\B(?=(\\d{3})+(?!\\d))/g, '.');\n }\n /* Only prices that are plainly a number can be added up. \"On\n request\" is not a number, and a total that quietly ignored it\n would be a lie \\u2014 so it is counted separately and said out loud. */\n function priceOf(it){\n var digits = String(it.price || '').replace(/[^0-9]/g, '');\n return digits ? parseInt(digits, 10) : null;\n }\n\n function drawItems(){\n var items = HERA.bag.all(),\n box = $('h-rv-items'),\n tot = $('h-rv-total'),\n cav = $('h-rv-caveat');\n\n if(!items.length){\n box.innerHTML =\n '\x3cdiv class=\"h-rv-empty\">' +\n '\x3cp>Your ' + esc(BAGNAME.toLowerCase()) + ' is empty, so there is nothing to send yet.\x3c/p>' +\n '\x3ca class=\"h-btn h-btn--ghost\" href=\"#blooms\" data-link=\"blooms\">Browse Blooms \\u2192\x3c/a>' +\n '\x3c/div>';\n tot.style.display = 'none';\n cav.style.display = 'none';\n $('h-rv-send').disabled = true;\n return;\n }\n $('h-rv-send').disabled = false;\n\n box.innerHTML = items.map(function(it, i){\n var rows = [];\n if(it.tone) rows.push(['Tone', it.tone]);\n if(it.wrapStyle) rows.push(['Wrapping', it.wrapStyle]);\n (it.options || []).forEach(function(o){ if(o && o.value) rows.push([o.label, o.value]); });\n if(it.wrapColour) rows.push(['Colour', it.wrapColour]);\n if(it.card) rows.push(['Card', '\\u201c' + it.card + '\\u201d']);\n if(it.special) rows.push(['Request', it.special]);\n\n var p = priceOf(it), lineText = '';\n if(CFG.showPrices && it.price){\n lineText = p !== null\n ? (it.priceFrom ? 'From ' : '') + money(p * (+it.qty || 1)) + '\\u0111'\n : esc(it.price);\n }\n\n return '\x3cdiv class=\"h-rv-item\" data-i=\"' + i + '\">' +\n '\x3cspan class=\"h-rv-shot\">' +\n (it.image ? '\x3cimg src=\"' + esc(it.image) + '\" alt=\"\">' : '') +\n '\x3c/span>' +\n '\x3cdiv>' +\n '\x3cp class=\"h-rv-nm\">' + esc(it.name) +\n (it.size ? '\x3cspan class=\"h-rv-sz\">' + esc(it.size) + '\x3c/span>' : '') + '\x3c/p>' +\n '\x3cdl class=\"h-rv-spec\">' + rows.map(function(r){\n return '\x3cdiv>\x3cdt>' + esc(r[0]) + '\x3c/dt>\x3cdd>' + esc(r[1]) + '\x3c/dd>\x3c/div>';\n }).join('') + '\x3c/dl>' +\n '\x3clabel class=\"h-rv-note\">\x3cspan>Anything about this one? ' +\n '\x3cspan class=\"h-bl-req\">(optional)\x3c/span>\x3c/span>' +\n '\x3ctextarea class=\"h-bl-ta\" data-note=\"' + i + '\" rows=\"2\" ' +\n 'placeholder=\"More pink, no lilies, extra greenery \\u2014 anything at all.\">' +\n esc(it.special || '') + '\x3c/textarea>\x3c/label>' +\n '\x3cdiv class=\"h-rv-line\">' +\n '\x3cdiv class=\"h-qty\" data-q=\"' + i + '\">' +\n '\x3cbutton type=\"button\" data-step=\"-1\" aria-label=\"Fewer\">\\u2212\x3c/button>' +\n '\x3cinput type=\"text\" inputmode=\"numeric\" value=\"' + (+it.qty || 1) +\n '\" aria-label=\"Quantity of ' + esc(it.name) + '\">' +\n '\x3cbutton type=\"button\" data-step=\"1\" aria-label=\"More\">+\x3c/button>' +\n '\x3c/div>' +\n '\x3cbutton class=\"h-ob-link\" type=\"button\" data-edit=\"' + i + '\">Edit\x3c/button>' +\n '\x3cbutton class=\"h-ob-link is-danger\" type=\"button\" data-del=\"' + i + '\">Remove\x3c/button>' +\n (lineText ? '\x3cspan class=\"h-rv-price\">' + lineText + '\x3c/span>' : '') +\n '\x3c/div>' +\n '\x3c/div>' +\n '\x3c/div>';\n }).join('');\n\n /* the total */\n if(CFG.showPrices){\n var sum = 0, unknown = 0;\n items.forEach(function(it){\n var p = priceOf(it);\n if(p === null) unknown++;\n else sum += p * (+it.qty || 1);\n });\n $('h-rv-total-v').textContent = money(sum) + '\\u0111' +\n (unknown ? ' + ' + unknown + ' to be quoted' : '');\n tot.style.display = '';\n }else{\n tot.style.display = 'none';\n }\n cav.style.display = '';\n\n /* the controls on each line */\n box.querySelectorAll('[data-del]').forEach(function(b){\n b.addEventListener('click', function(){ HERA.bag.remove(+b.dataset.del); });\n });\n /* Notes are per bouquet. Saved when the box loses focus rather than\n on every keystroke: saving redraws this list, and redrawing it\n mid-sentence would take the cursor away from whoever is typing. */\n box.querySelectorAll('[data-note]').forEach(function(t){\n t.addEventListener('change', function(){\n var items = HERA.bag.all();\n var i = +t.dataset.note;\n if(!items[i]) return;\n if(String(items[i].special || '') === t.value) return;\n items[i].special = t.value;\n if(HERA.bag.replaceAt) HERA.bag.replaceAt(i, items[i]);\n });\n });\n\n box.querySelectorAll('[data-edit]').forEach(function(b){\n b.addEventListener('click', function(){\n var i = +b.dataset.edit;\n /* Editing a bouquet happens on the Blooms page, where the\n product view lives. If we are not there, send them over\n with the bag open rather than half-editing here. */\n if(typeof window.HERA_editBagItem === 'function'){\n window.HERA_editBagItem(HERA.bag.all()[i], i);\n }else{\n HERA.bag.open();\n }\n });\n });\n box.querySelectorAll('.h-qty[data-q]').forEach(function(w){\n var i = +w.dataset.q, input = w.querySelector('input');\n w.querySelectorAll('[data-step]').forEach(function(b){\n b.addEventListener('click', function(){\n HERA.bag.setQty(i, (+HERA.bag.all()[i].qty || 1) + (+b.dataset.step));\n });\n });\n input.addEventListener('change', function(){\n HERA.bag.setQty(i, parseInt(String(input.value).replace(/[^0-9]/g,''), 10) || 1);\n });\n });\n }\n\n /* ---------- 02 \\u00b7 contact channels --------------------------- */\n /* Apps only. Phone and email are already collected as required\n fields above, so offering them again here was asking the same\n question twice \\u2014 this list is about which app to message on. */\n var ALLCH = (F.channels || []).map(function(c){ return c; });\n\n function chKey(n){ return n.toLowerCase().replace(/[^a-z0-9]+/g,''); }\n\n function drawChannels(){\n $('rv-channels').innerHTML = ALLCH.map(function(c, i){\n var id = 'rv-ch-' + chKey(c.name);\n return '\x3clabel class=\"h-bl-chip\">' +\n '\x3cinput type=\"checkbox\" name=\"rv-ch\" id=\"' + id + '\" value=\"' + esc(c.name) + '\"' +\n (i === 0 ? ' checked' : '') + '>\x3cspan>' + esc(c.name) + '\x3c/span>\x3c/label>';\n }).join('');\n $('rv-channels').querySelectorAll('input').forEach(function(cb){\n cb.addEventListener('change', syncChannels);\n });\n }\n\n function chosenChannels(){\n return [].map.call(form.querySelectorAll('input[name=\"rv-ch\"]:checked'),\n function(i){ return i.value; });\n }\n\n /* A field appears for each channel chosen, and disappears when it is\n unchecked. Anything already typed is kept, so unticking by mistake\n does not lose what they wrote. */\n var chTyped = {};\n function labelFor(n){\n if(/whatsapp/i.test(n)) return ['WhatsApp number', 'tel', '+84 \\u2026'];\n if(/^zalo/i.test(n)) return ['Zalo number', 'tel', '+84 \\u2026'];\n if(/telegram/i.test(n)) return ['Telegram username', 'text', '@yourname'];\n if(/instagram/i.test(n)) return ['Instagram handle', 'text', '@yourname'];\n if(/facebook/i.test(n)) return ['Facebook name or profile', 'text', 'Your profile name'];\n return [n, 'text', ''];\n }\n\n function syncChannels(){\n var picked = chosenChannels(), box = $('rv-ch-fields');\n\n /* remember what is typed before redrawing */";
                            __hb3+="\n box.querySelectorAll('input[data-ch]').forEach(function(i){\n chTyped[i.dataset.ch] = i.value;\n });\n\n box.innerHTML = picked.map(function(n){\n var d = labelFor(n);\n if(!d[1]) return ''; /* phone and email reuse the fields above */\n var k = chKey(n);\n return '\x3clabel class=\"h-bl-field\">\x3cspan>' + esc(d[0]) + '\x3c/span>' +\n '\x3cinput class=\"h-bl-in\" type=\"' + d[1] + '\" data-ch=\"' + k + '\" ' +\n 'id=\"rv-chf-' + k + '\" placeholder=\"' + esc(d[2]) + '\" ' +\n 'value=\"' + esc(chTyped[k] || '') + '\">\x3c/label>';\n }).join('');\n\n /* the preferred list only offers what they actually picked */\n var sel = $('rv-preferred'), was = sel.value;\n sel.innerHTML = picked.length\n ? picked.map(function(n){ return '\x3coption value=\"' + esc(n) + '\">' + esc(n) + '\x3c/option>'; }).join('')\n : '\x3coption value=\"\">Choose a contact method above first\x3c/option>';\n if(picked.indexOf(was) > -1) sel.value = was;\n }\n\n /* ---------- 03 \\u00b7 when --------------------------------------- */\n var MONTHS = ['January','February','March','April','May','June',\n 'July','August','September','October','November','December'];\n var notice = (F.noticeDays == null) ? 1 : +F.noticeDays;\n var earliest = new Date(); earliest.setHours(0,0,0,0);\n earliest.setDate(earliest.getDate() + notice);\n\n var dS = $('rv-d'), mS = $('rv-m'), yS = $('rv-y'),\n hS = $('rv-hh'), nS = $('rv-mm'), echo = $('rv-when-echo');\n\n function opt(v, l, on){ return '\x3coption value=\"' + v + '\"' + (on ? ' selected' : '') + '>' + l + '\x3c/option>'; }\n\n function buildWhen(){\n var y0 = earliest.getFullYear();\n yS.innerHTML = opt(y0, y0, true) + opt(y0 + 1, y0 + 1, false);\n mS.innerHTML = MONTHS.map(function(nm, i){ return opt(i, nm, i === earliest.getMonth()); }).join('');\n var hrs = F.hours || {from:8, to:20}, out = '';\n for(var h = +hrs.from; h <= +hrs.to; h++) out += opt(h, (h < 10 ? '0' : '') + h, h === 10);\n hS.innerHTML = out;\n nS.innerHTML = (F.minutes || [0,15,30,45]).map(function(mn){\n return opt(mn, (mn < 10 ? '0' : '') + mn, mn === 0);\n }).join('');\n fillMonths(); fillDays();\n\n $('rv-windows').innerHTML = (F.windows || ['Specific time','Morning','Afternoon','Evening','Flexible'])\n .map(function(w, i){\n return '\x3clabel class=\"h-bl-chip\">\x3cinput type=\"radio\" name=\"rv-win\" value=\"' + esc(w) + '\"' +\n (i === 1 ? ' checked' : '') + '>\x3cspan>' + esc(w) + '\x3c/span>\x3c/label>';\n }).join('');\n $('rv-windows').querySelectorAll('input').forEach(function(r){\n r.addEventListener('change', syncWindow);\n });\n syncWindow();\n }\n function fillMonths(){\n var y = +yS.value;\n [].forEach.call(mS.options, function(o){\n o.disabled = new Date(y, +o.value + 1, 0) < earliest;\n });\n if(mS.options[mS.selectedIndex] && mS.options[mS.selectedIndex].disabled){\n for(var i = 0; i < mS.options.length; i++){\n if(!mS.options[i].disabled){ mS.selectedIndex = i; break; }\n }\n }\n }\n function fillDays(){\n var y = +yS.value, m = +mS.value, last = new Date(y, m + 1, 0).getDate();\n var keep = +dS.value || earliest.getDate(), out = '';\n for(var d = 1; d <= last; d++){\n var tooSoon = new Date(y, m, d) < earliest;\n out += '\x3coption value=\"' + d + '\"' + (tooSoon ? ' disabled' : '') +\n (d === keep && !tooSoon ? ' selected' : '') + '>' + d + '\x3c/option>';\n }\n dS.innerHTML = out;\n if(dS.selectedIndex < 0 || dS.options[dS.selectedIndex].disabled){\n for(var i = 0; i < dS.options.length; i++){\n if(!dS.options[i].disabled){ dS.selectedIndex = i; break; }\n }\n }\n showWhen();\n }\n function chosenDate(){ return new Date(+yS.value, +mS.value, +dS.value); }\n function isoDate(d){\n return d.getFullYear() + '-' +\n String(d.getMonth() + 1).padStart(2,'0') + '-' +\n String(d.getDate()).padStart(2,'0');\n }\n function niceDate(){\n return chosenDate().toLocaleDateString(undefined, { weekday:'long', day:'numeric', month:'long' });\n }\n function showWhen(){\n echo.textContent = niceDate();\n echo.classList.toggle('is-low', chosenDate() < earliest);\n recap();\n }\n function windowValue(){\n var r = form.querySelector('input[name=\"rv-win\"]:checked');\n return r ? r.value : '';\n }\n function syncWindow(){\n var specific = /specific/i.test(windowValue());\n var fold = $('rv-clock-fold');\n fold.classList.toggle('is-off', !specific);\n fold.setAttribute('aria-hidden', specific ? 'false' : 'true');\n fold.querySelectorAll('select').forEach(function(s){\n if(specific) s.removeAttribute('tabindex'); else s.setAttribute('tabindex','-1');\n });\n recap();\n }\n function timeText(){\n var w = windowValue();\n if(!/specific/i.test(w)) return w;\n return String(hS.value).padStart(2,'0') + ':' + String(nS.value).padStart(2,'0');\n }\n yS.addEventListener('change', function(){ fillMonths(); fillDays(); });\n mS.addEventListener('change', fillDays);\n dS.addEventListener('change', showWhen);\n hS.addEventListener('change', recap);\n nS.addEventListener('change', recap);\n\n /* ---------- 04 \\u00b7 delivery or pick up ------------------------ */\n function isDelivery(){ return $('rv-how-d').checked; }\n /* There is nothing to fold open any more -- the address is settled\n in the reply rather than asked for here -- so this only refreshes\n the summary. */\n function syncHow(){ recap(); }\n $('rv-how-d').addEventListener('change', syncHow);\n $('rv-how-p').addEventListener('change', syncHow);\n\n /* ---------- 05 \\u00b7 who they are for --------------------------- */\n function forSomeoneElse(){ return $('rv-me-n').checked; }\n function syncWho(){\n var on = forSomeoneElse(), fold = $('rv-recip');\n fold.classList.toggle('is-off', !on);\n fold.setAttribute('aria-hidden', on ? 'false' : 'true');\n fold.querySelectorAll('input').forEach(function(e){\n if(on) e.removeAttribute('tabindex'); else e.setAttribute('tabindex','-1');\n });\n recap();\n }\n $('rv-me-y').addEventListener('change', syncWho);\n $('rv-me-n').addEventListener('change', syncWho);\n\n /* ---------- 07 \\u00b7 the recap ---------------------------------- */\n function addressText(){\n return isDelivery() ? 'Delivery \\u2014 address to be confirmed' : 'Picking it up';\n }\n function recipientText(){\n if(!forSomeoneElse()) return (val('rv-name') || 'You') + ' (yourself)';\n var n = val('rv-rname'), p = val('rv-rphone');\n return (n || '\\u2014') + (p ? ' \\u00b7 ' + p : '');\n }\n function contactText(){\n var picked = chosenChannels();\n if(!picked.length) return '\\u2014';\n var pref = val('rv-preferred');\n return picked.map(function(c){ return c === pref ? c + ' (preferred)' : c; }).join(' \\u00b7 ');\n }\n\n function budgetText(){\n var v = amount();\n if(v === null) return '\\u2014';\n var c = currency();\n return c.code === 'VND' ? group(v,'VND') + c.symbol : c.symbol + group(v,c.code);\n }\n function recap(){\n var n = HERA.bag.count();\n var rows = [\n ['Your order', n + (n === 1 ? ' item' : ' items'), 'h-rv-s-you'],\n ['Date & time', niceDate() + ' \\u00b7 ' + (timeText() || '\\u2014'), 'h-rv-s-when'],\n ['Delivery', addressText(), 'h-rv-s-how'],\n ['Recipient', recipientText(), 'h-rv-s-who'],\n ['Contact', contactText(), 'h-rv-s-you'],\n ['Budget', budgetText(), 'h-rv-s-notes']\n ];\n $('h-rv-recap').innerHTML = rows.map(function(r){\n return '\x3cdiv>\x3cdt>' + esc(r[0]) + '\x3c/dt>\x3cdd>' + esc(r[1]) + '\x3c/dd>' +\n '\x3cbutton class=\"h-rv-jump\" type=\"button\" data-go=\"' + r[2] + '\">Edit\x3c/button>\x3c/div>';\n }).join('');\n $('h-rv-recap').querySelectorAll('[data-go]').forEach(function(b){\n b.addEventListener('click', function(){\n var s = document.getElementById(b.dataset.go);\n if(s) s.scrollIntoView({behavior:'smooth', block:'start'});\n });\n });\n }\n /* the recap follows anything typed, without a save button */\n form.addEventListener('input', recap);\n\n /* ---------- validation -------------------------------------- */\n function clearErrs(){\n form.querySelectorAll('.h-bl-err').forEach(function(n){ n.remove(); });\n form.querySelectorAll('[aria-invalid]').forEach(function(n){ n.removeAttribute('aria-invalid'); });\n var eb = $('h-rv-errs');\n eb.classList.remove('is-on');\n eb.innerHTML = ''; /* not just hidden \\u2014 emptied */\n }\n function bad(el, msg, list){\n if(el){\n el.setAttribute('aria-invalid','true');\n var s = document.createElement('span');\n s.className = 'h-bl-err'; s.textContent = msg;\n (el.cl";
                            __hb3+="osest('label') || el.parentNode).appendChild(s);\n }\n list.push({ msg: msg, el: el });\n }\n function looksLikeEmail(v){ return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/.test(v); }\n function looksLikePhone(v){ return (v.replace(/[^0-9]/g,'').length >= 8); }\n\n /* ---------- budget --------------------------------------------\n Same pattern as the Blooms custom-order form: type it however\n feels natural -- 500000, 500.000, \"500k\" -- every non-digit is\n stripped, and what we understood is echoed back underneath. */\n var moneyEl = $('rv-budget'), curSel = $('rv-currency'), budgetEcho = $('rv-budget-echo');\n function currency(){\n var list = F.currencies || [];\n for(var i=0;i\x3clist.length;i++){ if(list[i].code === curSel.value) return list[i]; }\n return list[0] || { code:'VND', symbol:'', min:0 };\n }\n /* A budget can be one figure or a range: \"400.000\", or\n \"350.000 to 750.000\". Every run of digits is picked out\n separately, so the dots people write in Vietnamese amounts do\n not run two numbers together. */\n function amounts(){\n var found = String(moneyEl.value).match(/\\d[\\d.,]*/g) || [];\n var out = [];\n for(var i = 0; i \x3c found.length; i++){\n var n = parseInt(found[i].replace(/[^0-9]/g,''), 10);\n if(!isNaN(n) \x26\x26 n > 0) out.push(n);\n }\n return out.sort(function(x, y){ return x - y; });\n }\n function amount(){\n var a = amounts();\n return a.length ? a[0] : null; /* the lowest, for checking */\n }\n function group(n, code){\n var s = String(n).replace(/\\B(?=(\\d{3})+(?!\\d))/g, code === 'VND' ? '.' : ',');\n return s;\n }\n function money1(n){\n var c = currency();\n return c.code === 'VND' ? group(n,'VND') + c.symbol : c.symbol + group(n, c.code);\n }\n function showMoney(){\n var a = amounts();\n budgetEcho.textContent = !a.length ? ''\n : a.length === 1 ? money1(a[0])\n : money1(a[0]) + ' \\u2013 ' + money1(a[a.length - 1]);\n recap();\n }\n if(moneyEl){\n moneyEl.addEventListener('input', showMoney);\n curSel.addEventListener('change', showMoney);\n }\n\n function check(){\n clearErrs();\n var errs = [];\n\n if(!HERA.bag.all().length) errs.push({ msg:'There is nothing in your ' + BAGNAME.toLowerCase() + ' yet.', el:null });\n\n if(!val('rv-name')) bad($('rv-name'), 'Please tell us your name.', errs);\n /* Email and phone are optional now, so only what was actually\n typed gets checked -- a half-written one is still caught. */\n if(val('rv-email') \x26\x26 !looksLikeEmail(val('rv-email'))) bad($('rv-email'), 'That email does not look complete.', errs);\n if(val('rv-phone') \x26\x26 !looksLikePhone(val('rv-phone'))) bad($('rv-phone'), 'That phone number looks too short.', errs);\n\n var picked = chosenChannels();\n if(!picked.length) bad($('rv-channels'), 'Please choose at least one way to reach you.', errs);\n /* every channel that was chosen needs its detail filled in */\n picked.forEach(function(n){\n var k = chKey(n), f = $('rv-chf-' + k);\n if(f && !f.value.trim()) bad(f, 'Please add your ' + n + ' details, or untick it.', errs);\n });\n if(picked.length && !val('rv-preferred')) bad($('rv-preferred'), 'Please choose a preferred method.', errs);\n\n if(chosenDate() < earliest){\n bad(dS, notice === 1 ? 'We need at least a day\u2019s notice.'\n : 'We need at least ' + notice + ' days\u2019 notice.', errs);\n }\n if(!windowValue()) bad($('rv-windows'), 'Please choose a time.', errs);\n\n /* Who they are for is optional -- a name can be settled in the\n reply, and asking for the recipient's phone before we have even\n spoken was a lot to demand of someone sending a gift. */\n\n /* a range is judged on its lower end -- that is the figure we\n would have to design to */\n var c = currency(), v = amount();\n if(v === null){\n bad(moneyEl, 'Please put a number here so we know what to design around.', errs);\n }else if(v \x3c c.min){\n var minP = c.code === 'VND' ? group(c.min,'VND') + c.symbol : c.symbol + group(c.min,c.code);\n /* \"500k\" and \"2 trieu\" are read as 500 and 2, which then look far\n under the minimum. Saying so is kinder than the bare message. */\n bad(moneyEl, v \x3c 10000\n ? 'Please write the amount in full \\u2014 350000 rather than 350k.'\n : 'Our smallest orders start around ' + minP + '.', errs);\n }\n\n\n return errs;\n }\n\n /* ---------- the complete request ---------------------------- */\n function reference(){\n var d = new Date();\n var y = String(d.getFullYear()).slice(2),\n m = String(d.getMonth() + 1).padStart(2,'0'),\n day = String(d.getDate()).padStart(2,'0');\n var rand = Math.random().toString(36).slice(2,6).toUpperCase();\n return 'HERA-' + y + m + day + '-' + rand;\n }\n\n function buildRequest(){\n var picked = chosenChannels();\n var handles = {};\n picked.forEach(function(n){\n var k = chKey(n), f = $('rv-chf-' + k);\n if(f) handles[n] = f.value.trim();\n });\n var items = HERA.bag.all();\n var sum = 0, unquoted = 0;\n items.forEach(function(it){\n var p = priceOf(it);\n if(p === null) unquoted++; else sum += p * (+it.qty || 1);\n });\n\n return {\n reference: reference(),\n placedAt: new Date().toISOString(),\n customer: {\n name: val('rv-name'),\n email: val('rv-email'),\n phone: val('rv-phone'),\n contactMethods: picked,\n preferredContact: val('rv-preferred'),\n handles: handles\n },\n fulfilment: {\n /* Built from the local year/month/day, NOT toISOString().\n Vietnam is UTC+7, so converting local midnight to UTC rolls\n the date back a day \\u2014 Hera would have been told the 24th\n for an order placed for the 25th. */\n date: isoDate(chosenDate()),\n dateText: niceDate(),\n time: timeText(),\n method: isDelivery() ? 'Delivery' : 'Pick up'\n },\n budget: {\n amount: amount(),\n currency: currency().code,\n text: budgetText()\n },\n delivery: isDelivery() ? { toBeConfirmed: true } : null,\n recipient: forSomeoneElse() ? {\n isCustomer: false,\n name: val('rv-rname'),\n phone: val('rv-rphone'),\n other: val('rv-rother')\n } : { isCustomer: true, name: val('rv-name'), phone: val('rv-phone'), other: '' },\n items: items.map(function(it){\n return {\n productId: it.productId,\n name: it.name,\n size: it.size,\n tone: it.tone,\n wrapStyle: it.wrapStyle,\n wrapColour: it.wrapColour,\n quantity: +it.qty || 1,\n cardMessage: it.card,\n specialRequest: it.special,\n listedPrice: it.price\n };\n }),\n orderNotes: val('rv-onote'),\n estimatedTotal: { amount: sum, currency: 'VND', itemsToQuote: unquoted }\n };\n }\n\n /* ------------------------------------------------------------\n THE ONE PLACE THAT SENDS.\n\n There is no destination yet, and this does not pretend\n otherwise \\u2014 the request is assembled in full and kept on the\n page so it can be checked, but nothing leaves the browser.\n\n When a destination is chosen, this function is the only thing\n that changes.\n ------------------------------------------------------------ */\n function sendRequest(request){\n window.HERA_lastRequest = request;\n try{ console.info('[Hera] Order request assembled \u2014 not sent anywhere yet:', request); }catch(e){}\n return true;\n }\n\n form.addEventListener('submit', function(e){\n e.preventDefault();\n var errs = check();\n if(errs.length){\n var box = $('h-rv-errs');\n box.innerHTML = '\x3cp>' + (errs.length === 1\n ? 'One thing needs your attention:'\n : errs.length + ' things need your attention:') + '\x3c/p>\x3cul>' +\n errs.map(function(x){ return '\x3cli>' + esc(x.msg) + '\x3c/li>'; }).join('') + '\x3c/ul>';\n box.classList.add('is-on');\n box.focus();\n var first = errs.filter(function(x){ return x.el; })[0];\n if(first) first.el.scrollIntoView({behavior:'smooth', block:'center'});\n return;\n }\n\n var request = buildRequest();\n sendRequest(request);\n\n form.style.display = 'none';\n var done = $('h-rv-done');\n /* Name the app they picked, so they know where to look rather\n than watching an inbox we are never going to write to. */\n var pref = val('rv-preferred');\n var where = $('h-rv-where');\n if(where){\n where.textContent = pref\n ? 'Please keep an eye on your ' + pref + ' \\u2014 that\\u2019s where we\\u2019ll message you.'\n : 'Please keep an eye on the app you chose \\u2014 that\\u2019s where we\\u2019ll message you.';\n }\n $('h-rv-ref').textContent = 'Reference ' + request.reference;\n done.classList.add('is-on');\n done.focus();\n window.scrollTo({ top: Math.max(0, done.offsetTop - 80), behavior:'smooth' });\n });\n\n /* ---------- go ---------------------------------------------- */\n drawChannels();\n syncChannels();\n buildWhen();\n syncHow();\n syncWho();\n drawItems();\n showMoney();\n recap();\n HERA.bag.onChange(function(){ drawItems(); recap(); });\n})();\n"; (function(){var s=document.createElement("script");s.textContent=__hb3;document.head.appendChild(s);__hb3=null;})();

                            All workshops

                            Public workshop

                             

                            Photo · this workshop
                            (function(){ var MONTHS = ['January','February','March','April','May','June', 'July','August','September','October','November','December']; var DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; var current = null; function $(id){ return document.getElementById(id); } function esc(s){ return String(s == null ? '' : s) .replace(/&/g,'\x26amp;').replace(/\x3c/g,'\x26lt;') .replace(/>/g,'\x26gt;').replace(/"/g,'\x26quot;'); } function when(w){ var d = new Date(w.date + 'T00:00:00'); return DAYS[d.getDay()] + ' ' + d.getDate() + ' ' + MONTHS[d.getMonth()]; } /* ---- drawing the page ------------------------------------------ */ function draw(w){ current = w; if(!w){ $('h-wk-h').textContent = 'That workshop has finished'; $('h-wk-stand').textContent = 'It may have been and gone, or the date may have changed. ' + 'Everything coming up is on the calendar.'; $('h-wk-facts').innerHTML = ''; $('h-wk-body').innerHTML = ''; $('h-wk-signup').hidden = true; return; } $('h-wk-h').textContent = w.title; $('h-wk-stand').textContent = w.standfirst || ''; /* the notice, if there is one and it still makes sense to show it */ var nt = $('h-wk-notice'); if(w.notice && w.status === 'open'){ nt.textContent = w.notice; nt.hidden = false; } else nt.hidden = true; var d = new Date(w.date + 'T00:00:00'); var place = String(w.place || '').split(/\u00b7|,/); $('h-wk-facts').innerHTML = fact('Date', when(w), d.getFullYear()) + fact('Time', w.time || '', w.length || '') + fact('Where', (place[0] || '').trim(), place.slice(1).join(',').trim()) + fact('Price', w.price || '', w.price ? 'Per person, everything included' : ''); var inc = String(w.included || '').split(';') .map(function(t){ return t.trim(); }).filter(Boolean); $('h-wk-body').innerHTML = block('What we\u2019ll make', w.makes) + (inc.length ? '\x3cdiv class="h-wk-blk">\x3ch3>What\u2019s included\x3c/h3>\x3cul class="h-wk-kit">' + inc.map(function(t){ return '\x3cli>' + esc(t) + '\x3c/li>'; }).join('') + '\x3c/ul>\x3c/div>' : '') + block('Who it\u2019s for', w.whofor); photos(w.images || []); /* how many places the list offers */ var opts = ''; for(var n = 1; n <= (w.maxparty || 1); n++){ opts += '\x3coption value="' + n + '">' + (n === 1 ? 'Just me' : n + ' people') + '\x3c/option>'; } $('h-wk-pl').innerHTML = opts; var open = w.status === 'open'; $('h-wk-form').hidden = !open; $('h-wk-cost').textContent = (w.price || '') + ' per person \u00b7 nothing charged now'; $('h-wk-closed').hidden = open; if(!open){ $('h-wk-su-h').textContent = w.status === 'full' ? 'Fully booked' : 'This workshop is cancelled'; $('h-wk-closed').innerHTML = w.status === 'full' ? '\x3cp>Every place has gone. Tell us and we\u2019ll hold you a place if one ' + 'frees up, and let you know first about the next date.\x3c/p>' + '\x3cp>\x3ca class="h-btn h-btn--ghost" href="#experiences">See the other dates\x3c/a>\x3c/p>' : '\x3cp>We\u2019ve had to cancel this one. Anyone who had reserved a place ' + 'has been emailed.\x3c/p>' + '\x3cp>\x3ca class="h-btn h-btn--ghost" href="#experiences">See the other dates\x3c/a>\x3c/p>'; } if(window.__heraScan) window.__heraScan(); } function fact(label, big, small){ if(!big) return ''; return '\x3cdiv>\x3cdt>' + esc(label) + '\x3c/dt>\x3cdd>' + esc(big) + (small ? '\x3csmall>' + esc(small) + '\x3c/small>' : '') + '\x3c/dd>\x3c/div>'; } function block(head, text){ if(!String(text || '').trim()) return ''; return '\x3cdiv class="h-wk-blk">\x3ch3>' + esc(head) + '\x3c/h3>\x3cp>' + esc(text) + '\x3c/p>\x3c/div>'; } /* one picture, or several that fade from one to the next */ function photos(list){ var fig = $('h-wk-fig'); if(!list.length) return; fig.innerHTML = ''; list.forEach(function(url, i){ var im = document.createElement('img'); im.src = url; im.alt = i === 0 ? (current ? current.title : '') : ''; im.loading = i === 0 ? 'eager' : 'lazy'; im.className = 'h-hero-slide' + (i === 0 ? ' is-on' : ''); fig.appendChild(im); }); if(list.length < 2) return; if(window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; var slides = fig.querySelectorAll('.h-hero-slide'), at = 0; setInterval(function(){ slides[at].classList.remove('is-on'); at = (at + 1) % slides.length; slides[at].classList.add('is-on'); }, 5200); } /* ---- a name for everyone after the first ----------------------- */ function guests(){ var n = +$('h-wk-pl').value, box = $('h-wk-guests'), rows = $('h-wk-guestrows'); if(n < 2){ box.hidden = true; rows.innerHTML = ''; return; } rows.innerHTML = ''; for(var i = 1; i < n; i++){ var d = document.createElement('div'); d.className = 'h-wk-guest'; d.innerHTML = '\x3cb>Guest ' + i + '\x3c/b>\x3cdiv class="h-wk-two">' + '\x3clabel class="h-bl-field">\x3cinput class="h-bl-in h-wk-gf" type="text" placeholder="First name">\x3c/label>' + '\x3clabel class="h-bl-field">\x3cinput class="h-bl-in h-wk-gl" type="text" placeholder="Last name">\x3c/label>' + '\x3c/div>'; rows.appendChild(d); } box.hidden = false; } /* ---- sending it ------------------------------------------------ */ function wire(){ var form = $('h-wk-form'); if(!form || form.getAttribute('data-wired')) return; form.setAttribute('data-wired','1'); $('h-wk-pl').addEventListener('change', guests); form.addEventListener('submit', function(ev){ ev.preventDefault(); var miss = [], first = null; form.querySelectorAll('.is-bad').forEach(function(f){ f.classList.remove('is-bad'); }); function need(el, what){ if(!el || String(el.value || '').trim()) return; var f = el.closest('.h-bl-field'); if(f) f.classList.add('is-bad'); miss.push(what); if(!first) first = el; } need($('h-wk-fn'), 'your first name'); need($('h-wk-ln'), 'your last name'); need($('h-wk-em'), 'your email'); need($('h-wk-hd'), 'a number or username we can reach you on'); var em = $('h-wk-em'), v = String(em.value || '').trim(); if(v && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)){ var f2 = em.closest('.h-bl-field'); if(f2) f2.classList.add('is-bad'); miss.push('a valid email address'); if(!first) first = em; } var gf = form.querySelectorAll('.h-wk-gf'), gl = form.querySelectorAll('.h-wk-gl'); for(var i = 0; i < gf.length; i++){ need(gf[i], 'guest ' + (i + 1) + '\u2019s first name'); need(gl[i], 'guest ' + (i + 1) + '\u2019s last name'); } var err = $('h-wk-err'); if(miss.length){ err.hidden = false; err.textContent = 'We still need ' + (miss.length === 1 ? miss[0] : miss.slice(0, -1).join(', ') + ' and ' + miss[miss.length - 1]) + '.'; if(first && first.focus) first.focus(); return; } err.hidden = true; var names = [$('h-wk-fn').value.trim() + ' ' + $('h-wk-ln').value.trim()]; for(var j = 0; j < gf.length; j++){ names.push(gf[j].value.trim() + ' ' + gl[j].value.trim()); } var each = parseInt(String(current && current.price || '').replace(/[^0-9]/g,''), 10) || 0; var total = String(each * names.length).replace(/\B(?=(\d{3})+(?!\d))/g, '.'); /* A hidden field carries the workshop itself into the email, so the request that reaches Hera says which date it is for. The delivery engine reads every field on the form. */ var stamp = document.createElement('input'); stamp.type = 'hidden'; stamp.name = 'Workshop'; stamp.setAttribute('data-send', '1'); /* the one hidden field that is sent */ stamp.value = (current ? current.title + ' \u2014 ' + when(current) + ', ' + current.time : ''); form.appendChild(stamp); $('h-wk-recap').innerHTML = row('Workshop', stamp.value) + row('Coming', names.join(', ')) + row('Email', $('h-wk-em').value.trim()) + row($('h-wk-ch').value, $('h-wk-hd').value.trim()) + (each ? row('Total', total + '\u0111') : ''); /* hiding the form is how the delivery engine knows it was accepted -- every form on this site says yes the same way */ form.hidden = true; $('h-wk-done').hidden = false; $('h-wk-done').scrollIntoView({ behavior: 'smooth', block: 'center' }); }); } function row(k, v){ return '\x3cdiv>\x3cdt>' + esc(k) + '\x3c/dt>\x3cdd>' + esc(v) + '\x3c/dd>\x3c/div>'; } /* ---- which workshop this is ------------------------------------ */ function start(){ if(!$('h-wk-h')) return false; wire(); if(!(window.HERA && HERA.workshops)){ return false; } HERA.workshops.find(HERA.workshops.wanted(), draw); return true; } var tries = 0; (function again(){ if(start()) return; if(++tries > 40) return; setTimeout(again, 150); })(); /* coming back to this section from the calendar, without a reload */ window.addEventListener('hashchange', function(){ if((location.hash || '').replace(/^#/,'') === 'workshop') start(); }); })();