@font-face{font-family:Palmore;src:url(fonts/PalmoreSemibold.woff2) format('woff2');font-weight:600;font-display:swap}
@font-face{font-family:'Funkis A';src:url(fonts/FunkisA-Regular.woff2) format('woff2');font-weight:400;font-display:swap}
@font-face{font-family:'Funkis A';src:url(fonts/FunkisA-Bold.woff2) format('woff2');font-weight:700;font-display:swap}

:root{
  --red:#D51008; --red-dark:#BE1E2D;   /* Figma "Christmas Dark Red" */
  --red-hero:#C72A25; --green:#268047; --dark-green:#2E4A38;
  --mint:#F3FEBA; --mint-light:#F6FFC5; --lime:#E0FF54; --lime-border:#C6EA28; --lime-hero:#E1FF67;
  --plate:#EAE6E1; --paper:url(assets/texture.webp);
}
*{box-sizing:border-box}
html{overflow-x:clip;scroll-behavior:smooth}
body{max-width:100%}
body{margin:0;background:var(--mint);font-family:'Funkis A',system-ui,sans-serif;color:var(--green);-webkit-font-smoothing:antialiased}
img{display:block;max-width:100%}
a{color:inherit}
.display{font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}

/* paper texture overlay */
/* z-index:-1 puts the grain above the section's background colour but behind every
   child. z-index:1 used to work only because .pane carried z-index:2 — and at
   <=640 .pane becomes display:contents, losing that, so the texture multiplied
   over the photos, tiers and CTAs. -1 does not depend on a child winning a z fight. */
/* isolation makes .paper its own stacking context, which is what lets the -1 pseudo
   land ABOVE this element's background colour instead of dropping behind it. */
.paper{isolation:isolate}
/* The grain tiles at its NATIVE size and never scales. It used to be background-size:
   cover, which stretched a 1200x875 source across the section — on a 2560 screen that
   is a 2x upscale, which is why the texture looked pixelated on big displays.
   texture.webp is now a 1520x1520 mirrored tile, so it wraps with no seam. */
.paper::after{content:"";position:absolute;inset:0;background-image:var(--paper);background-size:auto;background-repeat:repeat;background-position:center;mix-blend-mode:multiply;opacity:.6;pointer-events:none;z-index:-1}

/* ---------- nav (behaviour mirrors grain.com.sg) ---------- */
/* Measured off grain.com.sg at 1440: the bar is 60 tall and its content runs
   x156..x1284 — symmetric 156px margins, i.e. a 1128px content box. 1176 - 48 of
   padding gives exactly that, so switching between the two tabs shows no shift. */
/* Box model copied from grain.com.sg, measured at 1440 and at 2543 (a 2559px window
   less its 15px scrollbar). Two things matter and both are easy to get wrong:
   the container is a CENTRED 1176px box with a 24px gutter -- not full-bleed -- and
   the links space themselves with their own 12px horizontal padding and ZERO flex
   gap. Substituting an equivalent-looking gap:26 drifts ~2px per link and lands the
   right-hand group 12px out, which is exactly what read as "jumpy" between tabs. */
/* Sticky, above everything: z-index 40 clears the sticky hero (1) and the fixed
   popup (30). The entrance animation already fades the nav rather than
   transforming it (see [data-enter-fade]) -- a transform here would make the
   bar its own containing block and kill the pin. */
.nav{background:var(--red);min-height:60px;display:flex;align-items:center;position:sticky;top:0;z-index:40}
.nav-in{width:100%;max-width:1176px;margin:0 auto;padding:0 24px;display:flex;align-items:center;gap:16px}
.nav-logo{width:80px;height:18px;flex:none}
/* the wordmark is an <a>, so it would otherwise inherit the links' 12px gutter
   and shrink the mark to 56px inside its own box. 2 classes outrank `.nav a`. */
.nav .nav-logo{padding:0}
.nav-logo img{width:100%;height:100%;object-fit:contain}
.nav-links{display:flex;align-items:center;gap:0;flex:1}
.nav-left,.nav-right{display:flex;align-items:center;gap:0}
.nav-right{margin-left:auto}
.nav a{color:var(--plate);font-family:inherit;font-size:14px;font-weight:700;text-decoration:none;white-space:nowrap;background:none;border:0;padding:0 12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px}
.nav a:hover{text-decoration:none}
.nav-burger{display:none;margin-left:auto;background:none;border:0;color:var(--plate);cursor:pointer;padding:4px}

/* ---------- buttons ---------- */
.btn{display:inline-flex;align-items:center;justify-content:center;
  background:#E0FF54;border:.5px solid #C6EA28;border-radius:2px;
  padding:.5rem 1.5rem;min-width:96px;max-width:296px;color:#268047;font-family:'Funkis A',system-ui,sans-serif;font-weight:700;font-size:15px;line-height:1.5;
  text-align:center;white-space:nowrap;text-decoration:none;cursor:pointer;
  filter:drop-shadow(1px 1px 5px rgba(187,211,94,.3));transition:filter .15s ease}
.btn:hover{filter:drop-shadow(1px 2px 7px rgba(187,211,94,.55))}
.btn:focus-visible{outline:2px solid var(--green);outline-offset:2px}
.ulink{color:var(--green);font-size:15px;text-decoration:underline;text-underline-offset:3px}

/* ---------- hero ---------- */
.hero{position:relative;height:auto;aspect-ratio:6912/3376;min-height:420px;background:var(--mint);overflow:hidden;isolation:isolate}
.hero-carousel{position:absolute;inset:0;z-index:0}
.hero-slide{position:absolute;left:50%;top:50%;width:100%;height:100%;max-width:none;
  transform:translate(-50%,-50%);object-fit:cover;object-position:center 64%;
  opacity:0;transition:opacity .55s ease}
.hero-slide.is-active{opacity:1}
.hero-copy{position:absolute;top:clamp(32px,9%,99px);left:50%;transform:translateX(-50%);display:flex;flex-direction:column;gap:12px;align-items:center;text-align:center;z-index:2}
.hero-copy .btn{margin-top:12px}
.hero-copy h1{margin:0;font-size:clamp(48px,4.2vw,72px);line-height:.78;color:var(--red-hero);letter-spacing:.01em}
.hero-copy p{margin:0;font-size:clamp(15px,1.1vw,18px);line-height:1.5;color:var(--green)}

.popup{position:fixed;right:24px;bottom:24px;width:400px;height:186px;background:var(--green);border-radius:24px;z-index:30;
  box-shadow:0 12px 34px rgba(30,60,35,.22)}
/* No standalone entry animation here. It used to run `pop .5s .6s both` on load,
   which fired independently of the landing sequence and put the card on screen
   before the fold had finished. The entrance below owns it now (it is simply
   visible straight away if JS never runs). */
@keyframes pop{from{opacity:0;translate:0 14px}to{opacity:1;translate:0 0}}
.popup[hidden]{display:none}
.popup-img{position:absolute;left:12px;top:12px;width:182px;height:160px;border-radius:12px;background:var(--mint-light);overflow:hidden}
.popup-img .mat{position:absolute;left:-69px;top:-130px;width:603px;height:590px;object-fit:cover;transform:rotate(64deg) scaleY(-1)}
.popup-img .cake{position:absolute;left:7.02px;top:26.48px;width:158.95px;height:108.04px;
  display:grid;place-items:center}
.popup-img .cake img{width:145px;height:73px;object-fit:cover;rotate:-15deg;
  box-shadow:2px 1.5px 1.5px rgba(45,49,24,.83)}
/* Centred against the 182x160 image, not pinned to a fixed top: the headline is
   two lines (Figma 13674:75837 `items-center`), and a hardcoded top overflowed
   the card the moment the copy grew a line. */
.popup-txt{position:absolute;left:206px;top:12px;height:160px;width:182px;
  display:flex;flex-direction:column;justify-content:center;gap:12px}
.popup-txt .popup-h{margin:0;font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase;font-size:30px;line-height:.72;color:var(--mint-light)}
.popup-txt p{margin:0;font-size:15px;line-height:1.5;color:var(--mint)}
.popup-txt b{font-weight:700}
.popup-txt s{opacity:.85}
.popup-x{position:absolute;right:12px;top:12px;width:24px;height:24px;display:grid;place-items:center;border:0;background:none;cursor:pointer;border-radius:100px}
.popup-x:hover{background:rgba(255,255,255,.14)}
.popup-x img{width:9px;height:9px}
.popup-x .x-circle{display:none}

/* ---------- marquee (Figma 13525:13769) ---------- */
/* 1728x74 green strip: Palmore caps in mint, each repeat separated by a star
   anise. Measured off the Figma render: text 463.3 wide, 49.7 gap, anise 24.8,
   48.6 gap — 585.4px per repeat. The caps sit 2px below the flex centre. */
.marquee{position:relative;height:74px;margin-top:-1px;overflow:hidden;background:var(--green);display:flex;align-items:center}
.mq-track{display:flex;align-items:center;flex:none;will-change:transform;
  animation:mqScroll var(--mq-dur,30s) linear infinite}
.mq-item{display:flex;align-items:center;flex:none;white-space:nowrap;
  font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase;
  font-size:48px;line-height:1;color:var(--mint)}
.mq-txt{position:relative;top:2px}
.mq-anise{display:block;flex:none;width:24.8px;height:24.8px;margin:0 48.6px 0 49.7px;
  background:url(assets/anise-mint.svg) no-repeat center/100% 100%}
/* translate3d, not the `translate` property — composited on the GPU and safe back to
   older Safari. This HAS to stay a CSS animation: it then ticks on the compositor and
   keeps its frame rate while the main thread is busy, which on this page is exactly
   while you scroll (the closing-section mask and the ornament drift both run layout
   reads per scroll frame). Driving the transform from rAF instead made it stutter.
   No hover pause: drinkjoyrush runs straight through. */
/* The travel is a MEASURED PIXEL distance (--mq-shift, written by the build in
   app.js), not `-50%`. Two reasons. (1) A percentage resolves against the track's
   BORDER-BOX width while the duration is derived from `scrollWidth/2`; whenever
   those two differ the loop lands a few px off its own seam and visibly hitches.
   Driving both off one measured number makes them agree by construction.
   (2) On iOS Safari a percentage transform on a composited flex item whose width
   is content-derived has a long history of resolving against a stale width —
   which shows up as a marquee that simply does not move. A px value cannot.
   The fallback keeps the old behaviour if --mq-shift was never written. */
