/* ===========================================================================
   Balloon Adventure Party Decor — Las Vegas
   Palette is Gali's own, sent 2026-08-03:
     beige #EADDD1 · dusty blue #83A4B6 · pink #F5E4F9 · purple #9D7F98 · black
   Everything else here is a tint or shade of those five. Nothing invented.
   =========================================================================== */

/* ---------------------------------------------------------------------------
   FONTS ARE SELF-HOSTED. 2026-08-14. Do not put the fonts.googleapis.com link
   back — it 404'd on Fraunces three separate times (08-12, 08-13, 08-14) and
   the fallback serif shipped to whoever was unlucky. Google's css2 endpoint
   hands a DIFFERENT file to different clients for the same URL, so the failure
   was never reproducible on demand and never fixable by retrying.

   These two are variable fonts: one file each covers every weight, and opsz is left
   as a range so the browser still optically sizes headings.
   ⚠ SOFT and WONK are NOT in these files — verified in the fvar table, not assumed.
   style.css used to ask for 'SOFT' 40,'WONK' 1 and no file ever shipped carried them,
   so that line was a no-op and deleting it changed zero pixels. The type Gali approved
   on 08-10 is the type without them. Turning them on is a real design change to every
   heading on the site, and it costs another 53,244 bytes on the latin file alone —
   so it is Julian's call to make deliberately, not a thing to switch on in passing.

   ⚠ THE FILES ARE NOT "PINNED" TO 400..700 / 400..500, whatever the refresh recipe
   below looks like — an earlier version of this comment said they were and it was
   wrong. Both fvar tables carry the full wght 100..900. The `font-weight:400 700`
   descriptor on each @font-face is what clamps the range; the bytes do not.
   That matters for one reason: the immutable cache argument rests on THESE EXACT
   BYTES, not on "some subset covering these weights". A re-subset file is a different
   file even at the same weights, and it must ship under a new name.

   ⚠ THE NAMES CARRY A CONTENT HASH and that is load-bearing, not decoration.
   vercel.json serves this directory `immutable` for a year, which tells a browser it
   need never ask again — safe only because changing a byte changes the URL. Never
   hand-edit these filenames or hand-write a hash: run `node tools/hash-fonts.mjs`,
   which renames the files and rewrites every reference here and in the head partial.
   `node tools/build-pages.mjs --check` fails if a name and its bytes ever disagree.

   Both are SIL Open Font License 1.1, which permits self-hosting and requires the
   licence to travel with the files — so assets/fonts/ ships OFL-Fraunces.txt and
   OFL-Jost.txt next to them. Loading from Google's CDN did not need that; hosting
   them ourselves does. Do not delete those two files as clutter.

   Refresh recipe (family query -> latin + latin-ext woff2 per family). The shipped
   bytes were checked byte-for-byte against what this URL serves, so it reproduces
   them — but re-run hash-fonts.mjs after, because a refresh may not:
     Fraunces:opsz,wght@9..144,400..700   Jost:wght@400..500
   --------------------------------------------------------------------------- */
