/* ============================================================================
   DR. OETKER BASE  +  GREAT INDIAN SANDWICH CAMPAIGN SKIN
   Architecture (spec S10 move 9): :root carries the corporate Dr. Oetker system
   verbatim. `.gis` on the app wrapper re-declares ONLY the campaign-owned
   custom properties. Delete the class and the page reverts to corporate.
   ============================================================================ */

/* ---- fonts (spec S13.2, verbatim mapping) ---- */
@font-face{font-family:"MomentsDisplay";font-style:normal;font-weight:700;font-display:swap;
  src:url("fonts/MomentsDisplay-700.woff2") format("woff2")}
@font-face{font-family:"MomentsDisplay";font-style:normal;font-weight:500;font-display:swap;
  src:url("fonts/MomentsDisplay-500.woff2") format("woff2")}
@font-face{font-family:"MomentsText";font-style:normal;font-weight:400;font-display:swap;
  src:url("fonts/MomentsText-400.woff2") format("woff2")}
@font-face{font-family:"AugustScript";font-style:normal;font-weight:400;font-display:swap;
  src:url("fonts/AugustScript-400.woff2") format("woff2")}
/* metric-matched local-Arial fallbacks (spec S2.2) - stop the swap reflow */
@font-face{font-family:"MomentsDisplay Fallback";src:local("Arial");
  ascent-override:115.99%;descent-override:29.97%;line-gap-override:0%;size-adjust:82.07%}
@font-face{font-family:"MomentsText Fallback";src:local("Arial");
  ascent-override:95.64%;descent-override:24.44%;line-gap-override:0%;size-adjust:99.44%}
@font-face{font-family:"AugustScript Fallback";src:local("Arial");
  ascent-override:118.23%;descent-override:48.09%;line-gap-override:0%;size-adjust:82.97%}

/* ============ BASE: the corporate Dr. Oetker system ============ */
:root{
  --font-primary:'MomentsDisplay','MomentsDisplay Fallback',Arial,sans-serif;
  --font-neutral:'MomentsText','MomentsText Fallback',Arial,sans-serif;
  --font-secondary:'AugustScript','AugustScript Fallback',Arial,cursive;

  --primary:#A45A2A;        /* terracotta - THE interactive colour, never campaign-owned */
  --primary-ink:#FFFFFF;    /* 5.15:1 on terracotta */
  --primary-hover:#8E4D24;  /* CTA pill hover. Also literal in 404.html's own palette copy */
  --darkgray:#292B29;       /* all body + heading text */
  --gray-500:#5A5E5A;       /* secondary text */
  --white:#FFFFFF;
  --cream:#EFDBB2;          /* hero wash + recipe card footer */
  --gold:#E1B87F;           /* alternating card footer */
  --beige:#F7E9D5;          /* footer band */
  --coolgray:#F4F5F4;       /* category card footer */
  --paper:#FDF9F4;          /* page body (bg-lightBrown). terracotta clears 4.5:1 here */
  --sage:#BBC4BA;           /* hairlines */
  --hairline:#E8EBE8;
  /* ONE error colour. The invalid border used to be --campaign #ED1C24 while
     the message under it was #B3121A: two reds 3px apart in the same control.
     #B3121A is the one that is legal as 14px text (6.64:1 on paper, 6.37:1 on
     the cool form panel), so it is the one both take. */
  --err:#B3121A;

  /* campaign hooks, neutral at base. .gis overrides these and nothing else. */
  --campaign:#A45A2A;
  --campaign-ink:#FFFFFF;
  --hero-fill:#EFDBB2;
  --hero-ink:#292B29;

  --r-card:16px; --r-pill:100px;
  --ease:cubic-bezier(.4,0,.2,1);
  --ease-out:cubic-bezier(0,0,.2,1);
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; --s7:48px;
}

/* ============ SKIN: The Great Indian Sandwich ============
   Colour temperature comes from the jar. Nothing structural moves. ~70/30
   neutrals to red: red is the hero curve, the seal and the confirmation only. */
.gis{
  --campaign:#ED1C24;        /* FunFoods pack red */
  --campaign-ink:#FFFFFF;    /* 4.38:1 - LEGAL ONLY at >=19px/700. Enforced below. */
  --hero-fill:#ED1C24;
  --hero-ink:#FFFFFF;
}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;
  /* vertical overscroll never chains to the browser: Android pull-to-refresh at
     scroll 0 would take the whole in-memory build with it. The -x axis is set on
     the rails (line ~668); this is the axis that never got it. */
  overscroll-behavior-y:contain;
  /* the UA default rgba(0,0,0,.18) fired a grey box on every option-card tap over
     cream and gold. --primary #A45A2A at 12%, literal because the property takes
     no alpha channel of its own. */
  -webkit-tap-highlight-color:rgba(164,90,42,.12)}
body{
  margin:0;background:var(--paper);color:var(--darkgray);
  font:400 16px/1.5 var(--font-neutral);
  -webkit-font-smoothing:antialiased;
}
::selection{background:var(--cream);color:var(--darkgray)}
:root{scrollbar-color:var(--sage) transparent}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:var(--sage);border-radius:var(--r-pill);border:3px solid var(--paper)}
::-webkit-scrollbar-track{background:transparent}
input{caret-color:var(--primary)}
/* spec S11.5: terracotta focus ring, never Tailwind blue */
:focus-visible{outline:2px solid var(--primary);outline-offset:2px;border-radius:6px}
:where(button,[role=button],input,label){touch-action:manipulation}
:where(button,[role=button]){cursor:pointer}
:where(button):disabled{cursor:not-allowed}
img{max-width:100%;display:block}

/* ============ shell ============ */
.app{max-width:440px;margin:0 auto;min-height:100dvh;background:var(--paper);
     display:flex;flex-direction:column;position:relative;overflow-x:clip}
/* corporate header: white, one 1px sage hairline expressed as a shadow (spec S1.5) */
.bar{position:sticky;top:0;z-index:20;background:var(--white);
     padding:calc(env(safe-area-inset-top) + 10px) var(--s4) 10px;
     display:flex;align-items:center;gap:var(--s3);
     box-shadow:var(--sage) 0 1px 0}
.bar img{width:78px;height:51px;object-fit:contain;flex:0 0 auto}
.bar b{font:400 12px/16px var(--font-neutral);letter-spacing:1.8px;text-transform:uppercase;
       color:var(--gray-500)}
main{flex:1;padding:var(--s5) var(--s4) calc(env(safe-area-inset-bottom) + 40px)}
main:has(>.hero){padding-top:0}
/* full-bleed children of main escape the 16px gutter */
.bleed{margin-inline:calc(var(--s4) * -1)}

/* ============ THE CURVE (spec S9.1) ============
   (c) clip-path for full-bleed colour/image blocks. ONE amplitude at every
   width. There used to be a shallow `mobile` mask (0.05 of the block, 11.5px
   on a 230px block) and a deep `desktop` one (0.16, 36.8px), and the opening
   screen used both: the hero photo deep, the red block under it shallow. Two
   statements of the brand's signature device 230px apart, disagreeing by 3.2x,
   and the shallow one read as a failed render. The deep mask is the dome, and
   it is the only one; index.html now defines exactly one clipPath. */