@keyframes mqScroll{
  from{transform:translate3d(0,0,0)}
  to{transform:translate3d(calc(var(--mq-shift, 50%) * -1),0,0)}
}

/* ---------- pricing sections ---------- */
/* --tab-min is the tallest this section gets across its tabs x both price modes,
   published by lockHeights(). Reserving it here means switching Buffet <-> Mini
   Buffet cannot change the page height, while the CTA stack itself stays free
   (the slack lands at the foot of the section, not between the button and the
   Download link). */
.split{position:relative;display:grid;grid-template-columns:1fr 1fr;min-height:max(900px,var(--tab-min,0px));background:var(--mint);overflow:hidden}
.split .photo{position:relative;overflow:hidden;min-height:420px}
.split .photo img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.split .pane{position:relative;min-width:0;display:flex;align-items:center;justify-content:center;padding:102px 24px;z-index:2}
.split.reverse .photo{order:2}
.split.reverse .pane{order:1}

.pricing{width:100%;min-width:0;max-width:713px;display:flex;flex-direction:column;gap:48px;align-items:center;text-align:center}
.pricing h2{margin:0;font-size:48px;line-height:.78;color:var(--red)}
.pricing .lede{margin:12px 0 0;font-size:15px;line-height:1.5}
/* Tab labels are H3s for document structure (SEO spec), but the heading must not
   introduce a box: display:contents drops it from the flex layout so the button
   stays the flex child and the row renders exactly as before. */
.tabs h3{display:contents;font:inherit;margin:0}
/* The crawler mirror of the closed tabs. .split is a grid and .addons a flex
   column, so an appended child would become a track item -- [hidden] alone is
   not always enough against a descendant display rule. Pin it off. */
.tab-seo,.tab-seo[hidden]{display:none}
.tabs{display:flex;gap:24px;justify-content:center;flex-wrap:wrap;font-family:Palmore,Georgia,serif;font-weight:600;font-size:36px;line-height:1;text-transform:uppercase;color:var(--green)}
.tabs button{font:inherit;font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase;color:inherit;background:none;border:0;padding:0;cursor:pointer;opacity:.5;transition:opacity .15s}
.tabs button:hover{opacity:.8}
.tabs button[aria-selected="true"]{opacity:1;text-decoration:underline;text-decoration-thickness:3px;text-underline-offset:5px;cursor:default}

.tabs button::first-letter{text-transform:uppercase}
.chip{display:inline-flex;align-items:center;gap:6px;height:42px;padding:12px;border:1px solid rgba(38,128,71,.3);border-radius:5px;
  font-family:'Funkis A',system-ui,sans-serif;font-weight:700;font-size:15px;color:var(--green);background:none;cursor:pointer}
.chip:focus-visible{outline:2px solid var(--green);outline-offset:2px}
/* OFF is the base state — a chip renders off unless it says aria-pressed="true".
   The reverse (on by default, off opted into) left every chip without the
   attribute reading as on: the disabled smaller-groups chip, which JS strips
   aria-pressed from, and the static Add-ons chip, which is a span. */
.chip .pill{position:relative;width:30.86px;height:18px;border-radius:100px;background:rgba(38,128,71,.32);flex:none;transition:background .18s ease}
.chip .pill img{position:absolute;left:2.6px;top:2.6px;width:12.86px;height:12.86px;opacity:0;transition:left .18s ease,opacity .12s ease}
/* off state: plain knob, no tick */
.chip .pill::after{content:"";position:absolute;left:2.6px;top:2.6px;width:12.86px;height:12.86px;border-radius:50%;
  background:var(--mint-light);opacity:1;transition:opacity .12s ease;pointer-events:none}
.chip[aria-pressed="true"] .pill{background:var(--green)}
.chip[aria-pressed="true"] .pill img{left:14.69px;opacity:1}
.chip[aria-pressed="true"] .pill::after{opacity:0}

/* a single-tier tab (Gathering Box, Meal Boxes) draws a 350x290 near-square
   frame centred in the 713 column, not a full-width band — Figma 13367:10534
   and 13367:11653. min() so it just fills the column on narrow screens. */
/* `round` scales the 646px-wide top/bottom slice of frame.svg down to fit the
   282px inner width, squashing the rule to a 2px pitch of 1px dots. `repeat`
   keeps the tile at its authored scale — 4px pitch, 2px dots, as in Figma.
   The left/right slice is 222px against a 222px inner height, so `round` is
   already exact there and stays. */
.frame.is-single{width:min(350px,100%);min-height:290px;margin-inline:auto;display:grid;align-content:center;
  border-image-repeat:repeat round;
  /* .frame's 10/16 padding is asymmetric and lifts centred content 3px; the
     square box centres the block exactly, so even it out */
  padding-block:13px}
.frame{position:relative;width:100%;min-width:0;max-width:100%;box-sizing:border-box;
  border:34px solid transparent;
  border-image-source:url(assets/frame.svg);
  border-image-slice:34 fill;
  border-image-width:34px;
  border-image-repeat:round;
  padding:10px 0 16px}
.tier-dots{display:none}
.tiers{display:grid;grid-template-columns:repeat(var(--cols,3),minmax(0,1fr));gap:0;justify-items:center;align-content:center}
.tier{display:flex;flex-direction:column;gap:12px;align-items:center;width:100%;max-width:230px;text-align:center}
.tier .amount{display:flex;flex-direction:column;align-items:center}
.tier .pre{margin:0 0 4px;font-size:15px;font-weight:700;line-height:1.5}
.tier h3{margin:0;font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase;font-size:30px;line-height:1;color:var(--red);opacity:.8}
.tier .price{margin:0;font-size:24px;font-weight:700;line-height:1.5}
.tier .price span{font-size:15px;font-weight:400}
.tier .gst{margin:0;font-size:15px;line-height:1.5}
.tier .meta{margin:0;font-size:14px;line-height:1.5}
.tier .min{margin:0;font-size:14px;font-weight:700;line-height:1.5}
.pricing .actions{display:flex;flex-direction:column;gap:24px;align-items:center}

/* ---------- add-ons ---------- */
.addons{position:relative;background:var(--mint);padding:138px 24px;overflow:hidden}
/* 1116 = the 936 three-card row + its 8px rail padding + a 48px gap and a 36px
   arrow on each side. At 1000 the rail was 902 wide against a 923 row, which is
   why the first and last cards were clipped. */
.addons-in{position:relative;z-index:2;min-width:0;max-width:1116px;margin:0 auto;display:flex;flex-direction:column;gap:48px;align-items:center;text-align:center}
/* Every header + byline pair uses the same construction as the hero: line-height .78
   on the header so the box hugs the caps, then a 12px byline offset. line-height:1
   left ~13px of dead space under the caps, which is what made these pairs read far
   looser than the hero at the same declared gap. */
.addons h2{margin:0;font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase;font-size:48px;line-height:.78;color:var(--red)}
.addons .lede{margin:12px 0 0;font-size:15px;line-height:1.5}
.cards-wrap{position:relative;width:100%;display:flex;align-items:center;gap:48px}
.cards[hidden],.station[hidden]{display:none}   /* [hidden] loses to display:flex without this */
.cards{display:flex;gap:48px;justify-content:center;overflow-x:auto;scroll-behavior:smooth;scrollbar-width:none;padding:4px}
.cards::-webkit-scrollbar{display:none}
/* The card width is NOT free: on desktop the two .arrow buttons are in-flow flex
   items of .cards-wrap, so .cards only gets 1116 - 37 - 37 - 96(gaps) = 946px, and
   3x280 + 2x48 + 8(padding) = 944 already fills it. Widening the card overflows the
   scroller, and justify-content:center then splits the overflow and clips the left
   card. The tile is the headroom: it was 200 inside a 280 card, so it grows to 280.
   Mobile overrides both below. --dish is untouched -- plates keep their relative
   sizes, the set just scales up 1.4x. */
.card{flex:none;width:280px;display:flex;flex-direction:column;gap:24px;align-items:center}
.card .tile{width:280px;height:280px;display:grid;place-items:center}
/* --dish normalises how big each plate READS in the tile. The plates are cropped
   differently — wagyu/paella/diane are edge-to-edge, the Ember platters carry a
   wide transparent margin — so object-fit alone rendered them at wildly different
   sizes. Measured per image from its alpha box; see ADDONS in app.js. */
.card .tile img{width:100%;height:100%;object-fit:contain;scale:var(--dish,1)}
.card h3{margin:0;font-size:15px;font-weight:700;line-height:1.5;color:var(--red)}
.card .serves{margin:0;font-size:14px;line-height:1.5;color:var(--dark-green)}
.card .price{margin:auto 0 0;font-size:15px;line-height:1.5}
  /* margin-top:auto pins every price to the bottom of the stretched card, so the
     row keeps one price baseline even when a title wraps to two lines (Ember's
     dino rib). Must stay in THIS rule — an earlier `margin-top:auto` is undone by
     this shorthand. */
.card .price b{font-weight:700}
.arrow{width:37px;height:37px;padding:0;border:0;background:none;display:grid;place-items:center;cursor:pointer;flex:none}
.arrow-ico{width:37px;height:37px;display:block}
#next .arrow-ico{transform:scaleX(-1)}
.arrow:hover .arrow-ico{opacity:.7}
.dots{display:flex;gap:6px;align-items:center}
/* 48px from the content block to the dots (Figma 13367:11426 — cards bottom 564,
   dots y 612). The add-ons flex column already supplies 6px, so 42 + 6 = 48. */