@font-face{
  font-family:'Fraunces'; font-style:normal; font-weight:400 700; font-display:swap;
  src:url(/assets/fonts/fraunces-latin.48282a41.woff2) format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Fraunces'; font-style:normal; font-weight:400 700; font-display:swap;
  src:url(/assets/fonts/fraunces-latin-ext.f120089b.woff2) format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:'Jost'; font-style:normal; font-weight:400 500; font-display:swap;
  src:url(/assets/fonts/jost-latin.235d8f89.woff2) format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Jost'; font-style:normal; font-weight:400 500; font-display:swap;
  src:url(/assets/fonts/jost-latin-ext.b2a539cd.woff2) format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

:root{
  --beige:#EADDD1;
  --cream:#F6EFE8;
  --paper:#FCFAF8;
  --blue:#83A4B6;
  --blue-deep:#5E8598;
  --blue-pale:#BFD4E0;  /* her blue lifted toward white, for small type on a photo.
                           #83A4B6 at 12px was the single tightest piece of text on
                           the site (4.83:1) and it was what stopped the booking
                           photograph from being allowed to look like a photograph. */
  --pink:#F5E4F9;
  --pink-deep:#E7CDEE;
  --plum:#9D7F98;
  --plum-deep:#6E5569;
  --ink:#17141A;
  --ink-soft:#4A424C;
  --muted:#6B616A;   /* darkened from #7A7078: the old value put the footer
                        legal line at 4.17:1 on cream, under the 4.5 floor */
  --line:rgba(23,20,26,.14);

  --wrap:1180px;
  --r:18px;
  --r-lg:26px;
  --shadow:0 18px 44px -22px rgba(45,32,48,.42);
  --shadow-lift:0 30px 70px -28px rgba(45,32,48,.55);
  --ease:cubic-bezier(.2,.7,.3,1);
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%; scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

body{
  margin:0;
  background:var(--paper);
  color:var(--ink);
  font:400 17px/1.62 'Jost',system-ui,-apple-system,'Segoe UI',sans-serif;
  letter-spacing:.002em;
  overflow-x:hidden;
  /* Sticky footer. /404 is shorter than a laptop screen, so the footer used to stop
     two thirds of the way down and leave a bare strip of --paper under it — the one
     page a visitor lands on when something has already gone wrong. min-height on the
     body plus flex:1 on <main> pushes the footer to the bottom of short pages and
     changes nothing on long ones. svh, not vh: on a phone vh is the address-bar-
     collapsed height, which overshoots and reintroduces the gap it is meant to close. */
  display:flex; flex-direction:column; min-height:100svh;
}
main{flex:1 0 auto}
/* height:auto matters more than it looks. Every <img> here carries width/height
   attributes so the browser reserves the box and nothing shifts on load — but
   those attributes also set a used height, which beats aspect-ratio unless the
   height is explicitly released. Without this line the 4:5 cards render 1250px
   tall at 373px wide. */
img{max-width:100%; height:auto; display:block}
a{color:inherit}

h1,h2,h3,.display{
  font-family:'Fraunces','Iowan Old Style',Georgia,serif;
  font-weight:700;
  line-height:1.07;
  letter-spacing:-.015em;
  margin:0;
  /* the 'SOFT' 40,'WONK' 1 that used to sit here asked for axes no shipped file
     ever carried — see the @font-face note at the top. It changed nothing. */
}
p{margin:0}

.wrap{width:min(var(--wrap),calc(100% - 40px)); margin-inline:auto}
@media (max-width:640px){ .wrap{width:calc(100% - 32px)} }

.skip{position:absolute;left:-9999px;top:0;background:var(--ink);color:#fff;padding:12px 18px;z-index:99}
.skip:focus{left:8px;top:8px}

:focus-visible{outline:3px solid var(--blue-deep); outline-offset:3px; border-radius:6px}
/* The two contact pills sit on the darkened photograph, where --blue-deep as a ring
   is about 3.5:1 against it — over the 3:1 floor for a non-text indicator and not by
   much. The pale blue is ~9:1 there. The form's own fields are on the white card and
   keep the darker ring, which is the one that reads on cream. */
.book__direct :focus-visible{outline-color:var(--blue-pale)}

/* --- the language flash -------------------------------------------------
   app.js is deferred and the Spanish swap needs the DOM, so a Spanish visitor
   always saw one frame of English. On one page that was a blink at the start of
   the visit; across four pages it is a blink on every click. The inline script in
   <head> sets .lang-pending before the body parses and app.js clears it the moment
   the swap is done.
   ⚠ The animation is the safety net, not decoration. If app.js fails to load, or
   throws before the swap, nothing would ever clear the class — so the body reveals
   itself at 0.8s regardless. A broken script must cost a flash, never a blank page. */
html.lang-pending body{opacity:0; animation:ba-reveal 0s .8s forwards}
@keyframes ba-reveal{to{opacity:1}}
@media (prefers-reduced-motion:reduce){ html.lang-pending body{opacity:1; animation:none} }

/* --- eyebrow / section heads ------------------------------------------- */
.eyebrow{
  font-size:12px; font-weight:500; letter-spacing:.22em; text-transform:uppercase;
  color:var(--plum-deep); margin-bottom:14px;
}
.sec-head{max-width:660px; margin-bottom:38px}
.sec-head h2{font-size:clamp(30px,4.4vw,46px)}
.sec-head p{margin-top:14px; color:var(--ink-soft); font-size:18px}

/* A section head with its "see all" link parked on the right. Only the home page
   uses it: every block there is a teaser for a page that holds the whole thing. */
.sec-head--row{
  max-width:none; display:flex; align-items:flex-end; justify-content:space-between;
  gap:24px 40px; flex-wrap:wrap;
}
.sec-head--row>div{max-width:660px}
.more{
  flex:none; text-decoration:none; color:var(--plum-deep); font-size:15.5px; font-weight:500;
  border-bottom:1.5px solid rgba(110,85,105,.3); padding-bottom:3px;
  transition:color .2s, border-color .2s;
}
.more::after{content:' \2192'}
.more:hover{color:var(--ink); border-color:var(--ink)}
@media (max-width:640px){ .sec-head--row{gap:18px} .more{font-size:15px} }

/* The one-line pointer under a teaser block. Says what the teaser left out, which
   is the only honest reason to ask somebody for a second click. */
.packs__note,.gal__note{
  margin-top:clamp(20px,2.6vw,30px); color:var(--ink-soft); font-size:16px; max-width:70ch;
}
.packs__note a,.gal__note a{color:var(--plum-deep); text-underline-offset:3px}
.packs__note a:hover,.gal__note a:hover{color:var(--ink)}

/* --- interior page head -------------------------------------------------
   Not a hero. The home page sells; these pages have already been chosen, so the
   job here is to name the page and get out of the way of the content. */
.pagehead{position:relative; padding:clamp(34px,5vw,62px) 0 clamp(30px,4vw,48px); overflow:hidden}
.pagehead::before{
  content:''; position:absolute; inset:0;
  background:linear-gradient(168deg,var(--beige) 0%,var(--cream) 62%,var(--paper) 100%);
  z-index:-2;
}
.pagehead::after{
  content:''; position:absolute; right:-16vw; top:-24vw; width:46vw; height:46vw;
  border-radius:50%; background:radial-gradient(circle at 35% 35%,var(--pink),transparent 68%);
  opacity:.7; z-index:-1; pointer-events:none;
}
/* Centred, where the home hero is left-aligned. Left-aligned here left the right
   half of a 1440px screen empty on every interior page — a title block with nothing
   opposite it reads as an unfinished layout, and the honest fixes were both worse:
   a photograph would repeat one that is already further down the same page, and a
   stat panel would need a number nobody has given us. */
.pagehead .wrap{text-align:center}
.pagehead h1{font-size:clamp(32px,4.8vw,52px); max-width:20ch; margin-inline:auto}
.pagehead__sub{margin-top:18px; color:var(--ink-soft); font-size:clamp(16.5px,1.5vw,18.5px); max-width:60ch; margin-inline:auto}
.pagehead .hero__cta{margin-top:26px; justify-content:center}
/* The 404 is the one page with less content than a laptop screen. The sticky footer
   on body pushed the footer down, which moved the bare strip of --paper rather than
   removing it — so the section itself takes the leftover height and centres in it.
   The tinted band now runs from the header to the footer whatever the screen is. */
.pagehead--404{
  padding-bottom:clamp(52px,7vw,90px);
  flex:1 0 auto; display:flex; flex-direction:column; justify-content:center;
}
main:has(.pagehead--404){display:flex; flex-direction:column}

/* --- buttons ------------------------------------------------------------ */
.btn,.btn-ghost{
  display:inline-flex; align-items:center; justify-content:center; gap:9px;
  font-family:'Jost',sans-serif; font-size:16px; font-weight:500;
  padding:15px 26px; border-radius:999px; border:1.5px solid transparent;
  text-decoration:none; cursor:pointer; white-space:nowrap;
  transition:transform .25s var(--ease), box-shadow .25s var(--ease), background .2s, color .2s;
}
.btn{background:var(--ink); color:var(--paper); box-shadow:0 10px 24px -14px rgba(23,20,26,.9)}
.btn:hover{transform:translateY(-2px); box-shadow:0 18px 34px -16px rgba(23,20,26,.75)}
.btn-ghost{background:transparent; color:var(--ink); border-color:rgba(23,20,26,.28)}
.btn-ghost:hover{background:rgba(23,20,26,.05); transform:translateY(-2px)}
.btn-blue{background:var(--blue-deep); color:#fff; box-shadow:0 10px 26px -14px rgba(94,133,152,.95)}
.btn-blue:hover{background:#517989}

/* --- header ------------------------------------------------------------- */
.hdr{
  position:sticky; top:0; z-index:60;
  display:flex; align-items:center; gap:18px;
  padding:12px clamp(16px,3vw,34px);
  background:rgba(252,250,248,.86);
  backdrop-filter:saturate(160%) blur(14px);
  -webkit-backdrop-filter:saturate(160%) blur(14px);
  border-bottom:1px solid transparent;
  transition:border-color .3s, box-shadow .3s;
}
.hdr.stuck{border-bottom-color:var(--line); box-shadow:0 10px 30px -26px rgba(45,32,48,.6)}
.hdr__logo{display:flex; align-items:center; gap:11px; text-decoration:none; margin-right:auto}
.hdr__logo img{width:52px; height:52px}
.hdr__logo b{
  font-family:'Fraunces',serif; font-size:18px; font-weight:700; line-height:1.05;
  letter-spacing:-.01em; display:block;
}
.hdr__logo span{
  display:block; font-size:10px; letter-spacing:.24em; text-transform:uppercase;
  color:var(--blue-deep); font-weight:500; margin-top:2px;
}
.nav{display:flex; align-items:center; gap:26px}
.nav a{font-size:15px; text-decoration:none; color:var(--ink-soft); transition:color .2s}
.nav a:hover{color:var(--ink)}
/* Which page you are on. A colour change alone fails for anyone who cannot see the
   difference, so the underline carries it too. */
.nav a[aria-current]{color:var(--ink); font-weight:500}
.nav a[aria-current]:not(.btn){border-bottom:2px solid var(--plum); padding-bottom:2px}
/* The CTA lives inside .nav, so ".nav a" out-specifies ".btn" and was painting
   the label muted-ink on a near-black pill — 1.89:1, effectively invisible. */
.nav a.btn{color:var(--paper)}
.nav a.btn:hover{color:var(--paper)}

.lang{
  display:inline-flex; border:1.5px solid var(--line); border-radius:999px;
  overflow:hidden; background:rgba(255,255,255,.6);
}
.lang button{
  font:500 13px/1 'Jost',sans-serif; letter-spacing:.06em;
  padding:9px 13px; border:0; background:transparent; color:var(--muted); cursor:pointer;
  transition:background .2s,color .2s;
}
.lang button[aria-pressed="true"]{background:var(--ink); color:var(--paper)}

.burger{display:none; width:44px; height:44px; border:1.5px solid var(--line); border-radius:12px;
  background:transparent; cursor:pointer; padding:0; align-items:center; justify-content:center; flex-direction:column; gap:5px}
.burger i{display:block; width:18px; height:1.6px; background:var(--ink); transition:transform .3s var(--ease), opacity .2s}
.burger[aria-expanded="true"] i:nth-child(1){transform:translateY(6.6px) rotate(45deg)}
.burger[aria-expanded="true"] i:nth-child(2){opacity:0}
.burger[aria-expanded="true"] i:nth-child(3){transform:translateY(-6.6px) rotate(-45deg)}

@media (max-width:940px){
  .burger{display:flex}
  .nav{
    position:fixed; inset:76px 12px auto 12px; flex-direction:column; align-items:stretch;
    gap:0; background:var(--paper); border:1px solid var(--line); border-radius:var(--r-lg);
    padding:10px; box-shadow:var(--shadow-lift);
    opacity:0; visibility:hidden; transform:translateY(-10px);
    transition:opacity .25s, transform .25s var(--ease), visibility .25s;
  }
  .nav.open{opacity:1; visibility:visible; transform:none}
  .nav a{padding:15px 14px; border-bottom:1px solid var(--line); font-size:17px}
  .nav a:last-of-type{border-bottom:0}
  .nav .btn{margin:10px 4px 4px; justify-content:center}
  .hdr__tel{display:none}
}
.hdr__tel{font-size:15px; text-decoration:none; color:var(--ink); font-weight:500}
@media (max-width:1120px){ .hdr .nav>a:not(.btn){font-size:14px} }
@media (max-width:1040px){ .hdr__tel{display:none} }

/* At 390px the logo, the burger and the language toggle came to exactly the
   available width and the "ES" half of the toggle lost its right edge. The
   toggle is the one thing that cannot be trimmed here — a Spanish-speaking
   visitor has to see it without opening a menu — so the logo gives up the room. */
@media (max-width:480px){
  .hdr{gap:10px; padding:10px 14px}
  .hdr__logo{gap:8px}
  .hdr__logo img{width:42px; height:42px}
  .hdr__logo b{font-size:15px}
  .hdr__logo span{font-size:9px; letter-spacing:.2em}
  .lang button{padding:9px 10px; font-size:12px}
  .burger{width:42px; height:42px}
}
/* The same squeeze one size down. At 320px the 480 tier still overran and the "ES"
   half of the toggle lost its right edge again — the page does not scroll sideways,
   so nothing catches it but a screenshot. The toggle is the one control that cannot
   be trimmed here: a Spanish speaker has to see it without opening the menu. */
@media (max-width:360px){
  .hdr{gap:7px; padding:9px 10px}
  .hdr__logo img{width:36px; height:36px}
  .hdr__logo b{font-size:13.5px}
  .hdr__logo span{font-size:8px; letter-spacing:.16em}
  .lang button{padding:8px 8px; font-size:11px}
  .burger{width:38px; height:38px}
}

/* --- hero --------------------------------------------------------------- */
.hero{position:relative; padding:clamp(30px,5vw,60px) 0 clamp(50px,6vw,86px); overflow:hidden}
.hero::before{
  content:''; position:absolute; inset:0 0 auto; height:78%;
  background:linear-gradient(168deg,var(--beige) 0%,var(--cream) 55%,var(--paper) 100%);
  z-index:-2;
}
.hero::after{
  content:''; position:absolute; right:-14vw; top:-16vw; width:46vw; height:46vw;
  border-radius:50%; background:radial-gradient(circle at 35% 35%,var(--pink),transparent 68%);
  opacity:.75; z-index:-1; pointer-events:none;
}
.hero__grid{display:grid; grid-template-columns:1fr 1fr; gap:clamp(28px,4vw,64px); align-items:center}
.hero h1{font-size:clamp(38px,5.6vw,68px)}
.hero h1 em{font-style:normal; color:var(--plum-deep)}
.hero__sub{margin-top:20px; font-size:clamp(17px,1.6vw,20px); color:var(--ink-soft); max-width:34ch}
.hero__cta{display:flex; flex-wrap:wrap; gap:12px; margin-top:30px}
.hero__note{margin-top:20px; font-size:14.5px; color:var(--muted)}

.hero__art{position:relative; padding-bottom:56px; padding-left:34px}
.hero__art .main{
  border-radius:210px 210px var(--r-lg) var(--r-lg);
  overflow:hidden; box-shadow:var(--shadow-lift); background:var(--beige);
}
.hero__art .main img{width:100%; height:auto; aspect-ratio:4/5; object-fit:cover}
.hero__art .inset{
  position:absolute; left:0; bottom:0; width:44%; max-width:230px;
  border-radius:var(--r); overflow:hidden;
  box-shadow:var(--shadow-lift); border:6px solid var(--paper); background:var(--beige);
}
.hero__art .inset img{width:100%; aspect-ratio:1/1; object-fit:cover}

@media (max-width:860px){
  .hero__grid{grid-template-columns:1fr}
  .hero__art{order:-1; padding-left:0; padding-bottom:0; max-width:440px; margin-inline:auto; width:100%}
  .hero__art .inset{display:none}
  .hero__sub{max-width:none}
  /* The 4/5 crop is 487px tall at 390px wide, which pushed the headline AND both
     buttons off the first screen of a phone — the photo was the entire fold and
     nothing told the visitor what to do. Her customers arrive from Instagram on a
     phone, so the crop gives up height until the h1 and the CTAs fit. */
  .hero__art .main img{aspect-ratio:auto; height:min(38vh,330px); object-position:center 38%}
  .hero{padding-top:18px}
  .hero__sub{margin-top:14px}
  .hero__cta{margin-top:20px}
  .hero__note{margin-top:14px}
}
/* 38vh was checked against one phone and one phone only. On a 375x667 iPhone SE —
   which Apple still sells — the first button was clipped to a sliver at the bottom
   edge, on a 360x800 Android it sat 21px under the fold, and on a 320x568 it was 114px
   under. A hero whose buttons are below the fold is decoration, so on a short screen
   the photo and the type give up height and the two buttons sit side by side instead
   of stacked, which is 66px on its own. Checked at 320x568, 360x640, 360x800, 375x667,
   390x844, 414x896 and 428x926 — not at one size and then assumed. */
@media (max-width:940px) and (max-height:820px){
  .hero__art .main img{height:min(28vh,220px)}
  .hero h1{font-size:clamp(30px,7.6vw,40px)}
  .hero__sub{font-size:15.5px; margin-top:10px}
  .hero__cta{margin-top:14px; flex-wrap:nowrap; gap:8px}
  .hero__cta .btn,.hero__cta .btn-ghost{flex:1 1 0; min-width:0; padding:13px 8px; font-size:14.5px}
  .hero__note{margin-top:10px; font-size:13px}
}
@media (max-width:940px) and (max-height:660px){
  .hero{padding-top:10px}
  .hero__art .main img{height:min(22vh,130px)}
  .hero h1{font-size:clamp(26px,7.2vw,32px)}
  .hero__sub{font-size:14.5px}
}

/* --- trust rail --------------------------------------------------------- */
/* Flat --ink read as a black stripe dropped between two warm sections. Same ink,
   lifted in the middle and cooled with her blue, so it reads as a band that was
   designed rather than a gap that was filled. */
.rail{
  color:var(--paper); position:relative;
  background:
    radial-gradient(58% 340% at 50% 50%,rgba(131,164,182,.20),transparent 72%),
    linear-gradient(90deg,#141118 0%,#221B27 50%,#141118 100%);
}
.rail ul{
  list-style:none; margin:0; padding:20px 0; display:flex; flex-wrap:wrap;
  gap:12px clamp(20px,4vw,52px); align-items:center; justify-content:center;
  font-size:15px;
}
.rail li{display:flex; align-items:center; gap:9px; color:rgba(252,250,248,.86)}
.rail li b{color:#fff; font-weight:500}
.rail svg{width:17px; height:17px; flex:none; stroke:var(--blue); fill:none; stroke-width:1.7}

/* --- sections ----------------------------------------------------------- */
section{scroll-margin-top:82px}
/* Even the pale bands carry a wash now. Flat --paper behind a text-only section
   reads as an unfinished page, and "How it works" sits directly above the dark
   booking slab where the drop from nothing to near-black was at its most abrupt.
   Weak on purpose: this has to stay clearly lighter than .band--beige or the
   alternation between sections stops doing its job. */
.band{
  padding:clamp(58px,7vw,104px) 0;
  background:
    radial-gradient(52% 42% at 88% 3%,rgba(131,164,182,.11),transparent 62%),
    radial-gradient(48% 40% at 3% 97%,rgba(245,228,249,.38),transparent 62%);
}
/* A flat cream fill behind white cards gives the cards nothing to sit on. Two
   very weak washes in her pink and blue, warming toward the bottom, so the band
   has a direction. Kept under .55 alpha on tints that are already pale — anything
   stronger and the price cards stop reading as raised. */
.band--beige{
  background:
    radial-gradient(56% 46% at 100% 0%,rgba(245,228,249,.55),transparent 62%),
    radial-gradient(46% 42% at 0% 100%,rgba(131,164,182,.14),transparent 64%),
    linear-gradient(180deg,#F8F2EC 0%,var(--cream) 52%,#F2E9E0 100%);
}

/* --- package cards ------------------------------------------------------ */
/* four cards since the three-panel tier got a photograph that actually shows
   three panels. Two-up before it gets cramped, one-up on a phone. */
.packs{display:grid; grid-template-columns:repeat(4,1fr); gap:clamp(14px,1.8vw,22px)}
@media (max-width:1080px){ .packs{grid-template-columns:repeat(2,1fr); gap:clamp(16px,2.2vw,26px)} }
@media (max-width:560px){ .packs{grid-template-columns:1fr} }

.pack{
  background:var(--paper); border-radius:var(--r-lg); overflow:hidden;
  border:1px solid var(--line); box-shadow:var(--shadow);
  display:flex; flex-direction:column;
  transition:transform .35s var(--ease), box-shadow .35s var(--ease);
}
.pack:hover{transform:translateY(-6px); box-shadow:var(--shadow-lift)}
.pack figure{margin:0; position:relative; background:var(--beige); overflow:hidden}
.pack figure img{width:100%; aspect-ratio:4/5; object-fit:cover; transition:transform .7s var(--ease)}
.pack:hover figure img{transform:scale(1.045)}
.pack__tag{
  position:absolute; left:14px; top:14px; z-index:2;
  background:rgba(252,250,248,.94); color:var(--ink);
  font-size:12px; letter-spacing:.14em; text-transform:uppercase; font-weight:500;
  padding:7px 13px; border-radius:999px;
}
.pack__body{padding:22px 22px 24px; display:flex; flex-direction:column; gap:9px; flex:1}
.pack__body h3{font-size:21px}
.pack__body p{color:var(--ink-soft); font-size:15.5px}
.pack__price{
  margin-top:auto; padding-top:14px; display:flex; align-items:baseline; gap:8px;
  border-top:1px solid var(--line);
}
.pack__price b{font-family:'Fraunces',serif; font-size:27px; font-weight:700; letter-spacing:-.02em}
.pack__price span{font-size:13.5px; color:var(--muted)}

/* --- price list --------------------------------------------------------- */
.pricecard{
  margin-top:clamp(22px,3vw,34px);
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-lg);
  box-shadow:var(--shadow); overflow:hidden;
  display:grid; grid-template-columns:1.25fr 1fr;
}
@media (max-width:820px){ .pricecard{grid-template-columns:1fr} }
.pricecard__list{padding:clamp(24px,3vw,34px)}
.pricecard__list h3{font-size:24px; margin-bottom:6px}
.pricecard__list>p{color:var(--ink-soft); font-size:15.5px; margin-bottom:20px}
.pricelist{list-style:none; margin:0; padding:0}
.pricelist li{
  display:flex; align-items:baseline; gap:12px; padding:13px 0;
  border-bottom:1px dashed var(--line); font-size:16.5px;
}
.pricelist li:last-child{border-bottom:0}
.pricelist .dots{flex:1; border-bottom:1px dotted rgba(23,20,26,.28); transform:translateY(-4px)}
.pricelist b{font-family:'Fraunces',serif; font-size:20px; font-weight:700; white-space:nowrap}
.pricecard__aside{
  background:linear-gradient(160deg,var(--beige),var(--pink));
  padding:clamp(24px,3vw,34px); display:flex; flex-direction:column; justify-content:center; gap:14px;
}
.pricecard__aside h4{font-family:'Fraunces',serif; font-size:22px; font-weight:700; margin:0; line-height:1.15}
.pricecard__aside p{font-size:15.5px; color:var(--ink-soft)}
.pricecard__aside ul{margin:0; padding-left:19px; font-size:15.5px; color:var(--ink-soft)}
.pricecard__aside li{margin:5px 0}
.pricecard__aside .btn{align-self:flex-start; margin-top:6px}
.pricecard__fine{font-size:14.5px; color:var(--ink-soft); margin-top:2px}

/* --- gallery ------------------------------------------------------------ */
/* Eighteen identical 4:5 tiles read as a contact sheet, and a contact sheet is the
   worst frame for phone photos shot in flat midday sun: it hands every frame the
   same weight, so the weakest ones set the tone. A grid with a lead tile and two
   wide ones gives her best work scale and lets the rest read as supporting. */
.gal{display:grid; grid-template-columns:repeat(4,1fr); gap:14px; grid-auto-flow:dense}
@media (max-width:900px){ .gal{grid-template-columns:repeat(3,1fr)} }
@media (max-width:560px){ .gal{grid-template-columns:repeat(2,1fr); gap:10px} }
/* The ratio lives on the figure, not the image. A spanning tile has to size its
   own cell or the row height comes from its neighbours and the crop lands on
   whatever was above the subject — which here is Las Vegas sky and power lines. */
.gal figure,.strip figure{
  margin:0; position:relative; overflow:hidden; aspect-ratio:4/5;
  border-radius:var(--r); background:var(--beige); box-shadow:var(--shadow);
}
.gal img,.strip img{width:100%; height:100%; object-fit:cover; display:block;
  transition:transform .8s var(--ease)}

/* The lead frame. Two columns by two rows is 4:5 again once the gap is counted,
   so it enlarges her cleanest photograph without recropping it. */
.gal .gal__lead{grid-column:span 2; grid-row:span 2}

/* Three wide crops, all of subjects that really are wider than they are tall: the
   dentistry storefront, the reunion room and the monster-truck setup. At 8:5 a
   two-column tile is exactly as tall as a one-column tile, so the grid packs with no
   holes — and a wide crop of a wide subject throws away sky rather than the setup.

   THREE, not two, and the count is the reason: 18 photos with a 4-cell lead is 23
   cells, and 23 leaves a hole in the last row. A third wide tile makes 24, which
   divides by 4, by 3 AND by 2 — so it ends flush at every breakpoint this grid has.
   ⚠ Do not demote these to one column on a phone. That was the obvious-looking call
   and it breaks the arithmetic: 4 + 17 = 21 cells, odd, so the gallery ended on a
   lone tile beside an empty cell on exactly the device this build is for.
   ⚠ CLASSES, not :nth-child. Position selectors were calibrated against a hand-ordered
   list, so adding one photo at the top silently moved an 8:5 crop onto a photograph
   nobody chose it for. The class travels with its own picture. */
.gal .gal__wide{grid-column:span 2; aspect-ratio:8/5}
.gal .gal__wide img{object-position:center 56%}
.gal figure:hover img,.strip figure:hover img{transform:scale(1.06)}
.gal figcaption,.strip figcaption{
  position:absolute; inset:auto 0 0; padding:34px 14px 13px; color:#fff; font-size:14px;
  background:linear-gradient(to top,rgba(20,14,22,.82),transparent);
  opacity:0; transform:translateY(8px); transition:opacity .3s, transform .3s var(--ease);
}
.gal figure:hover figcaption, .gal figure:focus-within figcaption,
.strip figure:hover figcaption, .strip figure:focus-within figcaption{opacity:1; transform:none}
@media (hover:none){ .gal figcaption,.strip figcaption{opacity:1; transform:none} }

/* Six uniform tiles, three across then two. Both counts divide six exactly, so the
   last row is never a lone tile beside an empty cell — the same arithmetic the main
   gallery has to satisfy, done the easy way because none of these tiles span. */
.strip{display:grid; grid-template-columns:repeat(3,1fr); gap:14px}
@media (max-width:820px){ .strip{grid-template-columns:repeat(2,1fr); gap:12px} }
@media (max-width:560px){ .strip{gap:10px} }

/* --- add-ons ------------------------------------------------------------ */
.addons{display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(16px,2.2vw,26px)}
@media (max-width:820px){ .addons{grid-template-columns:1fr} }
/* 4:5, not a squat landscape box. The popcorn machine is a tall object and in a
   wide card it read as a stripe behind the balloons rather than the thing for sale. */
/* ⚠ min-width:0 is load-bearing, not tidying. A grid item defaults to min-width:auto,
   and aspect-ratio 4/5 with min-height:400px gives this box an intrinsic minimum width
   of 320px — so in the 3-column track it refused to shrink and pushed the whole PAGE
   wider than the viewport at 320, 860 and 900px. A real sideways scrollbar on a phone,
   live for nine days, and invisible to the screenshot check because that measures
   scrollWidth on the widths it was told to sample. */
.addon{
  position:relative; border-radius:var(--r-lg); overflow:hidden; aspect-ratio:4/5;
  min-height:400px; min-width:0;
  display:flex; align-items:flex-end; box-shadow:var(--shadow); background:var(--beige);
}
@media (max-width:1024px){ .addon{min-height:300px} }
@media (max-width:400px){ .addon{min-height:240px} }
.addon img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover; transition:transform .8s var(--ease)}
.addon:hover img{transform:scale(1.05)}
/* The scrim is heavier than it looks like it needs to be because two of these
   three photographs are pale — a candy cart in daylight and a white storefront
   sign — and white type over them failed contrast at the top of the block. */
.addon__body{
  position:relative; z-index:2; padding:26px 24px 24px; color:#fff; width:100%;
  background:linear-gradient(to top,rgba(18,12,20,.94) 0%,rgba(18,12,20,.82) 34%,rgba(18,12,20,.45) 70%,transparent 100%);
}
.addon__body h3{text-shadow:0 1px 12px rgba(18,12,20,.55)}
.addon__body h3{font-size:22px; color:#fff}
.addon__body p{margin-top:7px; font-size:15px; color:rgba(255,255,255,.9)}

/* --- steps -------------------------------------------------------------- */
.steps{display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(20px,3vw,40px); counter-reset:s}
@media (max-width:820px){ .steps{grid-template-columns:1fr; gap:26px} }
/* The rule used to be a border-top with the number painting over it in the band's
   own flat colour. That only works while the band IS flat — the moment a section
   gets a wash behind it, the number shows as a pale rectangle sitting on the line.
   The line is now a background that simply starts after the number, so it does not
   care what is behind it. */
.step{
  position:relative; padding-top:22px;
  background-image:linear-gradient(90deg,transparent 0 34px,var(--line) 34px);
  background-repeat:no-repeat; background-size:100% 2px; background-position:0 0;
}
.step::before{
  counter-increment:s; content:counter(s,decimal-leading-zero);
  font-family:'Fraunces',serif; font-size:15px; font-weight:700; color:var(--blue-deep);
  position:absolute; top:-13px; left:0;
}
.step h3{font-size:21px; margin-bottom:9px}
.step p{color:var(--ink-soft); font-size:16px}

/* --- booking ------------------------------------------------------------ */
/* THIS SECTION HAS A PHOTOGRAPH BEHIND IT AND THAT IS THE POINT.
   It was flat --ink with a blob in the corner, then flat --ink with gradients over
   it, and Julian called both of them bland — twice. Gradients on a party-decor site
   are not a background, they are an absence of one. What is behind the quote form
   now is HER OWN BALLOON WALL, scrimmed only as far as the white form and the white
   type actually need. A balloon backdrop behind the form that asks you which balloon
   backdrop you want.
   ⚠ It is TWO cuts of the same photograph, not one: a wide band above 860px and a
   portrait cut at 860 and under — see the media query below for why. 37KB of webp
   for the band, 30KB for the portrait, and a visitor loads one of them, never both.
   Recipes: tools/recrop.py, book-bg and book-bg-tall. */
.book{
  color:var(--paper); position:relative; overflow:hidden;
  background:#131017 url(/assets/img/book-bg.jpg) center/cover no-repeat;
  /* Second declaration, not a fallback inside one: a browser too old for image-set
     ignores this line entirely and keeps the .jpg above. Nothing 404s either way. */
  background-image:image-set(url(/assets/img/book-bg.webp) type("image/webp"),
                             url(/assets/img/book-bg.jpg) type("image/jpeg"));
}
/* ⚠ A PHONE GETS A DIFFERENT PHOTOGRAPH, NOT A SMALLER COPY OF THIS ONE — 2026-08-14.
   The wide 2:1 band is the wrong picture for a tall narrow box: cover scales it by
   height, so a phone saw roughly a tenth of its width. Every version of that was a
   dark smear — first with the neon sign dead centre arriving as a white squiggle,
   then, panned to 76%, as an out-of-focus wall of nothing. It never read as balloons,
   which is the only reason the photo is there.
   book-bg-tall is a PORTRAIT cut of the same wall, chosen for a phone: dense pastels
   and one of her dinosaur balloons, no sky, no sliver of the neighbour's house, and
   no piece of the Happy Birthday sign — a cropped word behind a form that also sells
   baby showers and grand openings is worse than no word.
   ⚠ THE SIGN CLAIM WAS FALSE UNTIL 2026-08-14 EVENING AND THE REASON IS WORTH KEEPING.
   The first cut ran to x 0.45 of the source and the sign's leftmost stroke starts at
   0.42, so a neon fragment sat hard against the file's right edge. It was invisible at
   390px purely by accident: this box is far taller than the image's 0.625 aspect there,
   so `cover` fits by HEIGHT and throws the sides away. Above ~685px the box crosses that
   aspect, `cover` fits by WIDTH instead, and the full image width paints — so every
   tablet in portrait (768-834) got the white curl the wide cut was re-cut to remove.
   The recipe now stops at x 0.41. **A `cover` background is not one picture, it is a
   different picture at every box aspect: check the widest width the rule serves, not
   just the narrowest.** 27KB of webp at 760×1216. Recipe: tools/recrop.py, book-bg-tall.
   ⚠ This breakpoint is 860, the SAME width where .book__grid collapses to one column.
   It used to be 700, which left 700-860px running the desktop photo under the mobile
   layout. Image and layout change together now — keep them on one number. */
@media (max-width:860px){
  .book{
    background-position:center;
    background-image:url(/assets/img/book-bg-tall.jpg);
    background-image:image-set(url(/assets/img/book-bg-tall.webp) type("image/webp"),
                               url(/assets/img/book-bg-tall.jpg) type("image/jpeg"));
  }
}
/* ⚠ THE SCRIM IS DIRECTIONAL AND THAT IS THE WHOLE POINT — 2026-08-14.
   A flat wash at .74-.82 across the full width darkened the half of the photo that
   sits BEHIND AN OPAQUE WHITE CARD, where nothing has to be readable. The balloons
   stopped reading as balloons and the section went back to looking like a dark
   textured slab — the exact thing Julian called bland, one step less obviously.

   Her plum bottom-left and her blue top-right do the tinting, so the photo reads as
   part of the brand rather than a stock darkening.

   The wide layout is two columns: white type on the left, the opaque .form card on the
   right. So the darkness follows the type — heavy down the left edge where the h2, the
   lede and the two contact pills sit, then released across the right half and the outer
   margin, where her balloon wall is finally allowed to be a photograph of balloons.

   ⚠ THIS DIRECTIONAL VERSION IS THE BASE RULE AND THE VERTICAL ONE IS THE OVERRIDE —
   it used to be the other way round, with the directional scrim inside
   `@media (min-width:861px)`. That left a sliver at fractional widths (860 < w < 861,
   which browser zoom and fractional device pixel ratios really do produce) where
   NEITHER query matched: the two-column grid, which flips at max-width:860, but the
   one-column top-heavy scrim. The left column's white type then sat on an
   under-darkened photo. One boundary condition, one number: everything that changes at
   the collapse now lives in the same max-width:860 block. */
.book::before{
  content:''; position:absolute; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(72% 96% at 96% 6%,rgba(56,98,122,.34),transparent 66%),
    radial-gradient(64% 104% at -10% 96%,rgba(104,70,100,.50),transparent 68%),
    linear-gradient(94deg,
      rgba(21,17,26,.88) 0%,
      rgba(20,16,25,.84) 34%,
      rgba(18,15,23,.66) 50%,
      rgba(16,13,21,.50) 66%,
      rgba(15,12,20,.46) 100%);
}
/* At the collapse the grid is one column — type on top, card underneath — so the same
   idea turns vertical: dark at the top where the type is, released toward the bottom
   around and below the card.
   ⚠ Do not push these alphas past ~.9: at that point the photo is gone and this is the
   flat black box again. And when you change them, do not eyeball it and do not type a
   number into this comment — RUN THE MEASUREMENT:

       python3 -m http.server 8877 --directory site &
       node tools/contrast.mjs          # exits 1 if anything drops under AA's 4.5

   Two hand-maintained copies of this table used to live here and in STATE.md, both
   stamped "measured", and on 2026-08-14 they disagreed with each other on every desktop
   row — then three reviewers re-sampling the same PNGs by hand produced three more
   answers. Sampling a screenshot by eye is not a measurement. The command is.
   Measured 2026-08-14 evening, and the only copy of these numbers that is allowed to
   exist outside the tool's own output:

       desktop   eyebrow 11.44  h2 17.41  lede 10.93  pill sub 7.98
       mobile    eyebrow  9.18  h2 14.51  lede  8.97  pill sub 7.05

   The floor is the 13.5px pill sub-label on mobile — not the eyebrow, which is what an
   older version of this comment told you to measure back when it was opaque --blue. */
@media (max-width:860px){
  .book::before{
    background:
      radial-gradient(85% 70% at 92% -6%,rgba(56,98,122,.40),transparent 64%),
      radial-gradient(90% 80% at -8% 106%,rgba(104,70,100,.52),transparent 66%),
      linear-gradient(180deg,rgba(24,19,29,.58) 0%,rgba(21,17,26,.62) 40%,rgba(17,14,22,.46) 76%,rgba(15,12,20,.32) 100%);
  }
}
/* One lit hairline on the top edge, so the section starts instead of just
   changing colour where the cream above it stops. */
.book::after{
  content:''; position:absolute; left:0; right:0; top:0; height:1px; z-index:1;
  pointer-events:none;
  background:linear-gradient(90deg,transparent,rgba(131,164,182,.55) 24%,rgba(157,127,152,.6) 68%,transparent);
}
/* Was an inline style="color:var(--blue)" on the partial, which put a colour decision
   in the markup where nobody measuring contrast would look for it. */
.book .eyebrow{color:var(--blue-pale)}
.book__grid{display:grid; grid-template-columns:.85fr 1.15fr; gap:clamp(30px,5vw,68px); align-items:start; position:relative; z-index:2}
@media (max-width:860px){ .book__grid{grid-template-columns:1fr} }
.book h2{font-size:clamp(30px,4.4vw,46px); color:#fff}
.book__lede{margin-top:16px; color:rgba(252,250,248,.8); font-size:17.5px}
.book__direct{margin-top:28px; display:flex; flex-direction:column; gap:11px}
.book__direct a{
  display:flex; align-items:center; gap:13px; text-decoration:none; color:#fff;
  border:1px solid rgba(252,250,248,.22); border-radius:var(--r); padding:15px 18px;
  transition:background .2s, border-color .2s, transform .25s var(--ease);
}
.book__direct a:hover{background:rgba(252,250,248,.09); border-color:rgba(252,250,248,.4); transform:translateX(3px)}
.book__direct svg{width:20px; height:20px; flex:none; stroke:var(--blue); fill:none; stroke-width:1.7}
.book__direct b{display:block; font-weight:500; font-size:16.5px}
/* .68, not the .6 it was. With the scrim lightened so her balloons actually read,
   this 13.5px sub-label became the tightest text in the section at 4.93:1 — passing,
   with almost no room. .68 buys back a point and costs nothing anyone can see. */
.book__direct span{display:block; font-size:13.5px; color:rgba(252,250,248,.68)}

.form{background:var(--paper); color:var(--ink); border-radius:var(--r-lg); padding:clamp(22px,3vw,34px); box-shadow:var(--shadow-lift)}
.form__row{display:grid; grid-template-columns:1fr 1fr; gap:14px}
@media (max-width:520px){ .form__row{grid-template-columns:1fr} }
.field{display:flex; flex-direction:column; gap:7px; margin-bottom:14px}
.field label{font-size:13.5px; font-weight:500; color:var(--ink-soft); letter-spacing:.02em}
.field input,.field select,.field textarea{
  font:400 16px/1.45 'Jost',sans-serif; color:var(--ink);
  padding:13px 15px; border:1.5px solid var(--line); border-radius:12px;
  background:#fff; width:100%; transition:border-color .2s, box-shadow .2s;
}
.field textarea{resize:vertical; min-height:92px}
.field input:focus,.field select:focus,.field textarea:focus{
  border-color:var(--blue-deep); box-shadow:0 0 0 3px rgba(131,164,182,.22); outline:none;
}
.form .btn{width:100%; margin-top:6px; padding:17px 26px; font-size:17px}
.form__fine{margin-top:14px; font-size:13.5px; color:var(--muted); text-align:center}

/* --- footer ------------------------------------------------------------- */
/* The footer catches the eye that just left the dark booking slab, so a flat
   cream rectangle is where the page visibly gives up. Her pink washes in from
   the top-left corner, her blue from the right, and the fill warms downward. */
.ftr{
  position:relative; overflow:hidden;
  background:
    radial-gradient(72% 130% at 8% -30%,rgba(245,228,249,.75),transparent 62%),
    radial-gradient(58% 130% at 102% -34%,rgba(131,164,182,.16),transparent 62%),
    linear-gradient(180deg,#F8F2EB 0%,#F1E7DD 100%);
  padding:clamp(38px,5vw,58px) 0 30px; border-top:1px solid var(--line);
}
.ftr__grid{display:flex; flex-wrap:wrap; gap:32px; align-items:flex-start; justify-content:space-between}
.ftr__brand{display:flex; align-items:center; gap:13px}
.ftr__brand img{width:64px; height:64px}
.ftr__brand b{font-family:'Fraunces',serif; font-size:19px; display:block}
.ftr__brand span{font-size:10px; letter-spacing:.24em; text-transform:uppercase; color:var(--blue-deep)}
.ftr__links{display:flex; flex-wrap:wrap; gap:10px 26px; font-size:15px}
.ftr__links a{text-decoration:none; color:var(--ink-soft)}
.ftr__links a:hover{color:var(--ink)}
.ftr__links a[aria-current]{color:var(--ink); font-weight:500}
.ftr__legal{margin-top:30px; padding-top:20px; border-top:1px solid var(--line); font-size:13.5px; color:var(--muted);
  display:flex; flex-wrap:wrap; gap:8px 20px; justify-content:space-between}

/* --- reveal ------------------------------------------------------------- */
.rise{opacity:0; transform:translateY(22px); transition:opacity .7s var(--ease), transform .7s var(--ease)}
.rise.in{opacity:1; transform:none}
@media (prefers-reduced-motion:reduce){ .rise{opacity:1; transform:none; transition:none} }

/* --- desktop send fallback ---------------------------------------------
   There is no SMS handler on most desktops, so the sms: link is a dead
   button there. This panel is what a desktop visitor gets instead. */
.sent{
  margin-top:18px; padding:18px; border-radius:var(--r);
  background:var(--cream); border:1px solid var(--line);
  opacity:0; max-height:0; overflow:hidden; padding-top:0; padding-bottom:0;
  transition:opacity .3s var(--ease), max-height .4s var(--ease), padding .3s var(--ease);
}
.sent.in{opacity:1; max-height:520px; padding-top:18px; padding-bottom:18px}
.sent__head{font-weight:500; font-size:15.5px; margin-bottom:11px}
.sent__body{
  width:100%; font:400 14.5px/1.55 'Jost',sans-serif; color:var(--ink);
  padding:13px 15px; border:1.5px solid var(--line); border-radius:12px;
  background:#fff; resize:vertical;
}
.sent__row{display:flex; flex-wrap:wrap; gap:10px; margin-top:12px}
.sent__row .btn,.sent__row .btn-ghost{flex:1; min-width:170px; padding:13px 20px; font-size:15px; margin-top:0}

/* --- the pricing calculator ---------------------------------------------
   The one thing Julian sold her on the 2026-07-31 call. Prices only from her
   own published figures; the output says "starting at" because that is the
   frame she agreed to on that call. */
.calc{
  margin-top:clamp(22px,3vw,34px);
  display:grid; grid-template-columns:1.15fr .85fr;
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-lg);
  box-shadow:var(--shadow); overflow:hidden;
}
@media (max-width:820px){ .calc{grid-template-columns:1fr} }
.calc__form{padding:clamp(24px,3vw,34px)}
.calc__form h3{font-size:24px; margin-bottom:6px}
.calc__lede{color:var(--ink-soft); font-size:15.5px; margin-bottom:22px}
.calc__field{margin-bottom:18px}
.calc__field>label{display:block; font-size:13.5px; font-weight:500; color:var(--ink-soft); margin-bottom:7px}
.calc__field select{
  font:400 16px/1.45 'Jost',sans-serif; color:var(--ink); width:100%;
  padding:13px 15px; border:1.5px solid var(--line); border-radius:12px; background:#fff;
  transition:border-color .2s, box-shadow .2s;
}
.calc__field select:focus{border-color:var(--blue-deep); box-shadow:0 0 0 3px rgba(131,164,182,.22); outline:none}

.stepper{display:flex; align-items:center; gap:9px; flex-wrap:wrap}
.stepper button{
  width:46px; height:46px; flex:none; border:1.5px solid var(--line); border-radius:12px;
  background:#fff; font:400 22px/1 'Jost',sans-serif; color:var(--ink); cursor:pointer;
  transition:background .2s, border-color .2s;
}
.stepper button:hover{background:var(--cream); border-color:rgba(23,20,26,.3)}
.stepper input{
  width:76px; text-align:center; font:500 17px/1 'Jost',sans-serif; color:var(--ink);
  padding:14px 6px; border:1.5px solid var(--line); border-radius:12px; background:#fff;
}
.stepper input:focus{border-color:var(--blue-deep); box-shadow:0 0 0 3px rgba(131,164,182,.22); outline:none}
.stepper__note{font-size:14px; color:var(--muted)}

.calc__out{
  background:linear-gradient(160deg,var(--beige),var(--pink));
  padding:clamp(24px,3vw,34px); display:flex; flex-direction:column; justify-content:center;
}
.calc__label{font-size:12px; font-weight:500; letter-spacing:.22em; text-transform:uppercase; color:var(--plum-deep)}
.calc__total{font-family:'Fraunces',serif; font-size:clamp(46px,6vw,62px); font-weight:700; line-height:1; margin-top:8px; letter-spacing:-.03em}
.calc__lines{list-style:none; margin:16px 0 0; padding:0; font-size:14.5px; color:var(--ink-soft)}
.calc__lines li{display:flex; justify-content:space-between; gap:14px; padding:5px 0; border-bottom:1px dashed rgba(23,20,26,.16)}
.calc__lines li:last-child{border-bottom:0}
.calc__lines b{font-weight:500; color:var(--ink); white-space:nowrap}
.calc__fine{margin-top:16px; font-size:13.5px; color:var(--ink-soft)}
.calc__out .btn{align-self:flex-start; margin-top:18px}
.pack__pre{font-size:14.5px; color:var(--muted); align-self:baseline}

/* --- phone action bar --------------------------------------------------- */
/* Desktop keeps the number in the header, so this exists only on a phone. */
.callbar{display:none}
/* 940, not 860: the burger appears at 940 and .hdr__tel hides at 1040, so between
   861 and 940 there was no visible phone number anywhere without opening the menu. */
@media (max-width:940px){
  .callbar{
    display:flex; gap:10px; position:fixed; z-index:60;
    left:12px; right:12px; bottom:calc(12px + env(safe-area-inset-bottom));
    opacity:0; visibility:hidden; transform:translateY(130%);
    transition:opacity .25s, transform .3s var(--ease), visibility .3s;
  }
  .callbar.show{opacity:1; visibility:visible; transform:none}
  .callbar__btn{
    flex:1; display:inline-flex; align-items:center; justify-content:center; gap:9px;
    height:54px; border-radius:999px; text-decoration:none;
    font:500 16px/1 'Jost',sans-serif; letter-spacing:.01em;
    box-shadow:0 14px 34px -16px rgba(23,20,26,.6);
  }
  .callbar__btn svg{
    width:19px; height:19px; fill:none; stroke:currentColor; stroke-width:1.7;
    stroke-linecap:round; stroke-linejoin:round;
  }
  .callbar__btn--call{background:var(--ink); color:var(--paper)}
  .callbar__btn--text{background:var(--paper); color:var(--ink); border:1.5px solid var(--line)}
  /* the footer's last line was sitting underneath the bar */
  .ftr{padding-bottom:96px}
}