.curve-b{clip-path:url(#curve-mask-bottom-desktop)}
/* (a) the viewBox="0 0 624 28" arc that sits on the top edge of a card footer.
   Deliberately wider than the card so its own rounded corners fall outside. */
.arc{position:absolute;bottom:99%;left:0;width:100%;height:16px;
     display:block;pointer-events:none}
.arc path{fill:var(--arcfill,var(--cream))}

/* ============ type (spec S2.3 - MomentsDisplay 700 for h1/h2 only,
   500 for card titles + control labels, MomentsText 400 for all else.
   NO weight 300, NO 600, NO bold body - those files do not exist.) ============ */
h1,h2{font-family:var(--font-primary);font-weight:700;color:var(--darkgray);margin:0 0 var(--s2);
      text-wrap:balance}
h1{font-size:28px;line-height:34px}
h2{font-size:28px;line-height:34px}
/* all three are tabindex="-1" programmatic focus targets, so no keyboard user
   loses a ring; #h-fin and #donetitle used to flash one while h1 did not */
h1:focus,#h-fin:focus,#donetitle:focus{outline:none}
h1.display{font-size:44px;line-height:50px;margin:0 0 var(--s3)}
h1.on-hero,h2.on-hero{color:var(--hero-ink)}
@media (min-width:768px){
  h1,h2{font-size:46px;line-height:52px}
  h1.display{font-size:72px;line-height:80px}
}
/* AugustScript: exactly once per site, 72px, cream, over an image (spec S2.4) */
.august{font-family:var(--font-secondary);font-weight:400;font-size:44px;line-height:30px;
        color:var(--cream);margin:0}
@media (min-width:768px){.august{font-size:72px;line-height:72px}}

.lede{font-size:16px;line-height:24px;color:var(--darkgray);margin:0 0 var(--s5);max-width:40ch}
@media (min-width:768px){.lede{font-size:20px;line-height:30px}}
/* eyebrow / meta. spec S11.5: no opacity:.7 on anything load-bearing. */
.meta{font-size:14px;line-height:20px;letter-spacing:1.4px;text-transform:uppercase;
      color:var(--gray-500);margin:0}
.sub{font-size:16px;line-height:24px;color:var(--gray-500);margin:0}

/* ============ progress ============ */
.prog{display:flex;gap:var(--s1);padding:var(--s3) var(--s4) 0;background:var(--white)}
.prog[hidden]{display:none}
.prog i{flex:1;height:4px;border-radius:var(--r-pill);background:var(--hairline);overflow:hidden;position:relative}
.prog i::after{content:"";position:absolute;inset:0;background:var(--primary);
  transform:scaleX(0);transform-origin:left;transition:transform .5s var(--ease)}
.prog i.on::after{transform:scaleX(1)}

/* ============ sandwich stack ============ */
/*
  2.5D "clay illustration" layer system. Ingredients are squashed-ellipse
  "pucks": a radial-gradient dome plus hard-edged stacked box-shadows faking an
  extruded side wall, overlapped via column-reverse + negative margins, sitting
  on a board that has its own edge thickness. No 3D transforms, no images.

  Scaling contract: the artwork is authored ONCE in a canonical 220x210 box and
  resized only by transform:scale() on .sw. box-shadow, border-radius and
  gradients all scale under a CSS transform, so the 52px thumbnail is a
  pixel-faithful copy of the hero, never a re-layout.
*/
.sw,.sw *{box-sizing:border-box}
.sw{--s:1;position:relative;width:220px;height:210px;
  flex-shrink:0;  /* every child is position:absolute, so this box has no
    intrinsic width; a flex parent would otherwise be
    free to shrink the real layout width toward 0 and break every left:50%
    child. The canonical 220px must stay literal - only scale() may resize it. */
  transform:scale(var(--s));transform-origin:50% 100%}

/* ---------- board ---------- */
.board{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);
  width:210px;height:88px;border-radius:20% / 50%;
  background:linear-gradient(150deg,#D89A52 0%,#BE7A3C 55%,#A4642E 100%);
  box-shadow:0 6px 0 0 #8D5426, 0 11px 0 0 #74431C,
             0 24px 20px -8px rgba(96,48,24,.32)}
.board::after{content:"";position:absolute;top:16%;right:12%;width:3.6%;height:8%;
  border-radius:50%;background:rgba(60,30,10,.35);
  box-shadow:inset 0 1px 1px rgba(0,0,0,.45)}

/* ---------- layer stack ---------- */
.layers{position:absolute;left:50%;bottom:68px;transform:translateX(-50%);
  width:100%;display:flex;flex-direction:column-reverse;align-items:center}
.ly{display:none}
.ly.on{display:block}

/* ---------- THE DROP ----------
   A newly picked layer falls onto the stack, squashes flat on impact and
   settles through one small rebound. Two deliberate choices:

   1. It animates the INDEPENDENT `translate` / `scale` properties, never
      `transform`. The scattered veg and crunch pieces carry their own
      `transform: rotate()`; an animation on `transform` wins the cascade and
      would flatten every rotation for the length of the drop and, with a
      forwards fill, permanently. translate/scale compose with transform
      instead, so a tilted chip falls in still tilted.
   2. Fill is `backwards`, not `both` - the delayed pieces hold their airborne
      pose during their delay instead of flashing at rest first, and the
      element's own declared transform comes back untouched at the end. */
@keyframes drop{
  0%  {translate:0 -38px;scale:.93 1.1;animation-timing-function:cubic-bezier(.45,0,.9,.55)}
  55% {translate:0 0;scale:1.09 .72;animation-timing-function:var(--ease-out)}
  76% {translate:0 -5px;scale:.985 1.05}
  100%{translate:0 0;scale:1}
}
/* a slab lands as one object; a scattered slot lands piece by piece, 55ms apart,
   because several things arriving in quick succession reads as food and one
   block appearing reads as a chart */
.ly.drop:not(:has(i)){animation:drop .46s var(--ease) backwards}
.ly.drop>i{animation:drop .4s var(--ease) backwards}
.ly.drop>i:nth-child(2){animation-delay:55ms}
.ly.drop>i:nth-child(3){animation-delay:110ms}
.ly.drop>i:nth-child(4){animation-delay:165ms}
.ly.drop>i:nth-child(5){animation-delay:220ms}
.ly.drop>i:nth-child(6){animation-delay:275ms}
/* the board takes the weight, timed to the impact frame at 55% of .46s */
@keyframes takeweight{0%,100%{scale:1}45%{scale:1.012 .955}}
.sw.impact .board{animation:takeweight .3s var(--ease) .2s}
/* before the first pick the board reads as waiting: a ghost of the base slice.
   ::before is the FIRST flex item of a column-reverse stack, i.e. the bottom
   one, so it lands exactly where the base bread will - same 190x40, same -26px. */
.sw.empty .layers::before{content:"";width:190px;height:40px;border-radius:50%;
  border:2px dashed var(--sage);margin-bottom:-26px}

/* generic "puck": squashed ellipse + extruded wall + soft contact shadow */
.puck{position:relative;width:var(--w);height:var(--h);border-radius:50%;
  background:radial-gradient(120% 140% at 32% 22%, var(--hi,rgba(255,255,255,.55)), transparent 60%),
             var(--fill);
  box-shadow:0 3px 0 0 var(--fill-dark), 0 6px 0 0 var(--fill-dark),
             0 9px 0 0 var(--fill-dark), 0 15px 10px -6px rgba(20,10,5,.35)}

/* ---------- bread ---------- */
.breadB{--w:190px;--h:40px;margin-bottom:-26px;z-index:1}
.breadT{--w:190px;--h:54px;margin-bottom:0;z-index:6}
.breadT::before{content:"";position:absolute;inset:0;border-radius:50%;
  border:5px solid var(--fill-dark);opacity:.5;
  -webkit-mask:linear-gradient(#000,transparent 55%);mask:linear-gradient(#000,transparent 55%)}
.breadB.white,.breadT.white{--fill:#f1dfb8;--fill-dark:#d8bd85;--hi:rgba(255,255,255,.7)}
.breadB.multi,.breadT.multi{--fill:#a9743f;--fill-dark:#82531f;--hi:rgba(255,235,200,.35)}
.breadB.sour,.breadT.sour{--fill:#d9ad63;--fill-dark:#b0813c;--hi:rgba(255,255,255,.5)}
.breadB.pav,.breadT.pav{--fill:#f3e2be;--fill-dark:#e0c48b;--hi:rgba(255,255,255,.8)}

/* ---------- mayo (thin drizzle) ---------- */
/* 178px is wider than the 172px filling that lands on top of it, so the mayo's
   rim survives step 3 instead of being swallowed. Width is safe to change:
   only --h is coupled to the hand-tuned overlap margins. */
.mayo{--w:190px;--h:14px;margin-bottom:-11px;z-index:2}
.mayo::after{content:"";position:absolute;inset:15% 8%;border-radius:50%;opacity:.8;
  background-image:repeating-linear-gradient(115deg, var(--drizzle) 0 3px, transparent 3px 12px)}
/* original + garlic are near-white and used to vanish against cream bread and
   the cream page. Legibility beats physical accuracy: both get a distinctly
   darker --fill-dark (which drives the extrusion) plus a thin outer ring, so
   the layer always shows an edge whatever sits above or below it. */
.mayo.orig{--fill:#f4ecd2;--fill-dark:#b39a5c;--drizzle:#fffaf0}
.mayo.burger{--fill:#f0d98a;--fill-dark:#cdb163;--drizzle:#fff3c4}
.mayo.garlic{--fill:#eef2df;--fill-dark:#93a06a;--drizzle:#ffffff}
.mayo.tandoori{--fill:#e07a3f;--fill-dark:#b0551f;--drizzle:#f7a35c}
.mayo.orig,.mayo.garlic{box-shadow:
  0 3px 0 0 var(--fill-dark), 0 6px 0 0 var(--fill-dark), 0 9px 0 0 var(--fill-dark),
  0 0 0 2px rgba(70,55,20,.5), 0 15px 10px -6px rgba(20,10,5,.35)}

/* ---------- filling (thick slab) ---------- */
.fill{--w:172px;--h:32px;margin-bottom:-22px;z-index:3}
.fill.aloo{--fill:#e8c15a;--fill-dark:#bd9636;--hi:rgba(255,255,240,.5)}
.fill.paneer{--fill:#f5edd4;--fill-dark:#d7c99f;--hi:rgba(255,255,255,.7)}
.fill.chicken{--fill:#d99a56;--fill-dark:#ab6f32;--hi:rgba(255,235,200,.45)}
.fill.veggies{--fill:#d3763f;--fill-dark:#a8531f;--hi:rgba(255,220,180,.4)}

/* ---------- veg (thin, multi-select, all four share one slot) ---------- */
.vslot{position:relative;width:178px;height:20px;margin-bottom:-17px;z-index:4}
.veg{position:absolute;inset:0;z-index:1}
/* the full-width leaf paints UNDER the scattered pieces, so tomato/onion/
   cucumber still read when lettuce is on */
.veg.lettuce{z-index:0}
.veg i{position:absolute;display:block}

.veg.lettuce i{left:0;top:-3px;width:178px;height:24px;
  border-radius:62% 38% 55% 45% / 48% 55% 45% 52%;
  background:radial-gradient(120% 140% at 30% 20%, rgba(255,255,255,.4), transparent 60%),
             linear-gradient(145deg,#8bcb52,#5fa233 70%);
  box-shadow:0 3px 0 0 #3f7a22, 0 10px 8px -4px rgba(20,30,5,.35)}
.veg.lettuce i::before{content:"";position:absolute;inset:-3px;border-radius:inherit;z-index:-1;
  background:repeating-radial-gradient(circle at 50% 50%, transparent 0 6px, rgba(95,162,51,.9) 7px 8px);
  -webkit-mask:radial-gradient(closest-side, transparent 92%, #000 94%);
  mask:radial-gradient(closest-side, transparent 92%, #000 94%)}

.veg.tomato i{left:51px;top:0;width:34px;height:16px;border-radius:50%;
  background:radial-gradient(120% 140% at 30% 25%, rgba(255,255,255,.75), transparent 55%),
             radial-gradient(circle at 50% 50%, #f2a5a0 0 30%, #e2453a 32% 78%, #b52c24 80%);
  box-shadow:0 2px 0 0 #922019, 0 5px 4px -2px rgba(20,0,0,.4)}
.veg.tomato i+i{left:92px;top:4px;width:29px;height:14px}

.veg.onion i{left:95px;top:-2px;width:30px;height:12px;border-radius:50%;
  background:repeating-radial-gradient(circle at 50% 50%, #f4e7ef 0 2px, #d7a9c6 2px 4px);
  box-shadow:0 2px 0 0 #b787a8, 0 4px 3px -2px rgba(30,10,20,.3)}

.veg.cucumber i{left:40px;top:2px;width:26px;height:12px;border-radius:50%;
  background:radial-gradient(120% 140% at 35% 25%, rgba(255,255,255,.6), transparent 60%),
             radial-gradient(circle at 50% 50%, #cdeab0 0 55%, #6fae4c 60% 82%, #4c8a34 84%);
  box-shadow:0 2px 0 0 #3c7028, 0 4px 3px -2px rgba(10,20,0,.3)}

/* ---------- crunch (irregular/scattered, multi-select) ---------- */
.cslot{position:relative;width:150px;height:10px;margin-bottom:-8px;z-index:5}
.crunch{position:absolute;inset:0}
.crunch i{position:absolute;display:block;background:var(--fill);
  border-radius:40% 60% 55% 45% / 55% 45% 55% 45%;
  box-shadow:0 1.5px 0 0 var(--fill-dark), 0 3px 3px -1px rgba(20,10,0,.3)}

.crunch.chips i{--fill:#e8c968;--fill-dark:#bd9c3d;width:16px;height:8px}
.crunch.chips i:nth-child(1){left:20px;top:0;transform:rotate(-8deg)}
.crunch.chips i:nth-child(2){left:55px;top:4px;transform:rotate(10deg)}
.crunch.chips i:nth-child(3){left:90px;top:-2px;transform:rotate(-4deg)}
.crunch.chips i:nth-child(4){left:118px;top:3px;transform:rotate(14deg)}

.crunch.nachos i{--fill:#e0a94c;--fill-dark:#b47f2c;width:14px;height:10px;
  border-radius:20% 20% 60% 60% / 20% 20% 70% 70%}
.crunch.nachos i:nth-child(1){left:12px;top:2px;transform:rotate(20deg)}
.crunch.nachos i:nth-child(2){left:50px;top:-3px;transform:rotate(-15deg)}
.crunch.nachos i:nth-child(3){left:84px;top:3px;transform:rotate(5deg)}
.crunch.nachos i:nth-child(4){left:115px;top:-1px;transform:rotate(-25deg)}

.crunch.friedonion i{--fill:#c9852f;--fill-dark:#9c621d;width:10px;height:5px;border-radius:50%}
.crunch.friedonion i:nth-child(1){left:18px;top:3px}
.crunch.friedonion i:nth-child(2){left:40px;top:-2px}
.crunch.friedonion i:nth-child(3){left:66px;top:4px}
.crunch.friedonion i:nth-child(4){left:92px;top:-1px}
.crunch.friedonion i:nth-child(5){left:118px;top:2px}

.crunch.namkeen i{--fill:#d9b24a;--fill-dark:#ac8a2f;width:6px;height:6px;border-radius:50%}
.crunch.namkeen i:nth-child(1){left:22px;top:2px}
.crunch.namkeen i:nth-child(2){left:38px;top:-3px}
.crunch.namkeen i:nth-child(3){left:60px;top:4px}
.crunch.namkeen i:nth-child(4){left:82px;top:-2px}
.crunch.namkeen i:nth-child(5){left:100px;top:3px}
.crunch.namkeen i:nth-child(6){left:122px;top:0}

/* ============ THE TWO-PART CTA (spec S3.1) ============
   110px box so the pill can be pulled up -50px and straddle the block above.
   Pill 50px R100 terracotta; ring 40px 1px white inset 5px, pointer-events:none.
   Nobody else's buttons look like this. */
.cta{display:flex;align-items:center;justify-content:center;
     position:relative;width:100%;max-width:250px;height:110px;min-width:150px;
     border:0;background:none;padding:0;color:inherit;font:inherit;
     -webkit-appearance:none;appearance:none;
     transition:scale 120ms var(--ease)}
/* THE PRESS CONTRACT (see Motion). Press is the independent `scale` property,
   never `transform`, so it composes with a card's resting off-centre scale
   instead of replacing it. 120ms on --ease. Whole control, not the pill: the
   ring is an inset child with pointer-events:none and scaling only the pill
   slides it inside a stationary ring. */
.cta:not(:disabled):active{scale:.985}
.cta .ring{position:absolute;z-index:2;pointer-events:none;
  border:1px solid var(--primary-ink);border-radius:var(--r-pill);
  width:calc(100% - 10px);height:40px;top:50%;left:50%;transform:translate(-50%,-50%)}
.cta .pill{display:block;position:relative;width:100%;height:50px;line-height:50px;text-align:center;
  background:var(--primary);color:var(--primary-ink);border-radius:var(--r-pill);
  text-transform:uppercase;font-family:var(--font-neutral);font-weight:400;
  font-size:12px;letter-spacing:1.8px;
  transition:background .15s var(--ease)}
@media (min-width:768px){.cta .pill{font-size:14px;letter-spacing:2.1px}}
@media (hover:hover){.cta:hover .pill{background:var(--primary-hover)}}
.cta:disabled .pill{background:var(--hairline);color:var(--gray-500)}
.cta:disabled .ring{border-color:var(--gray-500)}
.cta .spin{display:inline-block;vertical-align:-3px;margin-right:6px;
  width:15px;height:15px;border:2px solid rgba(255,255,255,.4);border-top-color:var(--white);
  border-radius:var(--r-pill);animation:sp .7s linear infinite}
@keyframes sp{to{transform:rotate(360deg)}}
.ctas{display:flex;flex-direction:column;align-items:center;gap:0}

/* ============ text link - the growing underline (spec S3.3).
   The system has NO outline/ghost button; this IS the secondary action.
   44px min target because we fix rather than inherit the site's a11y debt. ============ */
.tlink{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:var(--s2);
  min-height:44px;padding:0 var(--s2);border:0;background:none;color:var(--darkgray);
  font-family:var(--font-neutral);font-weight:400;font-size:16px;line-height:1;
  letter-spacing:1.6px;text-transform:uppercase;
  /* .tlink never reset this, so the <a> instance carried the UA's default
     underline in --darkgray while the <button> instance (reset, in the
     confirmation) had none: two renderings of one component. Worse, it made the
     brand's grow-on-hover underline draw a SECOND, terracotta line under a link
     that already looked underlined, which is the whole affordance lost. */
  text-decoration:none}
/* The growing underline is brand-mandated (DESIGN.md 650, 757: one of the three
   things hover may do), so the BEHAVIOUR stays and only the implementation
   changes. It used to transition `width` and `left` — both layout properties,
   so every frame relaid out and repainted instead of staying on the compositor,
   on the most-used affordance in the finale and on the mid-range Android this
   campaign actually ships to. Geometry is now static at its final size and the
   growth is a scaleX from the left edge: visually identical, compositor-only.
   Same hook .prog i already uses at line 148. */
.tlink::after{content:"";position:absolute;display:block;left:1%;width:98%;bottom:8px;
  border-bottom:1px solid var(--primary);
  transform:scaleX(0);transform-origin:left;
  transition:transform .25s var(--ease)}
.tlink:focus-visible::after{transform:scaleX(1)}
@media (hover:hover){.tlink:hover::after{transform:scaleX(1)}}

/* ============ THE SEAL (spec S9.2) - 8-lobed mark ============ */
.seal{position:relative;width:var(--sz,54px);height:var(--sz,54px);flex:0 0 auto;
      display:flex;align-items:center;justify-content:center}
.seal svg{position:absolute;top:0;left:0;width:100%;height:100%}
.seal .disc{fill:var(--disc,var(--campaign))}
.seal .lobe{fill:none;stroke:var(--lobe,#fff);stroke-width:var(--sw,1);stroke-linejoin:round}
.seal b{position:relative;z-index:1;font-family:var(--font-primary);font-weight:700;
        color:var(--lobe,#fff);font-size:22px;line-height:1;text-align:center}
.seal.lg{--sz:84px;--sw:2}
.seal.lg b{font-size:19px;line-height:23px;letter-spacing:.5px;text-transform:uppercase}

/* mechanic: seal-numbered steps (spec S10 move 8) */
.mech{position:relative;--arcfill:var(--campaign);background:var(--campaign);color:var(--white);
  padding:var(--s6) var(--s4) var(--s7);margin-top:var(--s7)}
.mech h2{color:var(--white)}
.mech ol{list-style:none;margin:var(--s5) 0 0;padding:0;display:flex;flex-direction:column;gap:var(--s4)}
.mech li{display:flex;align-items:center;gap:var(--s4)}
/* inside a red block the seal is line art only - the "numbered recipe step"
   variant of the mark (spec S9.2 case 1), not the filled badge. */
.mech .seal{--disc:transparent}
/* white on #ED1C24 is 4.38:1 -> legal only at >=19px/700. This is 19px/700. */
.mech li p{margin:0;font-family:var(--font-primary);font-weight:700;font-size:19px;
  line-height:26px;color:var(--white);text-wrap:balance}

/* ============ CARD GRAMMAR (spec S3.6/S3.7) ============
   Image or packshot on top, warm footer below, arc on the footer's top edge,
   footer fill alternating cream/gold across the grid. Zero shadows. */
/* flex column + flex:1 on the footer so every tile in a grid row ends at the
   same baseline when one name wraps to two lines and its neighbours do not */
.opt{position:relative;border:0;background:none;padding:0;text-align:left;color:var(--darkgray);
  border-radius:var(--r-card);overflow:hidden;display:flex;flex-direction:column;
  transition:transform .4s var(--ease),scale 120ms var(--ease)}
/* a flat swatch carries no information beyond its colour, so it is sized to be
   recognisable and no larger: 84px, against 120px for the packshot step, whose
   jars are the one place real product art appears and do deserve the room. */
.opt .swatch{flex:0 0 auto;height:84px;border-radius:var(--r-card) var(--r-card) 0 0;position:relative;
  background:var(--swatch);display:grid;place-items:center;overflow:hidden}
.opt .swatch img{height:116px;width:auto;object-fit:contain;
  transition:transform .4s var(--ease)}
@media (hover:hover){.opt:hover .swatch img{transform:scale(.88)}}
.opt .foot{display:block;position:relative;background:var(--arcfill);padding:8px 12px 12px;
  border-radius:0 0 var(--r-card) var(--r-card);min-height:44px;flex:1 1 auto}
/* The arc is filled --arcfill, which IS the footer colour, so on a tile whose
   swatch sits close to its own footer the brand's signature device disappears:
   bread/white measured 1.04:1 swatch-to-footer, crunch/namkeen 1.09, nachos
   1.14, chips 1.19, filling/aloo 1.27. A 1px sage edge means the curve always
   reads whatever the two fills do. Same call DESIGN.md already records one step
   out, where the near-white mayos gained a --fill-dark and a ring: legibility
   over physical accuracy. non-scaling-stroke because the arc is
   preserveAspectRatio="none" and would otherwise render the stroke thinner
   horizontally (0.28px) than vertically (0.57px). */
.opt .foot .arc path{stroke:var(--sage);stroke-width:1;vector-effect:non-scaling-stroke}
.opt .name{display:block;font-family:var(--font-primary);font-weight:500;font-size:20px;
  line-height:26px;letter-spacing:.6px;color:var(--darkgray)}
.opt .tick{position:absolute;top:8px;right:8px;width:26px;height:26px;border-radius:var(--r-pill);
  background:var(--campaign);color:var(--white);display:none;place-items:center;z-index:3}
.opt[aria-pressed=true] .tick{display:grid}
.opt[aria-pressed=true]{outline:2px solid var(--primary);outline-offset:-2px}
.opt:active{scale:.985}

/* ============ presets - recipe-card variant, real dish photography ============ */
.presets{display:flex;gap:var(--s3);overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:var(--s4);
  margin:0 calc(var(--s4) * -1);padding:var(--s2) var(--s4) var(--s5);scrollbar-width:none}
.presets::-webkit-scrollbar{display:none}
.preset{scroll-snap-align:start;flex:0 0 172px;border:0;background:none;padding:0;
  border-radius:var(--r-card);overflow:hidden;text-align:left;color:var(--darkgray);
  transition:scale 120ms var(--ease)}
.preset .shot{display:block;position:relative;height:120px;overflow:hidden;
  border-radius:var(--r-card) var(--r-card) 0 0;background:var(--cream)}
.preset .shot img{width:100%;height:100%;object-fit:cover;
  transition:transform .4s var(--ease)}
@media (hover:hover){.preset:hover .shot img{transform:scale(1.06)}}
.preset .foot{display:block;position:relative;background:var(--arcfill);padding:10px 14px 18px;min-height:76px;
  border-radius:0 0 var(--r-card) var(--r-card)}
.preset b{display:block;font-family:var(--font-primary);font-weight:500;font-size:20px;
  line-height:26px;letter-spacing:.6px}
.preset span{display:block;font-size:14px;line-height:20px;color:var(--darkgray);margin-top:2px}
.preset:active{scale:.97}
.presets .preset:nth-child(odd){--arcfill:var(--cream)}
.presets .preset:nth-child(even){--arcfill:var(--gold)}

/* ============ hero blocks ============ */
.hero{position:relative;z-index:1;background:var(--cream);overflow:hidden}
.hero img{width:100%;height:230px;object-fit:cover}
/* MEASURED: cream #EFDBB2 over this photograph, even behind a 33% scrim, is
   2.13:1 average and 1.08:1 at its worst pixel. AugustScript therefore sits on
   the campaign red field instead of on the photo - 3.22:1, and large text.
   It is still the one script line per page the spec allows (S2.4). */
.heroblock .august{margin-bottom:var(--s2)}
.heroblock{background:var(--hero-fill);color:var(--hero-ink);padding:var(--s6) var(--s4) var(--s7)}

/* ============ cards ============ */
.card{position:relative;background:var(--white);border-radius:var(--r-card);
  border:1px solid var(--sage);padding:var(--s5) var(--s4) var(--s5)}
.code{font-family:var(--font-neutral);font-size:14px;letter-spacing:2.1px;
  text-transform:uppercase;color:var(--darkgray)}
.tags{display:flex;flex-wrap:wrap;gap:var(--s2);margin-top:var(--s4);justify-content:center}
.tag{background:var(--white);border:1px solid var(--sage);border-radius:var(--r-pill);
  padding:6px 14px;font-size:14px;line-height:20px;letter-spacing:1.4px;text-transform:uppercase;
  color:var(--darkgray)}
.note{background:var(--beige);border-radius:var(--r-card);
  padding:var(--s4);font-size:16px;line-height:24px;color:var(--darkgray);margin-top:var(--s4);
  display:flex;gap:var(--s3)}
.note svg{flex:0 0 auto;margin-top:3px}
.note b{font-family:var(--font-primary);font-weight:500}


/* ============ form - pill input, a documented extension (spec S11.4) ============ */
.field{margin-top:var(--s5)}
.field label{display:block;font-family:var(--font-primary);font-weight:500;font-size:20px;
  line-height:26px;margin-bottom:var(--s2)}
.field input{width:100%;min-height:52px;padding:0 var(--s5);border-radius:var(--r-pill);
  border:1px solid var(--sage);background:var(--white);
  font:400 16px var(--font-neutral);color:var(--darkgray)}
.field input::placeholder{color:var(--gray-500)}
.field input:focus{border-color:var(--primary);outline:2px solid var(--primary);outline-offset:1px}
.field input[aria-invalid=true]{border-color:var(--err);border-width:2px}
.help{font-size:14px;line-height:20px;color:var(--gray-500);margin:var(--s2) 0 0;padding-left:var(--s5)}
.err{font-size:14px;line-height:20px;color:var(--err);margin:var(--s2) 0 0;padding-left:var(--s5);
  display:flex;gap:var(--s2);align-items:center}

.center{text-align:center}
.mt5{margin-top:var(--s5)}
.fine{display:block;text-align:center;color:var(--gray-500);font-size:14px;line-height:20px;
  letter-spacing:1.4px;text-transform:uppercase;padding:var(--s5) var(--s4) 0}


/* ============================================================================
   THE KITCHEN — one continuously scrolling document
   ----------------------------------------------------------------------------
   Vertical scroll = progression through the recipe. Horizontal scroll = choosing
   within a step. Nothing is scroll-jacked: no wheel/touchmove listener exists in
   this file. The page RESPONDS to native scroll via scroll-snap, position:sticky
   and IntersectionObserver, and never fights it.
   ============================================================================ */

/* --barH is measured from the real header in JS (safe-area makes it variable).
   --counterH is the reserved height of the sticky worktop; every .step pads its
   own top by exactly that much, which is why the rail can never be occluded at
   the snap position. Change one and the other must follow. */
:root{ --barH:62px; --slabH:52px; --counterH:clamp(252px,38svh,340px); --cardW:172px }

html{scroll-snap-type:y proximity;scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
main{padding:0 var(--s4) calc(env(safe-area-inset-bottom) + 40px)}
/* .finale is in this list for the same reason as the rest, and it is the one that
   was missing. Without it .closing was the LAST snap target in the document, so
   past the closing beat the user was free-flicking with nothing marking the spot:
   at the closing snap the finale starts 48px below the fold at both 375x667 and
   375x812, which measured as 0px of plate on screen — the payoff for the whole
   scroll cropped out of frame. Reaching it took another 644px of momentum that
   nothing asked for. Snapping it to --barH lands the plate flush at the top of
   the content column instead. Safe here specifically because the counter is a
   whole .closing behind by this point, so the snap cannot pull the two sandwiches
   back onto one screen — confirmed by the 1px overlap sweep, still 0 frames. */
.opening,.starters,.howto,.step,.closing,.finale{scroll-snap-align:start;scroll-margin-top:var(--barH)}

/* ---------- the opening ---------- */
/* the one deliberate overlap on this screen (The One Overlap Rule) */
.opening .heroblock{margin-top:-38px;padding-top:var(--s6)}
/* the vertical gesture, taught once: a drip running down off the counter edge */
.cue{display:flex;flex-direction:column;align-items:center;gap:6px;
  color:var(--primary);padding:var(--s6) 0 var(--s5)}
.cue .lbl{font-size:12px;letter-spacing:1.8px;text-transform:uppercase;color:var(--gray-500)}
.cue .ln{width:1px;height:30px;background:var(--primary)}
/* The arrow is drawn inside a 24x24 icon box but its glyph starts 8.6px down,
   so the 6px flex gap rendered as a 14.6px hole between the line and the arrow
   while the label sat 6px above the line. Pull the box up by its own internal
   padding and the two gaps read the same. */
.cue svg{display:block;margin-top:-9px}
@media (prefers-reduced-motion:no-preference){
  .cue svg{animation:cuedown 2.6s var(--ease) infinite}
}
@keyframes cuedown{0%,62%,100%{translate:0}78%{translate:0 6px}}

.starters,.howto{padding-top:var(--s6)}

/* ---------- the worktop ----------
   .counter is the sticky scene; .plate is the same surface, not sticky, used
   once at the finale so the handoff from the sticky sandwich to the plated one
   lands on identical geometry instead of a jump cut. */
.counter,.plate{position:relative;height:var(--counterH);
  margin-inline:calc(var(--s4) * -1);background:var(--paper)}
.counter{position:sticky;top:var(--barH);z-index:8}
/* HISTORY, so it is not reintroduced. Steps used to reserve --counterH as their
   own padding-top while the sticky counter ALSO occupied --counterH of flow, so
   the band was paid for twice and the first rail started one blank screen below
   the worktop. The fix then was to pull `.counter + .step` up by --counterH.
   Both the padding and the pull are gone now — see `.stepin` below, which pins
   the content to the counter's lip instead of reserving a band under it, so the
   counter's flow height is paid for exactly once and there is nothing to cancel.
   If a pull is ever needed again it goes on the STEP as margin-top and NEVER as
   a negative margin-bottom on .counter, which looks equivalent and is not: a
   sticky element is clamped to its containing block by its MARGIN box, so a
   -counterH bottom margin buys the counter another --counterH of stick past the
   end of .build. That measured as 205px of scroll with both sandwiches on screen
   at 375x667. */
.counter .air,.plate .air{position:absolute;left:0;right:0;top:0;bottom:var(--slabH);
  background:radial-gradient(54% 64% at 50% 100%, var(--beige) 0%, rgba(247,233,213,0) 72%)}
/* the worktop slab. Its top edge IS the brand arc — the counter's front lip and
   the card-footer curve are the same device, which is the whole point. */
.slab{position:absolute;left:0;right:0;bottom:0;height:var(--slabH);--arcfill:var(--cream);
  background:linear-gradient(180deg,var(--cream) 0%,var(--gold) 100%)}
.counter .sw,.plate .sw{position:absolute;left:50%;bottom:calc(var(--slabH) - 6px);
  --s:.86;transform:translateX(-50%) scale(var(--s));transform-origin:50% 100%;z-index:2}
/* progress: five tracks let into the worktop, terracotta fill (3.79:1 on cream,
   clears the 3:1 non-text threshold). Replaces the old header rail. */
.prog{position:absolute;left:0;right:0;bottom:11px;z-index:3;
  display:flex;gap:var(--s1);padding:0 var(--s5);background:none}
.prog[hidden]{display:none}

/* ---------- a step ----------
   padding-top === --counterH, so at the snap position the rail sits exactly
   below the sticky worktop. Between snap positions the outgoing rail slides up
   BEHIND the worktop, which is opaque — the scene occludes it for free. */
/* No justify-content here on purpose (flex-start). Centring the content in the
   band below the padding pushed it away from the counter's lip on any viewport
   tall enough to leave slack — a measured 64px of paper between the worktop and
   the step heading at 375x900. The content belongs against the counter it was
   just served from; the slack collects at the foot of the step, which is scroll
   run, not a gap at rest. Desktop re-centres it, where the worktop is its own
   full-height column and centring is the right answer. */
/* The second term of the max() is load-bearing twice over, and dropping it
   regresses both things at 375x667. Removing the old padding-top let the step
   collapse onto its min-height of (100svh - --barH) = 596px, and:
   1. the pinned block no longer fits between the counter's lip and the step's
      own bottom edge, so sticky clamps it ABOVE the lip — measured 11px of the
      heading hidden behind the worktop at the snap;
   2. 596px is under 0.909 x 667 = 606px, which is the floor the one-live-step
      IntersectionObserver needs. Measured two live steps at once at y=2176.
   The step therefore has to stay at least as tall as the counter plus the block
   it has to hold. 400px is the block (measured 354px) with slack for a name that
   wraps. On every other tested viewport (100svh - --barH) is already the larger
   term, so this changes nothing there and the observer headroom is untouched:
   +47px at 375x667, 2.9px at 375x812 and 10.9px at 375x900, both unchanged. */
.step{min-height:max(calc(100svh - var(--barH)),calc(var(--counterH) + 400px));
  padding-top:0;display:flex;flex-direction:column}

/* ---------- .stepin: why the void is gone ----------
   The step reserved --counterH of padding so the sticky worktop had a band to
   paint over. At the snap that band is exactly covered and it looks right; the
   trouble is that `scroll-snap-type: y proximity` lets the user rest ANYWHERE,
   and off the snap the band is just visible cream. Measured worst case: 292px
   of nothing at 375x667, 432px at 375x812, 489px at 375x900 — with the heading
   shoved to the bottom of the screen and the rail below the fold. Note the gap
   is (step top in viewport - --barH), i.e. purely the distance from the snap:
   it does not depend on --counterH, so shrinking the worktop buys nothing.

   So: no reserved band. The step's content is one block pinned to the worktop's
   lip instead. Scrolling down past a snap, the outgoing step's content stays
   pinned there, clamped by its own step box, so its bottom edge rides up to
   exactly where the incoming step's content top is arriving — the two are
   contiguous and the screen below the counter is never empty. The residual is
   only the step's own bottom slack at the snap itself, ~10% of the viewport.

   The step box keeps min-height:calc(100svh - --barH) deliberately: the
   IntersectionObserver's one-live-step guarantee needs every step to stay above
   ~0.909x viewport, and only its padding changed, never its height.

   ESCAPE HATCH, one line: if two sticky elements at different offsets misbehave
   on a real iOS Safari, put `padding-top:var(--counterH)` back on .step above.
   That returns the layout to exactly its previous, known-good geometry whatever
   sticky does — it only gives the off-snap void back. It is safe on its own;
   do not also reinstate a pull on `.counter + .step`.

   position:sticky survives `.app{overflow-x:clip}` overhead because `clip` is
   not a scroll container — and .counter, one level up the same chain, already
   proves it. */
.stepin{position:sticky;top:calc(var(--barH) + var(--counterH))}
.steph{margin:0 0 var(--s1);font-size:28px;line-height:34px}
.step .meta{margin-bottom:var(--s2)}

/* ---------- the prep tray ----------
   Full-bleed and horizontally padding-free: .rail's centring padding is
   calc(50% - --cardW/2), and that 50% resolves against the tray's content box.
   Put inline padding on the tray and every rail mis-centres. */
.tray{position:relative;margin-inline:calc(var(--s4) * -1);padding:var(--s4) 0 var(--s5);
  margin-top:var(--s5);background:var(--white);--arcfill:var(--white)}
.railwrap{position:relative}
.rail{position:relative;display:flex;gap:var(--s3);overflow-x:auto;
  scroll-snap-type:x mandatory;
  /* the whole nested-scroll contract: horizontal overscroll never chains to the
     page, and touch-action stays `auto` so the browser keeps its own axis lock —
     setting pan-x here would eat a diagonal thumb swipe's vertical component. */
  overscroll-behavior-x:contain;
  padding-inline:calc(50% - var(--cardW) / 2);
  scrollbar-width:none}
.rail::-webkit-scrollbar{display:none}
.rail .opt{flex:0 0 var(--cardW);scroll-snap-align:center;
  transform:scale(.9);transition:transform .25s var(--ease),scale 120ms var(--ease)}
.rail .opt.at{transform:scale(1)}
/* the independent `scale` property composes with the transform above instead of
   replacing it, so a press does not cancel the off-centre scale */
.rail .opt:active{scale:.97}
.rail .opt .foot{min-height:84px}
.rail .opt .swatch{height:120px}
.rail .opt .swatch img{height:116px}
.rail .opt:nth-child(odd){--arcfill:var(--cream)}
.rail .opt:nth-child(even){--arcfill:var(--gold)}
.rail .opt[aria-checked=true],.rail .opt[aria-pressed=true]{outline:2px solid var(--primary);outline-offset:-2px}
.rail .opt[aria-checked=true] .tick{display:grid}

/* the chip: ONE component for both grammars. On a single-select step it reads
   the state that centring already committed; on a multi step it names the one
   action the card offers. Visibility, not display, so nothing reflows mid-scroll. */
.opt .chip{display:inline-flex;align-items:center;gap:5px;margin-top:6px;visibility:hidden;
  border-radius:var(--r-pill);border:1px solid var(--sage);background:var(--white);
  color:var(--darkgray);padding:3px 10px;font-family:var(--font-neutral);font-weight:400;
  font-size:12px;line-height:18px;letter-spacing:1.8px;text-transform:uppercase}
.rail .opt.at .chip,.opt[aria-pressed=true] .chip,.opt[aria-checked=true] .chip{visibility:visible}
.opt[aria-pressed=true] .chip,.opt[aria-checked=true] .chip{
  background:var(--primary);border-color:var(--primary);color:var(--primary-ink)}
.opt .chip .ck{display:none;line-height:0}
.opt[aria-pressed=true] .chip .ck,.opt[aria-checked=true] .chip .ck{display:block}

/* themed arrow hints. The system's small round control — white, 1px sage, 44px,
   no shadow — and they are real paging buttons, not decoration. The glyph is the
   brand's own bow with a knife tip on it, never a generic chevron. */
.nudge{position:absolute;top:50%;transform:translateY(-50%);z-index:4;
  width:44px;height:44px;border-radius:var(--r-pill);padding:0;
  background:var(--white);border:1px solid var(--sage);color:var(--primary);
  display:grid;place-items:center;
  transition:background .15s var(--ease),color .15s var(--ease),scale 120ms var(--ease)}
.nudge:not(:disabled):active{scale:.97}
.nudge.prev{left:4px}
.nudge.next{right:4px}
@media (hover:hover){.nudge:not(:disabled):hover{background:var(--cream)}}
.nudge:disabled{color:var(--sage);border-color:var(--hairline)}
@media (prefers-reduced-motion:no-preference){
  .railwrap:not(.touched) .nudge.next:not(:disabled){animation:swipehint 2.8s var(--ease) infinite}
}
@keyframes swipehint{0%,64%,100%{translate:0}78%{translate:5px}90%{translate:0}}
.hint{margin:var(--s4) var(--s4) 0;text-align:center;font-size:14px;line-height:20px;
  color:var(--gray-500)}

/* the beat between the last ingredient and the plate, and the reason it is a
   WHOLE screen rather than a comfortable pause.
   Let d be the distance from the viewport top to the plate. The counter is on
   screen while d > --barH (above that the opaque header covers it); the plate is
   on screen while d < the viewport height. Both are therefore true for every d
   between the two, at every viewport, unless a full viewport of run separates
   the end of the counter's sticky container from the top of the plate. Shorter
   than that and the handoff cannot exist — which is why this beat also has to
   sit OUTSIDE .build: inside it, it only lengthened the counter's sticky range
   and the plate still began the instant the counter's container ended.
   lvh, NOT svh, and the difference is the whole guarantee. svh is the viewport
   with the mobile toolbars SHOWING — the smallest it gets. Size the beat off that
   and the moment the toolbars retract the visible viewport is taller than the
   separation and both sandwiches are back on screen. lvh is the tallest the
   viewport can get, so the beat is >= the visible viewport at every toolbar
   state: exactly one viewport retracted, more than one while the toolbars show.
   The + --s7 is deliberate slack on that boundary. One viewport of separation is
   the minimum and lands the crossover on the exact pixel where the counter's
   bottom meets --barH; sub-pixel layout and fractional DPR can round either way
   there. 48px more costs nothing, and the run carries type rather than air.

   It used to spend that whole screen on one centred line, which is why the
   client screenshotted it as more empty space: a 24px line in the middle of
   875px of cream is 400px of nothing above it and 400px below. The height is
   not negotiable, so the beat has to carry something — and what belongs here is
   the METHOD, the making read back as a recipe in cooking voice. Deliberately
   not the five picks as labels: the finale one screen later already shows the
   generated name, the code and a pill per pick, and repeating that here would
   be the same information twice in a row, which is its own kind of filler.
   This closes the making; the finale opens the result.
   Laid out with space-evenly rather than centred, so the lines distribute down
   the screen they were given instead of huddling in the middle of it. */
/* The beat still fills a screen, because the counter has to clear the viewport
   before the plate arrives. What it must NOT do is distribute its children to
   the edges of that screen: space-evenly on a 100lvh box pushed the heading
   and the five steps into separate islands, measured at 75.5px of dead air
   between 40px rows on a 390x844 phone. The group is centred and spaced from
   the scale instead, so the rhythm is the same on every device height. */
.closing{min-height:calc(100lvh - var(--barH) + var(--s7));
  display:flex;flex-direction:column;justify-content:center;gap:var(--s7);
  padding:var(--s7) var(--s4);text-align:center}
.closing p{margin:0;font-family:var(--font-primary);font-weight:500;font-size:24px;
  line-height:30px;letter-spacing:.72px;color:var(--darkgray)}
/* the method. Same seal-numbered grammar as "How It Works", which is the brand's
   own device for an ordered recipe — there in white line art on red, here as the
   filled mark on paper. */
.method{list-style:none;margin:0;padding:0;width:100%;max-width:340px;
  align-self:center;display:flex;flex-direction:column;gap:var(--s5);
  text-align:left}
.method li{display:flex;align-items:center;gap:var(--s4)}
.method .seal{--sz:40px}
.method .seal b{font-size:17px}
.method p{font-size:20px;line-height:26px;letter-spacing:.6px;text-wrap:balance}

/* ---------- the finale ---------- */
.finale{padding-bottom:var(--s7)}
/* an h3 (it is subordinate to h2#h-fin), styled to carry across everything the
   h1,h2 rule gave it: the display face, the colour and the balance */
.minor{font-family:var(--font-primary);font-weight:500;font-size:24px;line-height:30px;
  letter-spacing:.72px;margin:var(--s7) 0 var(--s2);color:var(--darkgray);text-wrap:balance}
.finale .plate .sw{--s:.86}
.fieldset{border:0;margin:0;padding:0;min-width:0}
.pillrow{display:flex;align-items:center;background:var(--white);border:1px solid var(--sage);
  border-radius:var(--r-pill);min-height:52px;padding-left:var(--s5)}
.pillrow:focus-within{border-color:var(--primary);outline:2px solid var(--primary);outline-offset:1px}
.pillrow[data-invalid=true]{border-color:var(--err);border-width:2px}
.pillrow .pfx{font-size:16px;color:var(--gray-500);flex:0 0 auto}
.pillrow input{flex:1;min-width:0;border:0;background:none;min-height:50px;padding:0 var(--s5) 0 var(--s2);
  font:400 16px var(--font-neutral);color:var(--darkgray)}
.pillrow input:focus{outline:none}
/* consent: 44px target, never pre-ticked (DPDP wants a real act of consent) */
.consent{display:flex;gap:var(--s3);align-items:flex-start;margin-top:var(--s5);
  padding:var(--s4);background:var(--beige);border-radius:var(--r-card)}
.consent input{flex:0 0 auto;width:24px;height:24px;margin:10px 0 0;accent-color:var(--primary)}
.consent label{font-size:16px;line-height:24px;color:var(--darkgray);min-height:44px;
  display:flex;align-items:center}
.consent[data-invalid=true]{outline:2px solid var(--err);outline-offset:2px}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
/* ============ scroll reveal (spec S8.3) - 10vh rise, 50/200/400/600/800 stagger ============ */
@media (prefers-reduced-motion:no-preference){
  .rv{opacity:0;transform:translateY(10vh);will-change:opacity,transform}
  .rv.in{opacity:1;transform:none;
    transition:opacity 600ms var(--ease-out) 50ms,transform 700ms var(--ease-out)}
    /* the reveal owns transform here; press lives on `scale` and carries its
     own 120ms leg, or the press would animate over the reveal's 700ms */
  .mech li,.presets .preset{transition:opacity 600ms var(--ease-out),transform 700ms var(--ease-out),scale 120ms var(--ease)}
  .mech:not(.in) li,.starters:not(.in) .preset{opacity:0;transform:translateY(10vh)}
  .mech.in li:nth-child(2),.starters.in .preset:nth-child(2){transition-delay:200ms}
  .mech.in li:nth-child(3),.starters.in .preset:nth-child(3){transition-delay:400ms}
  .mech.in li:nth-child(4),.starters.in .preset:nth-child(4){transition-delay:600ms}
  .mech.in li:nth-child(5),.starters.in .preset:nth-child(5){transition-delay:800ms}
}


/* ============ responsive — three layouts, one tree, CSS only ============ */
@media (min-width:481px){
  :root{--cardW:200px;--counterH:clamp(280px,38svh,380px);--slabH:60px}
  .app{max-width:700px}
  .hero img{height:300px}
  .counter .sw,.plate .sw{--s:.95}
}
@media (min-width:1025px){
  :root{--cardW:220px;--slabH:64px}
  .app{max-width:1120px}
  .hero img{height:420px}
  /* the worktop moves to its own column and the recipe scrolls beside it.
     The counter's grid area spans every step row, so it is tall enough to stay
     stuck for the whole build without any JS. */
  .build{display:grid;grid-template-columns:420px minmax(0,1fr);column-gap:var(--s7);align-items:start}
  .counter{grid-column:1;grid-row:1 / span 6;margin-inline:0;
    height:calc(100svh - var(--barH) - var(--s5));top:calc(var(--barH) + var(--s5));
    border-radius:var(--r-card);overflow:hidden}
  .counter .sw{--s:1.45}
  .step{grid-column:2;min-width:0;padding-top:0;justify-content:center}
  /* nothing to pin against here: the worktop is its own full-height column, so
     the step content is never under it and has no lip to hold on to. */
  .stepin{position:static}
  .tray{margin-inline:0;border-radius:var(--r-card)}
  .finale{display:grid;grid-template-columns:420px minmax(0,1fr);column-gap:var(--s7);align-items:start}
  .finale .plate{grid-column:1;grid-row:1 / span 4;margin-inline:0;position:sticky;
    top:calc(var(--barH) + var(--s5));height:calc(100svh - var(--barH) - var(--s5));
    border-radius:var(--r-card);overflow:hidden}
  .finale .plate .sw{--s:1.45}
  .finale>:not(.plate){grid-column:2;min-width:0}
  .finale .minor:first-of-type{margin-top:var(--s5)}
  .mech ol{flex-direction:row}
  .mech li{flex:1}
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}
  /* a snap that animates is still a snap that moves under a reduced-motion
     user's finger; kill the smoothing, keep the snapping */
  html{scroll-behavior:auto}
  .rail{scroll-behavior:auto}
}

/* ============ short viewports: landscape phones and the 568px class ============
   --counterH's 252px FLOOR does not scale down, and .stepin is sticky at
   (--barH + --counterH) with a ~374px block under it. Measured at rest:
   375x480 pushed 197px of that block below the fold and 320x568 pushed 129px —
   in both cases the option card's name and its "On the board" chip, i.e. the
   entire readable payload of the control, were off-screen at the snap position.

   The floor is NOT lowered globally, because .step's min-height
   max(100svh - --barH, --counterH + 400px) leans on the second term to stay
   above the IntersectionObserver's 0.909x-viewport one-live-step threshold:
   at 320x568 the first term is only 497px against a 516px requirement, so the
   652px from the second term is what holds the guarantee. Keeping --counterH
   at 180px keeps that term at 580px — 1.02x viewport at 568, 1.21x at 480,
   both still clear. Verified by measurement at all nine tested sizes.

   The worktop shrinks through --s, the same scale hook the width breakpoints
   already drive, so the 2.5D renderer is
   untouched. */
@media (max-height:660px){
  :root{--counterH:clamp(180px,34svh,230px)}
  .counter .sw,.plate .sw{--s:.62}
  .steph{font-size:22px;line-height:26px}
  .step .meta{margin-bottom:var(--s1)}
  .tray{margin-top:var(--s3);padding:var(--s3) 0 var(--s4)}
  .rail .opt .swatch{height:76px}
  .rail .opt .swatch img{height:72px}
  .rail .opt .foot{min-height:68px}
  .hint{margin-top:var(--s2)}
}

/* ============ WebGL stack ============
   The canvas takes the whole worktop rather than living inside .sw's 220x210
   box: .sw is scaled by --s from a 50% 100% origin, and a canvas inside that
   would be resampled rather than rendered at size. The CSS stack stays in the
   DOM underneath: it is what the page falls back to if the context is lost. */
.glstage{position:absolute;inset:0;z-index:2}
.glstage canvas{display:block;width:100%;height:100%}
.counter.gl > .sw,.plate.gl > .sw{display:none}


/* Skip link: the first focusable thing in the document, invisible until it has
   focus. The builder is a long scroll narrative, so a keyboard user reaching
   the page should be able to jump the header rather than tab through it. */
.skip{position:absolute;left:var(--s4);top:var(--s4);z-index:100;
  transform:translateY(-200%);
  background:var(--primary);color:var(--primary-ink);
  padding:var(--s3) var(--s4);border-radius:var(--r-pill);
  font-family:var(--font-neutral);font-size:14px;text-decoration:none;
  transition:transform .18s var(--ease)}
/* :focus, not :focus-visible. A skip link is reached only by keyboard, and
   :focus-visible does not match a programmatic focus() call, which is exactly
   how an assistive tool may land on it. */
.skip:focus{transform:translateY(0);outline:2px solid var(--darkgray);outline-offset:2px}