.addons .dots{margin-top:42px}
.dots i{width:6px;height:6px;border-radius:100px;background:#E68766}
.dots i.on{background:var(--red)}   /* same size — Figma 13367:11469 */
/* desktop runs a larger dot with matching spacing; mobile keeps the 6px pair */
@media(min-width:1201px){
  .dots{gap:9px}
  .dots i{width:9px;height:9px}
}
/* Figma 13367:11473 / 13573:61026 — THREE ROWS at every width: the two order
   buttons stacked, then Download menu. A max-content grid sizes itself to the
   widest button and stretches every row to it, so they stay matched as the copy
   changes per tab. */
.addons .ctas{display:grid;justify-items:stretch;justify-content:center;gap:24px;
  width:max-content;max-width:100%;margin-inline:auto}
/* width:100%, not auto. The grid column is already max-content of the widest button
   and justify-items:stretch should fill it, but Safari sizes a stretched grid item
   to its OWN max-content when the container is max-content — which is how these two
   ended up different lengths. Filling the column explicitly cannot be got wrong. */
.addons .ctas .btn{width:100%;max-width:none;white-space:nowrap}
.addons .ctas .ulink{text-align:center}

/* ---------- FAQ (Figma 13806:800 collapsed / 13806:921 open) ---------- */
/* 900px column centred in the 1728 frame, 48px from the header to the list.
   Every row is rule / 24px / question / 24px / rule; opening slips the answer in
   between on the same 24px rhythm. Those 24s are measured off the CAPS, not the
   line box, so the padding backs the font's own leading out: Palmore at
   line-height:1 leaves 7px above the caps and 9.5px below, Funkis A at 1.5 leaves
   7px above its caps and 7.5px under the baseline. Everything is in em so the
   rhythm survives the responsive clamp on the question size. */
.faq{--faq-pull:7px;--faq-tail:17px;
  /* The Figma frame is a constant 1728x900 with the block floating in its middle —
     232px of air above and below collapsed, 126 when all five are open. Fixed padding
     alone gave a 708px section that hugged the content. min-height reserves the frame
     (52vw = 900/1728, so it tracks the width between 1200 and 1728) and align-content
     centres the block in it; the padding stays as a FLOOR, so if the copy ever grows
     past the frame the section expands instead of clipping. */
  position:relative;background:var(--mint);padding:126px 24px;overflow:hidden;
  min-height:min(900px,52vw);display:grid;align-content:center;
  /* minmax(0,1fr), NOT an auto track. An auto grid column sizes to the item's
     max-content, and .faq-in carries width:900px — so the column became 900px wide
     on a 430px phone and its own max-width:100% then resolved against THAT, not the
     viewport. minmax(0,1fr) pins the track to the container; .faq-in's margin:0 auto
     does the horizontal centring, so no justify-items is needed. */
  grid-template-columns:minmax(0,1fr);
  /* no border here — the add-ons section above ends in a permanent star-anise rail,
     and that IS the divider between the two blocks */
  }
/* NOTE: position:relative alone is NOT enough here — the hero is sticky at z-index:1,
   so every section that scrolls OVER it must be in the hero-pin z-index:1 list below.
   Without that the pinned hero paints straight over this section. */
.faq-in{position:relative;z-index:2;width:900px;max-width:100%;margin:0 auto;
  display:flex;flex-direction:column;gap:48px}
.faq h2{margin:0;font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase;
  font-size:48px;line-height:.78;color:var(--red);text-align:center}
.faq-list{display:flex;flex-direction:column;border-top:1px solid var(--green)}
.faq-item{border-bottom:1px solid var(--green)}
.faq h3{margin:0;font-weight:inherit}
.faq-q{display:flex;width:100%;align-items:center;justify-content:space-between;gap:24px;
  margin:0;padding:.472em 0 .417em;background:none;border:0;cursor:pointer;text-align:left;
  color:var(--green);font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase;
  font-size:36px;line-height:1;-webkit-tap-highlight-color:transparent}
.faq-q:hover .faq-qt{opacity:.75}
.faq-q:focus-visible{outline:2px solid var(--green);outline-offset:4px}
.faq-qt{transition:opacity .15s ease}

/* plus that loses its upright when the row opens — Figma Group 48184.
   Pinned to the FIRST line's cap centre, so a question that wraps on mobile keeps
   the toggle beside its opening line instead of floating to the block's middle.
   .465em = cap top (.194) + half the cap height (.271) below the content-box top. */
.faq-ico{position:relative;flex:none;width:18px;height:18px;align-self:flex-start;margin-top:calc(.465em - 9px);
  transition:transform .38s cubic-bezier(.3,.8,.35,1)}
.faq-ico i{position:absolute;background:var(--green);transform-origin:50% 50%;
  transition:transform .38s cubic-bezier(.3,.8,.35,1),opacity .22s ease}
.faq-ico i:first-child{left:0;top:8px;width:18px;height:2px}
.faq-ico i:last-child{left:8px;top:0;width:2px;height:18px}
/* half a turn, not a quarter: a plus is 4-fold symmetric, so a 90deg spin lands on a
   shape identical to where it started and reads as no movement at all. Through 180
   the upright collapses into the bar it is crossing, so the glyph visibly TURNS into
   the minus rather than cutting to it. */
.faq-q[aria-expanded="true"] .faq-ico{transform:rotate(180deg)}
.faq-q[aria-expanded="true"] .faq-ico i:last-child{opacity:0;transform:scaleY(0)}

/* 0fr -> 1fr so the panel animates without a measured height (same trick as the nav).
   The 7px cap-pull lives on .faq-a, OUTSIDE .faq-a-in's overflow:hidden. As a
   negative margin on the <p> it sat INSIDE the clip box and the first line lost its
   top 7px — invisible on this copy because it only ate the leading, but any accent
   or tall ascender would have been sliced. .faq-a is 0-height when collapsed, so
   applying the pull only in .is-open leaves the closed rhythm untouched. */
.faq-a{display:grid;grid-template-rows:0fr;
  transition:grid-template-rows .34s cubic-bezier(.3,.8,.35,1),margin-top .34s cubic-bezier(.3,.8,.35,1)}
.faq-a-in{overflow:hidden;min-height:0}
.faq-item.is-open .faq-a{grid-template-rows:1fr;margin-top:calc(var(--faq-pull) * -1)}
.faq-a p{margin:0;padding:0 0 var(--faq-tail);font-size:18px;line-height:1.5;color:var(--green)}
@media(prefers-reduced-motion:reduce){
  .faq-a{transition:none}
  .faq-ico,.faq-ico i{transition:none}
}

/* ---------- closing ---------- */
.closing{position:relative;width:100%;aspect-ratio:6912/2400;min-height:420px;display:grid;place-items:center;background:var(--mint-light);overflow:hidden}
/* The scatter barely moves as the section scrolls past — embersmokery's effect, but
   DAMPED rather than hard-pinned. A true viewport pin (background-attachment:fixed)
   would have to cover a 1.76:1 viewport with a 2.88:1 artwork, zooming the band 1.64x
   and losing the scatter. The section's own ratio is 2.88, identical to the artwork,
   so covering the SECTION is 1:1. Oversizing 18% buys enough travel for a slow drift
   at near-natural scale; app.js sets --sy from scroll progress, clamped inside the
   bleed so paper never shows at the edges. */
.orn-layer{position:absolute;inset:0;z-index:0;overflow:hidden;pointer-events:none;background:var(--mint-light)}
/* The stage covers the section exactly, so each piece's left/top/width is a
   percentage of the SECTION. The values are Figma 13367:11544 verbatim, divided by
   that frame's 1728x600 -- the same 2.88 aspect .closing carries, so the pattern
   maps one to one. It is a full-bleed pattern: pieces are MEANT to run off all four
   edges and the section's overflow:hidden is the crop. */
.orn-stage{position:absolute;left:0;top:0;width:100%;height:100%;
  transform:translate3d(0,var(--sy,0px),0)}
/* Paper grain for the closing field. It lives on .orn-LAYER, not .orn-stage: as a
   ::after on the stage it painted AFTER the cut-outs and so sat on top of them.
   isolation + z-index:-1 lands it above the layer's #F6FFC5 ground and below the
   stage, i.e. exactly between the colour and the ornaments. Note the multiply does
   shift the visible field off a flat #F6FFC5 — that is inherent to the blend. */
/* ---------- closing cut-out reveal (woonpact.gent's masked opening) ----------
   The section's ground is Christmas red; the ornament artwork is masked to the badge
   silhouette (Figma 13573:60980) and the mask SCALES with scroll, so the scene opens
   out of a diamond. --cut is driven by app.js, 0% closed to ~260% fully open.
   The copy sits above the mask and is never clipped. */
.closing{background:var(--red-dark)}   /* same ground as the footer it runs into */
.orn-layer{isolation:isolate;
  -webkit-mask-image:url(assets/cutout.svg); mask-image:url(assets/cutout.svg);
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-position:center; mask-position:center;
  -webkit-mask-size:var(--cut,260%) var(--cut,260%); mask-size:var(--cut,260%) var(--cut,260%);
  mask-mode:match-source}
/* no JS, no reduced motion, no mask support -> the artwork simply fills the section */
@media(prefers-reduced-motion:reduce){
  .orn-layer,.closing .in{-webkit-mask-image:none;mask-image:none}
}
.orn-layer::before{content:"";position:absolute;inset:0;background-image:var(--paper);
  background-size:auto;background-repeat:repeat;mix-blend-mode:multiply;opacity:.6;pointer-events:none;z-index:-1}
.orn-flat{display:none}
.orn-i{position:absolute;height:auto;display:block;
  transform:translate3d(var(--tx,0px),var(--ty,0px),0);
  transition:transform .45s cubic-bezier(.22,.61,.36,1)}
/* iOS Safari renders fixed layers in a scrolling section as a blurry over-zoomed
   patch, and there is no cursor to track anyway — so phones get the plain,
   section-anchored composition. */
@media (hover:none),(max-width:1200px){
  .orn-layer{position:absolute}
  /* one flat plate replaces the 16 cut-outs. It is sized to the VIEWPORT and app.js
     keeps it there as the page scrolls, so the image itself never moves — the
     section is a window travelling across it. */
  .orn-stage{display:none}
  /* The section takes the plate's own 1720:2200 ratio, so cover is an exact fit and
     nothing is cropped — and the plate is STATIC, travelling with the copy rather
     than sliding under it. */
  .orn-flat{display:block;position:absolute;inset:0;width:100%;height:100%;
    object-fit:cover;object-position:center}
  .orn-i{transition:none}
}

@media(prefers-reduced-motion:reduce){ .orn-i{transition:none} html{scroll-behavior:auto} }
/* The copy carries the SAME cut-out as the artwork, so the headline is revealed by
   the opening instead of floating on the red ground where red type is unreadable.
   It has to span the section for that: mask-size is a percentage of the element's
   own box, so a 547px-wide box would open its mask at a different rate to the
   full-bleed layer and the two would drift apart. Hence inset:0 here and the
   measure moved onto the children. */
.closing .in{-webkit-mask-image:url(assets/cutout.svg); mask-image:url(assets/cutout.svg);
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-position:center; mask-position:center;
  -webkit-mask-size:var(--cut,260%) var(--cut,260%); mask-size:var(--cut,260%) var(--cut,260%);
  mask-mode:match-source;
  position:absolute;inset:0;z-index:2;display:flex;flex-direction:column;gap:12px;
  align-items:center;justify-content:center;text-align:center;color:var(--red);padding:0 24px}
.closing .in>*{max-width:547px}

/* The cut-out runs on mobile too. It was switched off here as a guess at a scroll
   stutter; the real cause was lockHeights() re-rendering on every URL-bar resize
   (fixed separately), so the mask is back. */
@media(max-width:1200px){
  /* keep the copy clear of the ornaments down the sides. 320, not 300: the longest
     line ("Nothing left to do but sit down and eat.") measures 307px, so a 300 cap
     broke "eat." onto its own line. */
  .closing .in>*{max-width:320px}
}
.closing h2{margin:0;font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase;font-size:48px;line-height:.78}
.closing p{margin:0;font-size:18px;line-height:1.5}
.closing a{color:var(--red)}

/* ---------- responsive ---------- */
/* ============================================================================
   ONE breakpoint. >=1200px gets the desktop design; below it, the mobile design
   — no in-between tablet hybrid. 1200 is where the 1118px desktop container
   stops fitting, so it is the point the desktop layout actually runs out of room.
   The @media(max-width:1400px) block below is fluid TYPE only, not a layout.
   The footer keeps grain.com.sg's own em breakpoints on purpose — it mirrors
   the live site's footer grid.
   Later mobile blocks intentionally override earlier ones (e.g. .split becomes
   display:flex + display:contents panes), so source order matters here.
   ============================================================================ */
@media(max-width:1400px){
  .pricing h2{font-size:clamp(30px,3.4vw,48px)}
  .tabs{font-size:clamp(19px,2.3vw,36px);gap:18px}
  .tier h3{font-size:clamp(21px,2vw,30px)}
  .addons h2,.closing h2,.faq h2{font-size:clamp(30px,3.4vw,48px)}
  .faq-q{font-size:clamp(28px,2.55vw,36px)}
}
@media(max-width:1200px){
  .hero{aspect-ratio:3/2;min-height:400px}
  .hero-copy{top:clamp(28px,7%,64px);width:100%;padding:0 24px}
  /* stacked popup — Figma 13448:10984 */
  .popup{left:auto;right:12px;bottom:12px;transform:none;
    width:206px;height:auto;min-height:0;padding:12px}
  .popup-img{position:relative;left:auto;top:auto;width:182px;height:160px}
  .popup-txt{position:static;width:182px;height:auto;margin:12px 0 0;padding:0;gap:12px;align-items:center;text-align:center}
  .popup-txt .popup-h{font-size:30px;line-height:.72}
  .popup-txt p{font-size:15px}
  .popup-x{top:-7px;right:-7px;width:30px;height:30px;padding:9px;
    background:var(--mint-light);border-radius:100px}
  .popup-x:hover{background:#fff}
  .popup-x .x-wide{display:none}
  .popup-x .x-circle{display:block;width:12px;height:12px}
  
  .split{grid-template-columns:1fr;min-height:var(--tab-min,0px)}
  .split .photo{order:1;height:clamp(260px,42vw,420px);min-height:0}
  .split .pane{order:2;padding:56px 24px 64px}
  .split.reverse .photo{order:1}
  .split.reverse .pane{order:2}
  
}
@media(max-width:1200px){
  /* open menu — Figma 13510:13703. The logo/burger row is its own grid row,
     so it cannot shift when the links expand below it. */
  .nav{min-height:0;align-items:stretch;display:block}
  .nav-in{display:grid;grid-template-columns:1fr auto;align-items:center;gap:0;
    max-width:none;padding:24px;column-gap:24px}
  .nav-logo{grid-column:1;grid-row:1;width:81.43px;height:18px}
  /* -4px pulls the 4px hit-area padding back so the GLYPH lands 24px off the edge,
     per Figma 13367:24227 (x388, right edge 406 in a 430 frame). */
  /* negative margins pull the 4px hit-area padding back out of the layout, so the
     GLYPH lands 24px off the edge (Figma 13367:24227) and the 20px-tall button no
     longer sets the row height — the 18px logo does, giving the 66px bar. */
  .nav-burger{grid-column:2;grid-row:1;display:block;margin:-4px -4px -4px 0;line-height:0}
  .nav-burger img{width:18px;height:12px;display:block}   /* Figma 13367:24228 */

  /* gap:0 — the base .nav-links carries gap:26px, which survives into this grid as a
     ROW gap and kept the collapsed menu 26px tall, pushing the bar to 94px. The open
     state spaces itself with margin-top + .nav-left padding-bottom, not this gap. */
  /* Figma 13510:13703 — ONE list, 47px pitch (23px item + 24px gap), first item 24px
     under the logo row, 24px of breathing room at the bottom.
     This used to be a two-row grid animated 0fr->1fr. With BOTH rows at 1fr the grid
     split its height evenly between .nav-left and .nav-right, which is what opened
     the ~250px hole above "Plan now". One flex column with a max-height transition
     has no such failure mode — the two groups are just part of the same stack. */
  .nav-links{grid-column:1/-1;grid-row:2;display:flex;flex-direction:column;gap:24px;
    width:100%;padding:0;opacity:0;max-height:0;overflow:hidden;
    /* expo-out: moves fast immediately then eases to a long, soft stop — the
       "fast to slow" read. max-height alone feels mechanical, so the links also
       ride a small translate that settles on the same curve. */
    transition:max-height .52s cubic-bezier(.16,1,.3,1),
               opacity .34s cubic-bezier(.16,1,.3,1),
               margin-top .52s cubic-bezier(.16,1,.3,1);
    margin-top:0;pointer-events:none}
  /* no padding-bottom here — .nav-in's own 24px already closes the bar */
  .nav-links.open{max-height:480px;opacity:1;margin-top:24px;pointer-events:auto}
  .nav-left,.nav-right{min-height:0;display:flex;flex-direction:column;
    align-items:flex-start;gap:24px;width:100%;margin:0}
  .nav-links a{font-family:'Funkis A',system-ui,sans-serif;font-weight:700;font-size:16px;line-height:23px;
    color:var(--plate);padding:0;width:auto;
    transform:translateY(-6px);transition:transform .52s cubic-bezier(.16,1,.3,1)}
  .nav-links.open a{transform:none}

  .pricing{gap:32px}
  .frame{border-width:26px;border-image-width:26px;border-image-slice:34 fill}
  .tiers{gap:20px}
  /* ---- add-ons, Figma 13367:24391 (430x932 frame) ----
     Measured off that frame: content starts at y24, the blocks sit 24 apart, the
     card is 280 wide with a 280 tile (x75 => 55 inside the 20px section padding),
     the arrows are 36x36 at 12 from each screen edge with their centres on y413,
     and the dots sit 46 under the card. The arrows page TABS here (stepTab), not
     cards, which is why there are exactly three dots. */
  /* 24px gutters for the whole section, so the CTA block keeps its own width and
     still never comes closer than 24px to the screen edge. The top is 48 to match the
     bottom — at 24 the heading sat tight under the green rule closing the block above. */
  /* every block opens and closes on the same 64px, so the green rules between them
     sit an equal distance from the heading above and the link below */
  .addons{padding:64px 24px 64px}
  .addons-in{gap:24px}
  .faq{padding:64px 24px}
  .faq-in{gap:24px}
  /* 36px like every other mobile header on the page */
  .faq h2{font-size:36px}
  /* Palmore's caps are only ~.54em, so the question needs ~27px to out-weigh a
     16px Funkis answer — below that the two read as the same weight. 1.1 so a
     question that wraps here is not set solid. */
  .faq-q{font-size:27px;gap:16px;line-height:1.1}
  .faq-ico{width:14px;height:14px;margin-top:calc(.465em - 7px)}
  .faq-ico i:first-child{top:6px;width:14px}
  .faq-ico i:last-child{left:6px;height:14px}
  .faq{--faq-pull:6px;--faq-tail:15px}
  .faq-a p{font-size:16px}
  /* Figma 13367:24394 — the title is 342x36, i.e. 36px like the pricing headers */
  .addons h2{font-size:36px}
  /* Figma 13461:119 — ADD-ONS is 73x30 here, so these tabs are 30px, NOT the
     36px the pricing tabs use. Same component, two sizes. */
  .addons .tabs{font-size:30px;gap:24px}
  .cards-wrap{display:block;position:relative}
  /* gap 64: the card sits 55..335 inside the 390 rail, and the right arrow occupies
     362..398 — a 24px gap put the next card at 379, colliding with it. 64 pushes
     the neighbour past the rail entirely, which is what the Figma frame shows. */
  /* Each card is a FULL-WIDTH page of the rail, so no part of the next SKU can bleed
     in — a fixed 280 card plus a gap always left a sliver showing at some widths.
     The tile stays 280 and centres inside its page. */
  .cards{justify-content:flex-start;gap:0;padding:0;margin:0;
    scroll-snap-type:x mandatory}
  /* the rail is pinned to the page in view (fitHeight in app.js), so the pages must
     keep their own height rather than stretching to the tallest of them */
  .cards{align-items:flex-start;overflow-y:hidden;
    transition:height .28s cubic-bezier(.22,.61,.36,1)}
  .card{width:100%;flex:0 0 100%;scroll-snap-align:center}
  /* the live-stations list rides the same rail as the cards, as one more page */
  .card-station{justify-content:center}
  .card-station .station{width:100%;padding:0}
  /* Sit every plate on the BOTTOM of its tile. contain-fitting a wide dish into a
     square tile leaves dead space above and below, and --dish shrinking the first
     three added more — all of it landing between the plate and its title. Bottom
     alignment moves that space up under the toggle, where it reads as breathing
     room rather than a gap, and keeps the titles on a common line across pages. */
  /* Centred, not bottom-anchored. end/bottom left the dish sitting on the tile's
     floor with ~62px of dead space above it, so the card read low in its column. */
  .card .tile{width:280px;height:280px;place-items:center}
  .card .tile img{object-position:center;transform-origin:center}
  /* the price used to carry margin-top:auto so it sank to the bottom of whatever
     height lockHeights() had reserved — that was the big hole under the price. */
  .card .price{margin:0}
  .arrow{position:absolute;top:174px;width:36px;height:36px;z-index:3}
  .arrow .arrow-ico{width:36px;height:36px}
  /* flush with the section's 24px gutter, so the arrows sit exactly 24px off the
     screen edge rather than the -8px overhang they used to carry */
  #prev{left:0}
  #next{right:0}
  .addons .dots{margin-top:46px}
}
@media(max-width:1200px){
  .tabs button{white-space:normal}
  .btn{white-space:normal}
  .tier .meta,.tier .gst,.tier .min{white-space:normal}
  /* mobile hero — 430x655 per Figma 13367:22275 / 13510:11013 / 13510:11038 */
  .hero{aspect-ratio:auto;height:655px;min-height:0;max-height:none}
  .hero-slide{left:0;top:0;width:100%;height:100%;transform:none;object-position:center}
  /* each slide carries its own crop, as framed in the design.
     All three swap to portrait cut-outs here — the desktop 2200x1075 files are a
     different crop and cover-fitting them into these boxes zooms past the
     framing. The cut-outs are transparent, so the paper reads behind them. */
  /* All three mobile files are now 860x1310 — 0.6565 against the 430x655 frame's
     0.6565 — so each one fills exactly. The bespoke crops these used to carry
     (s1 pushed down 21.83%, s3 a 150% landscape stretch) are what was clipping
     the tops; with a matching ratio none of it is needed. */
  .hero-slide.s1,.hero-slide.s2,.hero-slide.s3{left:0;top:0;width:100%;height:100%}
  /* same proportions as desktop, scaled 26/48 */
  /* 36px type with 18px of breathing room top and bottom (height follows the
     content rather than the old fixed 40px band) */
  .marquee{height:auto;padding:18px 0}
  .mq-item{font-size:36px}
  .mq-txt{top:1px}
  .mq-anise{width:13.4px;height:13.4px;margin:0 26.3px 0 26.9px}
  .tiers{grid-template-columns:1fr;gap:28px}
  .frame{padding:30px 16px}
    .tier h3{font-size:22px}
  .tier .price{font-size:22px}
  .tier .pre{font-size:14px}
  .closing{aspect-ratio:1720/2200;height:auto;min-height:0;padding:0}
  .closing h2{font-size:clamp(26px,7vw,40px)}
  .closing p{font-size:16px}

}
@media(prefers-reduced-motion:reduce){
  .popup{animation:none}
  .hero-slide{transition:none}
  .mq-track{animation:none}
}


/* ---------- nav responsive (hamburger, like grain.com.sg) ---------- */
/* An older copy of this breakpoint used to sit here and override the Figma block
   above — it forced padding:0 18px (nav 56px tall, logo at y2) and display:none on
   .nav-links, which also killed the grid-rows open/close animation. Removed. */

/* ---------- footer — mirrors grain.com.sg's grid + breakpoints ---------- */
.footer{background:var(--red-dark);color:var(--plate);position:relative;z-index:10}
.footer a{color:var(--plate);text-decoration:none}
.footer-main{width:100%;max-width:72rem;margin:0 auto;padding:3rem 1.5rem 0;display:flex;flex-wrap:wrap;align-items:flex-start}
.foot-left{box-sizing:border-box;width:16.66667%;padding:0 .5rem}
.foot-left img{width:36px}
.foot-right{box-sizing:border-box;min-width:0;width:83.33333%;padding:0 .5rem}
.foot-lists{display:flex;flex-wrap:wrap;width:100%}
.foot-list{box-sizing:border-box;width:20%;padding-right:1rem;margin-bottom:30px}
.foot-list.mailer{width:20%;padding-right:0}
.footer h4{margin:0 0 .25rem;color:#fff;font-weight:700;font-size:1rem;line-height:1.75rem;text-transform:uppercase}
.footer .mailer-head{font-size:1.125rem;text-transform:none;line-height:1.5;margin-bottom:.75rem}
.footer ul{margin:0 0 1.5rem;padding:0;list-style:none;line-height:1.75rem}
.footer li{padding-top:.5rem}
.footer a,.footer li{font-size:.875rem;line-height:1.25rem}
.mailer-copy{margin:0 0 1.5rem;font-size:.875rem;line-height:1.5}
.mailer form{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.mailer input{flex:1 1 200px;min-width:0;background:#F4EFE7;border:0;border-radius:2.5px;padding:12px;font:inherit;font-size:12px;color:#333}
.mailer input::placeholder{color:#878787}
.mailer .btn{font-size:12px}
.news-note{margin:10px 0 0;font-size:12px;line-height:1.5;color:#fff}
.grain-info{display:flex;align-items:flex-start;gap:1.5rem;margin-top:0;flex-wrap:wrap}
.grain-logos{display:flex;gap:1rem;align-items:center}
.grain-logos img{height:40px;width:auto}
/* Figma 13367:24547 — the company name sits 9px above the address group, and the
   three lines inside it are 3px apart. <br>s gave one flat line-height pitch. */
.grain-addr{font-size:.75rem;line-height:1.5;display:flex;flex-direction:column;gap:3px}
.grain-addr p{margin:0}
.grain-addr .addr-name{margin-bottom:6px}   /* 3 gap + 6 = the 9 Figma sets */

.bottom-row{color:#fff;text-align:center;padding:1rem 0;min-height:80px}
.footer .btn{color:var(--red-dark)}   /* same red as the footer ground */
.footer-divider{max-width:none;margin:1rem 0;height:1px;background-color:rgba(255,255,255,.25)}
.bottom-in{width:100%;max-width:72rem;margin:0 auto;padding:0 2rem;display:flex;align-items:center}
.foot-small{margin-left:16.66667%;line-height:1.85}
.foot-small a{font-size:.75rem;line-height:1rem;display:inline-block}
.foot-small a:not(:last-child){margin-right:1rem}
/* The pair sits immediately after Privacy Policy, not hard right: the live site's
   bottom bar is one left-aligned group of Terms / Privacy Policy / f / instagram. */
.foot-social{margin-left:1rem;line-height:1.85;display:flex;align-items:center}
.foot-social a{display:inline-flex;padding:0 .5rem;color:var(--plate)}

/* tablet: 40.063em–60em — lists reflow 35% / 65%, last floats right */
/* mobile: ≤40em — everything stacks, info block centres */
@media only screen and (max-width:1200px){
  .footer-main{padding:3rem 1.25rem 0;display:block}
  .foot-left,.foot-right{width:100%;padding:0}
  .foot-left{padding-bottom:.5rem}
  .foot-lists{display:block}
  .foot-list,.foot-list.mailer{width:100%;padding-right:0;margin-bottom:1.5rem}
  .footer h4{font-size:.875rem;line-height:1.25rem}
  .grain-info{flex-direction:column;align-items:center;text-align:center;gap:3rem}
  .grain-logos{justify-content:center}
  .mailer form{flex-direction:column;align-items:stretch}
  .mailer input,.mailer .btn{width:100%}
  .bottom-in{flex-direction:column;gap:.5rem;padding:0 1.25rem}
  .foot-small{margin-left:0;text-align:center}
}
/* ≤30em — socials drop to their own centred line, as on grain.com.sg */
@media only screen and (max-width:1200px){
  .foot-social{float:none;margin:1.5rem 0 0;justify-content:center;width:100%}
}

/* ---------- early-bird collapsed badge (Figma 12547:674) ---------- */
.eb-badge{position:fixed;right:24px;bottom:24px;width:103px;height:105px;padding:0;border:0;background:none;cursor:pointer;z-index:20}
.eb-badge[hidden]{display:none}
/* badge.svg carries Figma 13573:60485's effects itself — a 0.5px #F6FFC5 stroke at
   10% and a 0 0 blur-2.5 black-10% shadow — drawn in the ORIGINAL 103x102 coordinate
   space with overflow:visible, so the geometry below is untouched and the text stays
   centred on the shape. */
.eb-shape{position:absolute;left:0;top:3px;width:103px;height:102px}
.eb-ribbon{position:absolute;left:50%;top:-22px;width:60px;height:auto;translate:-50% 0;rotate:-35deg}
.eb-text{position:absolute;left:50%;top:50%;translate:-50% -50%;width:100%;
  font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase;font-size:24px;line-height:.72;
  color:var(--mint-light);text-align:center;white-space:nowrap}
.eb-badge:hover .eb-shape{filter:brightness(1.05)}

/* collapse / expand — animate `scale`+`opacity` so `transform` stays free */
@keyframes ebCollapse{from{scale:1;opacity:1}to{scale:.24;opacity:0}}
@keyframes ebPop{from{scale:.5;opacity:0}to{scale:1;opacity:1}}
/* reopening runs the collapse backwards: the badge shrinks away and the card grows
   out of the same bottom-right corner it folded into */
@keyframes ebExpand{from{scale:.24;opacity:0}to{scale:1;opacity:1}}
@keyframes ebShrink{from{scale:1;opacity:1}to{scale:.5;opacity:0}}
.popup.is-collapsing{transform-origin:bottom right;animation:ebCollapse .42s cubic-bezier(.4,0,.2,1) forwards;pointer-events:none}
.eb-badge.is-popping{animation:ebPop .34s cubic-bezier(.34,1.3,.5,1) backwards}
/* Both of these scale a card holding a large image. Without an explicit layer the
   browser re-rasterises it every frame, which is what made the expand stutter;
   will-change + backface-visibility promote it once and composite thereafter. */
.popup.is-collapsing,.popup.is-expanding{will-change:transform,opacity;backface-visibility:hidden}
.popup.is-expanding{transform-origin:bottom right;animation:ebExpand .42s cubic-bezier(.4,0,.2,1) backwards}
.eb-badge.is-shrinking{animation:ebShrink .22s cubic-bezier(.4,0,.2,1) forwards;pointer-events:none;
  will-change:transform,opacity;backface-visibility:hidden}
@media(max-width:1200px){
  .eb-badge{right:14px;bottom:14px;transform:scale(.88);transform-origin:bottom right}
}
@media(prefers-reduced-motion:reduce){
  .popup.is-collapsing,.popup.is-expanding{animation:none}
  .eb-badge.is-popping,.eb-badge.is-shrinking{animation:none}
}

/* CTA stack — some tabs carry a second CTA (e.g. Order Add-Ons only) */
/* One max-content column with both buttons filling it, so "Order Christmas Buffet"
   and the Add-Ons button below it are the same length. width:100% rather than
   relying on justify-items:stretch — see .addons .ctas for why. */
.cta-stack{display:grid;justify-content:center;justify-items:stretch;gap:12px;
  width:max-content;max-width:100%;margin-inline:auto}
.cta-stack .btn{min-width:240px;width:100%;max-width:none}
.split .photo img{transition:opacity .25s ease}

/* tab swap — photo cross-fade layers */
.split .photo{position:relative}
.split .photo img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.split .photo .photo-b{opacity:0}
.tiers,.cta-stack{transition:none}

/* hero copy flips to mint over the darker second slide (Figma 12509:165446) */

.hero-copy h1,.hero-copy p{transition:color .4s ease}
.hero-copy.is-light h1{color:var(--mint-light)}
.hero-copy.is-light p{color:var(--mint)}
/* No scrim on slide 2: hero2.webp already has its tint baked into the photo, so
   only the copy colour flips (is-light above). */
.chip:disabled{cursor:default}

/* ---------- giving section (Figma 13367:11482) ---------- */
.giving{position:relative;background:var(--green);min-height:388px;display:grid;place-items:start center;grid-template-columns:minmax(0,1fr);
  padding:42px 24px;overflow:hidden;z-index:6}
.giving::before{content:"";position:absolute;inset:0;background-image:var(--paper);background-size:auto;background-repeat:repeat;
  mix-blend-mode:multiply;opacity:.5;pointer-events:none;z-index:1}
.giving-in{position:relative;z-index:2;width:525px;max-width:100%;
  display:flex;flex-direction:column;gap:12px;align-items:center;text-align:center;color:var(--mint-light)}
/* Figma 13636:63972 — 154x144, sitting directly on top of the heading. The -12px
   cancels .giving-in's 12px flex gap so the bow touches the title the way it does in
   the mock; the gap below the title (title -> body) stays at 12. */
.giving-ribbon{display:block;width:154px;height:144px;object-fit:contain;margin-bottom:-12px}
.giving-in h2{margin:0;font-family:Palmore,Georgia,serif;font-weight:600;text-transform:uppercase;
  font-size:48px;line-height:.78;color:var(--mint-light)}
.giving-in p{margin:0;font-size:18px;line-height:1.5}
/* 12px on top of .giving-in's own 12px gap, so the button sits 24px under the
   copy rather than crowding it. [hidden] needs the !important-free override
   because .btn sets display:inline-flex, which outranks the UA [hidden] rule. */
.giving-cta{margin-top:12px}
.giving-cta[hidden]{display:none}
@media(max-width:1200px){
  /* Figma 13367:24430 — 308 tall, content starting 43 from the top, bow at 77x72 */
  .giving{min-height:308px;padding:43px 24px}
  .giving-ribbon{width:77px;height:72px}
  /* Figma 13367:24436 — 264x36, so 36px. clamp(28,7vw,48) pinned it to ~30 at 430,
     the same floor that was shrinking the pricing and add-ons headers. */
  .giving-in h2{font-size:36px}
  .giving-in p{font-size:16px}
}

/* ---------- full-viewport first fold (desktop) ----------
   The nav + hero must add up to exactly one screen, so nothing below shows on
   landing. The hero's 6912/3376 aspect-ratio made its height a function of WIDTH,
   which left 46-188px of the marquee peeking depending on the screen. Keying it to
   the viewport instead makes the fold exact at any size. --nav-h is measured by
   app.js; 56px is the fallback the bar has always been on desktop. */
@media(min-width:1201px){
  /* +1px because .marquee carries margin-top:-1px to kill the seam against the hero:
     without this it is pulled a pixel up INTO the fold and shows as a hairline.
     The svh line is the one that matters on iPad Safari (an iPad Pro landscape is
     1366 and lands in THIS block): `100vh` there is the LARGE viewport — the height
     the page gets once the toolbars have scrolled away — so a `100vh` hero is
     taller than what you can actually see, and everything under it, the marquee
     first, starts below the fold. `svh` is the small viewport, i.e. toolbars
     showing, which is the state the page loads in. `dvh` would track the toolbars
     live and re-lay-out the sticky hero mid-scroll; `svh` holds still. Browsers
     without svh keep the vh line above it. */
  .hero{aspect-ratio:auto;height:calc(100vh - var(--nav-h,56px) + 1px);
    height:calc(100svh - var(--nav-h,56px) + 1px);min-height:420px;max-height:none}
}

/* ---------- hero pin ----------
   The hero sticks to the top of the viewport and the rest of the page scrolls OVER
   it. Everything below the hero is opaque and sits at the same z-index, so later
   sections cover the hero by DOM order as you scroll.
   (html uses overflow-x:clip, not hidden, so sticky still works.) */
/* pinned BELOW the sticky nav, not behind it -- at top:0 the bar would cover the
   hero's first 60px for the whole scroll. nav + hero still sum to one screen:
   the hero is calc(100vh - var(--nav-h) + 1px) tall. */
.hero{position:sticky;top:var(--nav-h,56px);z-index:1}
.marquee,.split,.addons,.faq,.giving,.closing{position:relative;z-index:1}

/* ---------- first-paint entrance ----------
   The above-the-fold furniture eases up in sequence on load. Same contract as the
   scroll reveal below: the hidden start state is scoped to html.js-enter, which JS
   adds at run time, so with JS off (or broken) nothing is ever left invisible.
   --d carries each element's place in the stagger. */
.js-enter [data-enter]{opacity:0;transform:translateY(22px)}
/* the sticky hero and the nav bar fade straight in — a transform on a sticky element
   makes it the containing block for its children, which breaks the pin */
.js-enter [data-enter][data-enter-fade]{transform:none}

.js-enter [data-enter].is-in{opacity:1;transform:none;
  transition:opacity .55s cubic-bezier(.65,.01,.05,.99) var(--d,0s),
             transform .55s cubic-bezier(.65,.01,.05,.99) var(--d,0s)}
@media(prefers-reduced-motion:reduce){
  .js-enter [data-enter],
  .js-enter [data-enter].is-in{opacity:1;transform:none;transition:none}
}

/* ---------- scroll reveal ----------
   Each section's content eases up the first time it scrolls into view, once.
   The hidden start state is scoped to html.js-reveal, which JS adds at run time —
   so if the script fails or is blocked the page still renders fully visible
   rather than as a blank column. */
.js-reveal [data-reveal]{opacity:0;transform:translateY(28px)}
.js-reveal [data-reveal].is-in{opacity:1;transform:none;will-change:auto;
  /* woonpact.gent's easing — a long tail that decelerates very late, which is what
     gives its reveals their unhurried feel (it uses it at 2.8s on the loader). */
  transition:opacity .9s cubic-bezier(.65,.01,.05,.99) var(--d,0s),
             transform .9s cubic-bezier(.65,.01,.05,.99) var(--d,0s)}
@media(prefers-reduced-motion:reduce){
  .js-reveal [data-reveal],
  .js-reveal [data-reveal].is-in{opacity:1;transform:none;transition:none}
}

/* ---------- star-anise rail — snows down, then crops flat on the section edge ----------
   In the source 26x25 glyph the lobe row spans y 9.511-11.409 and a single stray centre
   dot (cx 12.64) spans y 11.131-13.021 — they OVERLAP by 0.278px, so no crop keeps the
   lobes whole while losing the dot. We cut at the dot's top, i.e. show 11.131/25 =
   0.44524 of the glyph; .split's / .addons' overflow:hidden does the cropping. Costs
   0.278px (at 26px scale) off the lobe diamonds' bottom taper — sub-pixel, invisible.
   Measured via getBBox + a pixel scan. Keep the RATIO if you resize the glyph. */
/* --anise-w is the ONE size knob: height and the visible band derive from it, and
   app.js reads it to set the pitch (--anise-w + 12px gap). Figma's rail draws the
   glyph at 46 wide. --anise-show/--anise-h must stay at 0.44524 — that is the
   crop ratio that lands just above the stray centre dot (see the note below). */
.anise-rail{--anise-w:46.0px;--anise-h:44.23px;--anise-show:19.69px;
  position:absolute;left:0;right:0;bottom:0;height:var(--anise-show);z-index:5;pointer-events:none}
.anise-rail[hidden]{display:none}
/* A divider rail does not snow, so it can clip to its own band: that both crops the
   glyphs (same cut the section's overflow:hidden gives the toggle rails) and stops
   the last flake overhanging the pane into the photo beside it. Never put this on a
   rail that animates — it would hide the flakes for the whole fall. */
.anise-rail.is-divider{overflow:hidden}

/* outer = vertical fall only (linear, so it reads as constant speed) */
.anise-flake{position:absolute;bottom:calc(var(--anise-show) - var(--anise-h));
  width:var(--anise-w);height:var(--anise-h);will-change:translate,opacity}
/* inner = sway + spin, so neither timing curve fights the other */
.anise-glyph{display:block;width:100%;height:100%;background-repeat:no-repeat;background-size:100% 100%;
  will-change:translate,rotate}
.anise-flake.is-red .anise-glyph{background-image:url(assets/anise-red.svg)}
.anise-flake.is-green .anise-glyph{background-image:url(assets/anise-green.svg)}

.anise-rail.is-falling .anise-flake{
  animation:aniseDrop var(--dur,3s) linear var(--d,0s) both}
.anise-rail.is-falling .anise-glyph{
  animation:aniseSway var(--dur,3s) ease-in-out var(--d,0s) both,
            aniseSpin var(--dur,3s) linear var(--d,0s) both}

@keyframes aniseDrop{
  0%  {translate:0 calc(var(--fall,700px) * -1); opacity:0}
  5%  {opacity:1}
  100%{translate:0 0; opacity:1}
}
/* ease-in-out applies per segment, giving a sinusoidal drift rather than hard turns */
@keyframes aniseSway{
  0%  {translate:calc(var(--dx,26px) * -1) 0}
  26% {translate:var(--dx,26px) 0}
  52% {translate:calc(var(--dx,26px) * -.6) 0}
  78% {translate:calc(var(--dx,26px) * .3) 0}
  100%{translate:0 0}
}
@keyframes aniseSpin{
  from{rotate:var(--rot,180deg)}
  to  {rotate:0deg}
}
@media(prefers-reduced-motion:reduce){
  .anise-rail.is-falling .anise-flake,
  .anise-rail.is-falling .anise-glyph{animation:none}
}

/* ---------- tab swipe ----------
   The panel slides out the way you are travelling and the next one comes in from
   the far side, so the direction reads as a swipe. --dir is +1 moving right
   through the tabs, -1 moving left; app.js sets it before adding the class.
   Translate + fade rather than a full-width slide: the card strip is wider than
   its container, so a true slide would need clipping and would fight the
   horizontal scroll the strip relies on at mobile widths. */
.panel{flex:1;min-width:0}
.panel.is-out{animation:panelOut .16s ease both}
.panel.is-in{animation:panelIn .26s cubic-bezier(.22,.61,.36,1) both}
@keyframes panelOut{
  from{opacity:1;transform:translateX(0)}
  to  {opacity:0;transform:translateX(calc(var(--dir,1) * -40px))}
}
@keyframes panelIn{
  from{opacity:0;transform:translateX(calc(var(--dir,1) * 40px))}
  to  {opacity:1;transform:translateX(0)}
}
@media(prefers-reduced-motion:reduce){
  .panel.is-out,.panel.is-in{animation:none}
}

/* ---------- add-ons: signature-tier chip, tooltip, live-station layout ----------
   Figma 13367:11426 (off) / 13527:16780 (on) / 13367:12315 (tooltip). The chip is a
   DIV, not a button: it holds two separate controls (info trigger + toggle), and a
   button inside a button gets re-parented by the HTML parser. */
.chip-sig{gap:6px;padding:12px;position:relative}   /* Figma 13367:11434 */
/* Ember/Live Stations have no signature switch, but the chip must keep its box or
   everything below it jumps up on a tab change. visibility:hidden also drops it
   from the a11y tree and makes its buttons unfocusable, so `hidden` is not needed. */
.chip-sig.is-off{visibility:hidden}
.chip-btn{display:inline-flex;align-items:center;gap:6px;font:inherit;font-family:inherit;
  color:inherit;background:none;border:0;padding:0;cursor:pointer}
.chip-btn[aria-pressed="true"] .pill{background:var(--green)}
.chip-btn[aria-pressed="true"] .pill img{left:14.69px;opacity:1}
.chip-btn[aria-pressed="true"] .pill::after{opacity:0}
.chip-btn:focus-visible{outline:2px solid var(--green);outline-offset:3px;border-radius:3px}

.info-wrap{display:inline-flex;flex:none}   /* static: the tip anchors to .chip-sig */
.info-ico{width:18px;height:18px;padding:0;border:0;background:none;cursor:pointer;display:block}
.info-ico img{width:18px;height:18px;display:block}
.info-ico:focus-visible{outline:2px solid var(--green);outline-offset:3px;border-radius:50%}

/* tooltip sits above the icon, caret pointing down at it */
.tip{position:absolute;bottom:calc(100% + 6px + 7px + 1px);left:21px;transform:translateX(-50%) translateY(4px);
  width:273px;max-width:72vw;padding:12px 16px;border-radius:5px;
  background:var(--green);color:var(--mint-light);
  font-size:13px;line-height:1.45;font-weight:400;text-align:center;
  opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,transform .16s ease,visibility .16s;
  z-index:12}
.tip::after{content:"";position:absolute;top:100%;left:50%;transform:translateX(-50%);
  border:7px solid transparent;border-top-color:var(--green)}
.info-wrap:hover .tip,.info-ico:focus-visible + .tip{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0)}

/* ---------- mobile tooltip, Figma 13642:65146 ----------
   267x64 bubble, 12px padding, a 13x6 caret sitting 34px in from the bubble's LEFT
   edge. Desktop centres the bubble on the icon (translateX(-50%)); at 430 that put
   its left edge at -14 and clipped it off screen. Offsetting by the caret's own
   position instead anchors the bubble at iconCentre - 40.5 (= 81.5 here, which is
   Figma's x=82) while the caret still points straight at the icon. */
@media(max-width:1200px){
  .tip{width:267px;max-width:calc(100vw - 24px);padding:12px;
    transform:translateX(-40.5px) translateY(4px)}
  .info-wrap:hover .tip,
  .info-ico:focus + .tip,
  .info-ico:focus-visible + .tip{transform:translateX(-40.5px) translateY(0)}
  /* caret centred on 40.5px from the bubble's left edge, 13x6 per the Figma vector */
  .tip::after{left:40.5px;border-width:6px 6.5px 0;border-top-color:var(--green)}
  /* a phone has no hover, so focus has to open it too */
  .info-ico:focus + .tip{opacity:1;visibility:visible}
}

/* left:21px = chip padding 12 + half the 18px icon, so the caret points at the icon
   while the 6px gap is measured off the chip's top border. Re-derive this if the
   chip padding ever changes, or the caret drifts off the icon. */

/* live stations — feature photo beside a priced list (Figma 13367:14597) */
.station{display:flex;gap:48px;align-items:center;justify-content:center;width:100%;padding:4px}
.station-img{flex:none;width:304px}
.station-img img{width:100%;height:auto;border-radius:10px;display:block}
.station-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:48px;
  text-align:left;min-width:0}
.station-list h3{margin:0;font-size:15px;line-height:1;font-weight:700;color:var(--red)}
.station-list .price{margin:8px 0 0}
/* /guest sits tight against the bold figure and stays regular weight */
.station-list .price .unit{font-weight:400}

@media(max-width:1200px){
  /* Figma 13372:30977 — 48px between the station photo and the first dish name */
  .station{flex-direction:column;gap:48px}
  .station-list{text-align:center;align-items:center;gap:18px}
}

/* The 1px green section rules that used to close these blocks off are GONE — the
   star-anise rail is permanent now and is the divider itself, at every width. See
   the rail block above. (Removed: `.split.is-ruled`, and the mobile-only
   `#feasts` rule with its `.is-snowing` transparent override, which existed purely
   to get out of the rail's way when early bird was on.) */

/* desktop: the wrappers are transparent, so the pricing column is unchanged */
.pricing-top,.pricing-bottom{display:contents}

/* ---------- mobile pricing — Figma 13367:22602 / 13367:25157 ---------- */
@media(max-width:1200px){
  /* flatten the section so the photo can sit between the tabs and the toggle */
  .split{display:flex;flex-direction:column;min-height:var(--tab-min,0px);gap:0}
  .split .pane,.split .pricing{display:contents}
  .pricing-top{display:flex;flex-direction:column;gap:18px;align-items:center;text-align:center;
    order:1;padding:64px 24px 0}
  /* The band is the Figma mobile frame's 430x250 photo, so it holds that RATIO at
     every width below 1200 instead of a fixed 250px height. A fixed height turned
     into a 4.8:1 letterbox strip on a tablet (1200x250) while the phone got 1.72:1
     — same layout, two different crops. aspect-ratio keeps the tablet on the
     phone's proportions, the way .closing already scales. height:auto is needed to
     beat the clamp() in the earlier mobile block; flex:none stops the column flex
     container shrinking it back. */
  .split .photo{order:2;position:relative;width:100%;aspect-ratio:430/250;height:auto;
    min-height:0;flex:none;margin-top:24px}
  .split.reverse .photo{order:2}
  .pricing-bottom{display:flex;flex-direction:column;gap:24px;align-items:center;
    order:3;padding:24px 12px 0;width:100%;box-sizing:border-box}
  .actions{order:4;padding:32px 24px 64px}
  .anise-rail{order:5}

  .pricing-top .lede{margin-top:12px !important}
  /* Matched to the add-ons tag row (.addons .tabs, 30px) so every tab row on a phone
     is one size. Figma's pricing frame draws these at 36, but next to the add-ons
     tags at 30 the two rows read as different components. */
  .tabs{font-size:30px;gap:24px}
  /* Figma 13367:22612 — "For bigger groups" is 217 wide; 30px rendered 181. */
  .pricing h2{font-size:36px}

  /* three tiers stay side by side, at mobile type sizes */
  .tiers{grid-template-columns:repeat(var(--cols,3),minmax(0,1fr));gap:12px}
  .tier{gap:6px;max-width:none}
  /* Figma 13572:57721 / 59103 / 13563:54183 / 54654 — the card grew to 257.4x200
     with 22.5 anise bands; the type scaled with it (CLASSIC 73x30, price box 36 tall,
     the 22-tall supporting lines). Everything below is read off those boxes. */
  .tier h3{font-size:30px}
  .tier .price{font-size:24px}
  .tier .pre{font-size:15px}
  .tier .price span{font-size:15px}
  .tier .gst,.tier .meta,.tier .min{font-size:15px}
  .tier .amount{gap:0}
  /* Figma 13367:23692 (Gathering Box) / 13367:24597 (Meal Boxes) — a lone tier draws
     the SAME 206x160 card as a deck tier, just centred with nothing to swipe to. So
     mobile has one card spec, and .frame stops drawing a border entirely: it is only
     the rail. The 350x290 / 246x200 boxes above are desktop-only. */
  .frame,.frame.is-single{border:0;border-image:none;padding:0;min-height:0;
    width:100%;max-width:none;overflow:visible}

    /* position dots for the deck — same 6px treatment as the add-ons carousel */
  .tier-dots{display:flex;gap:6px;align-items:center;justify-content:center}
  .tier-dots[hidden]{display:none}
  .tier-dots i{width:6px;height:6px;border-radius:100px;background:#E68766;transition:background .18s ease}
  .tier-dots i.on{background:var(--red)}

/* ---- tier deck, Figma 13563:48029 ----
     Each tier carries its OWN dotted frame (205.9 x 160, 24 apart) and the row
     scrolls horizontally instead of squeezing three columns into 430px. The
     border-image moves off .frame and onto .tier. With a single tier there is
     nothing to scroll — the rail's 112px padding just centres it. */
  .frame .tiers{display:flex;grid-template-columns:none;gap:24px;
    align-content:stretch;justify-items:stretch;
    overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;
    -webkit-overflow-scrolling:touch;scrollbar-width:none;
    /* The rail spans the full viewport: -12px cancels .pricing-bottom's 12px padding.
       The inline padding is half the leftover width, so a card snapped to centre
       lands dead centre of the screen — including the first and last, which would
       otherwise have nothing to scroll against. */
    /* % padding resolves against the PARENT (406px), but the rail is 430 wide after
       the -12px margins — hence the +24px, or the first and last card land 12px
       shy of centre. (406 + 24 - 206) / 2 = 112. */
    margin-inline:-12px;padding-inline:calc((100% + 24px - 257px) / 2)}
  .frame .tiers::-webkit-scrollbar{display:none}
  .frame .tier{flex:0 0 257px;width:257px;max-width:none;min-height:200px;
    scroll-snap-align:center;box-sizing:border-box;
    border:22px solid transparent;
    border-image-source:url(assets/frame.svg);
    border-image-slice:34 fill;
    border-image-width:22px;
    border-image-repeat:round;
    /* Figma stacks the blocks 6px apart inside the card (CLASSIC y0, amount y30,
       courses/min y77) — 12px overflowed the 160px box to 173. */
    justify-content:center;gap:6px}
  /* Figma 13573:61026 — the two order buttons are EQUAL width, stacked, with
     Download menu under them. Same max-content grid as the add-ons CTAs: the
     container sizes to the wider button and stretches both to it. nowrap keeps
     "Order Christmas Buffet" on one line against the blanket .btn rule above. */
  .cta-stack{display:grid;justify-items:stretch;justify-content:center;
    width:max-content;max-width:100%;margin-inline:auto}
  .cta-stack .btn{min-width:222px;width:100%;max-width:none;white-space:nowrap}
}

/* ---------- tablet hero — the LANDSCAPE plate, not the phone cut-out ----------
   Below 1200 the hero was a flat `height:655px` showing the 860x1310 portrait
   cut-outs. Those are authored to fill a 430x655 frame EXACTLY (0.6565 both), so
   on a phone they are pixel-perfect — and on a tablet they fail twice over:

   | viewport | box ratio | source upscale @2x |
   |---|---|---|
   | 430  | 0.66 | 1.00 — exact |
   | 834  | 1.27 | 1.94x |
   | 1024 | 1.56 | 2.38x |
   | 1200 | 1.83 | **2.79x** |

   1. COVER on a box that is far wider than the source centres a tall image and
      eats 615px of it at 834 — the food that the design keeps at the bottom rides
      up THROUGH the copy, which is the overlap on an iPad.
   2. A 860px-wide source stretched over 1200 CSS px at DPR 2 is 2400 device px:
      a 2.79x upscale, hence the soft hero.

   Both are the same mistake — a phone asset asked to do a tablet's job. So from
   720 up the hero takes the DESKTOP treatment entirely: the landscape plates
   (the `<picture>` portrait source is now capped at 719.98px) at the desktop's
   own full-viewport height.

   The plates were RE-EXPORTED at 3456x1688 from the 6912x3376 originals for
   this (they were 2200x1075). Filling a portrait tablet means cover-cropping a
   2.047 image down to ~0.74, so only ~36% of the plate's width is on screen and
   the effective render is far wider than the box — 2312 CSS px at 834. The old
   2200px file could not pay for that; 3456 can:

   | viewport | hero | source scale @DPR2 |
   |---|---|---|
   | 744 (iPad Mini) | 744x1068 | 1.27x |
   | 834 (iPad Air)  | 834x1129 | 1.34x |
   | 1024 landscape  | 1024x703 | 0.83x |
   | 1200            | 1200x735 | 0.87x |
   | 1440 DESKTOP    | 1440x841 | **1.00x** — was 1.56x |

   Desktop got sharper for free. The ceiling is iPad Pro 12.9 portrait
   (1024x1301) at 1.54x, which is still better than what desktop shipped with.

   There is no high-res PORTRAIT source to go back to — the 860x1310 files are a
   separate re-composition of the same shoot (the food is rearranged, not
   cropped), so they cannot be re-exported larger. That is why the tablet takes
   the landscape plate rather than a bigger cut-out. */
@media(min-width:720px) and (max-width:1200px){
  /* FULL SCREEN, same expression as the desktop rule — nav + hero sum to exactly
     one viewport at every width now, not just above 1200. The +1px is the
     marquee's margin-top:-1px (it kills the seam against the hero; without the
     +1 the fold lands a pixel inside the hero and shows as a hairline). */
  .hero{aspect-ratio:auto;height:calc(100vh - var(--nav-h,56px) + 1px);
    height:calc(100svh - var(--nav-h,56px) + 1px);min-height:420px;max-height:none}
  /* restore the desktop slide transform the mobile block flattened. The 64%
     vertical anchor is what keeps the table, not the ceiling, in frame. */
  .hero-slide{left:50%;top:50%;transform:translate(-50%,-50%);object-position:center 64%}
  /* .hero-slide.s1/.s2/.s3 above is (0,2,0) and pins left/top to 0 — it has to be
     beaten at equal specificity or the slides sit a half-frame off. */
  .hero-slide.s1,.hero-slide.s2,.hero-slide.s3{left:50%;top:50%}
}

/* ---------- tablet pricing — all tiers at once, no swipe ----------
   The block above is a one-card-at-a-time swipe deck, which is right on a phone
   (3 x 257 + 2 x 24 = 819px cannot fit in 430) but wrong from 768 up, where the
   whole deck fits on screen and asking the reader to scroll sideways HIDES two
   of the three prices. Everything else stays exactly as the mobile layout has
   it — centred tabs, the same cards, stacked CTAs.

   One grid, no breakpoint arithmetic: `minmax(0,257px)` caps each column at the
   authored card width and lets it shrink only when the row would not otherwise
   fit (232px at 768). That matters because .tier's frame is a FIXED-SIZE
   border-image — growing the card past 257 rescales the slice; shrinking it is
   what `border-image-repeat:round` already handles.

   Phones are untouched: below 720 the swipe deck above still applies, dots and
   all. 720 is the tablet line because the SMALLEST iPad is 744 CSS px wide in
   portrait (Mini) while the widest phone in portrait is ~450 — a line at 768
   would have left the Mini on the phone deck. A phone in LANDSCAPE (844 on an
   iPhone 14) also clears it, which is right: plenty of width, very little
   height, exactly the case the deck was protecting against. */
@media(min-width:720px) and (max-width:1200px){
  .frame .tiers{display:grid;
    grid-template-columns:repeat(var(--cols,3),minmax(0,257px));
    justify-content:center;align-items:stretch;gap:24px;
    /* undo the deck: no sideways scroll, no snap, and drop the negative margin +
       centring padding that existed only to park a snapped card mid-screen */
    overflow:visible;scroll-snap-type:none;margin-inline:0;padding-inline:0}
  .frame .tier{flex:none;width:auto;scroll-snap-align:none}
  /* the dots counted cards in the deck; with every card on screen they count
     nothing. JS still sets .tier-dots[hidden] for the 1-tier tabs — this just
     takes the 2+ case out too, and the reveal pass skips what does not render. */
  .tier-dots{display:none}
}

/* ---------- mobile footer — Figma 13367:24510 (430 frame) ----------
   One centred column on the dark-red ground: logo, the three link lists, the
   mailer, then the address with the halal/ISO marks under it. The desktop
   footer's 5-column grid and the old 40em/30em stacking are gone; this is the
   only sub-1200 footer layout. */
@media(max-width:1200px){
  /* Type, from 13367:24512: section headings Funkis A Bold 18 in #FFF, every other
     line Regular 15 in #EAE6E1. The 382px content column in a 430 frame puts the
     gutters at 24, not the 45 the safe-margin guide suggested. */
  .footer-main{padding:48px 24px 0;display:block;text-align:center}
  .foot-left{width:100%;padding:0 0 20px}
  .foot-left a{display:inline-block}
  .foot-left img{width:auto;height:50px}
  .foot-right{width:100%;padding:0}
  .foot-lists{display:block}
  .foot-list,.foot-list.mailer{width:100%;padding-right:0;margin:0 0 33px}
  /* Sized off grain.com.sg's own mobile footer: 14px throughout, every line on a
     36px pitch (20px line box + 16px lead), and 52px between one list and the next. */
  .footer h4{font-size:14px;line-height:20px;margin:0}
  .footer ul{margin:0;line-height:normal}
  .footer li{padding-top:16px}
  .footer li a{font-size:14px;line-height:20px}
  .footer .mailer-head{font-size:18px;line-height:normal;margin:0 0 12px}
  .mailer-copy{margin:0 0 24px;font-size:15px;line-height:1.5}
  /* the form stays a row — 232 + 12 + button, filling the 382 content column */
  .mailer form{flex-direction:row;align-items:stretch;justify-content:center;gap:12px;flex-wrap:nowrap}
  .mailer input{flex:1 1 232px;width:auto;min-width:0;font-size:15px}
  .mailer .btn{flex:none;width:auto;min-width:0;padding:12px 18px;font-size:15px}
  /* markup runs logos-then-address; Figma puts the address first */
  .grain-info{flex-direction:column;align-items:center;text-align:center;gap:48px;margin-top:54px}
  .grain-logos{justify-content:center;gap:25px;align-items:center}
  .grain-logos img:first-child{height:40.3px}   /* halal 43.4 x 40.3 */
  .grain-logos img+img{height:38px}             /* iso   40.3 x 38   */
  .grain-addr{font-size:15px;line-height:1.5}
  /* Figma 13367:24582 — the rule runs full bleed, then 48px of air before the legal
     links, 24 between them and the social pair, and 48 under it to close the page. */
  .footer-divider{max-width:none;margin:0}
  .bottom-row{padding:0 0 48px;min-height:0}
  .bottom-in{flex-direction:column;gap:24px;padding:48px 24px 0}
  .foot-small{margin-left:0;line-height:normal;display:flex;flex-wrap:wrap;gap:12px;justify-content:center}
  .foot-small a{font-size:15px;line-height:normal;margin:0}
  /* 61.55 x 24 for the pair in Figma — 24px glyphs, 13.5 between */
  .foot-social{float:none;margin:0;justify-content:center;width:100%;gap:13.5px}
  .foot-social a{padding:0}
  .foot-social svg{width:24px;height:24px}
}

/* ---------- mobile paper grain ----------
   The tile is 1520px of real pixels. On a phone you only ever see ~430 of them, so
   the grain reads several times coarser than it does on a desktop screen — the
   texture became the loudest thing on the page. Scaling the tile DOWN (never up)
   puts the grain back at roughly the density desktop shows, and because it is a
   mirrored tile it still wraps seamlessly at any size.
   Placed at the end of the file on purpose: media queries add no specificity, so
   these have to come after the three base .paper / .orn-layer / .giving rules. */
@media(max-width:1200px){
  .paper::after,
  .orn-layer::before,
  .giving::before{background-size:460px 460px}
}
