/* ============================================================
   La Selezione — stylesheet (packaging-led edition)
   The striped meal-kit boxes lead the visuals. Colours are
   sampled straight from the box art and used as BIG SOLID
   FIELDS, with the candy stripe kept only as an accent edge.
   Each flavour carries its own INK (the type colour printed
   on that box): navy on turquoise, green on pink, red on yellow.
   Display Lora · body Montserrat · script Parisienne.
   ============================================================ */

:root{
  /* base */
  --panna:      #FCF7EE;   /* cream background          */
  --panna-2:    #F5ECD8;   /* warm parchment band       */
  --notte:      #16306B;   /* brand navy                */
  --notte-deep: #0F2247;   /* dramatic dark act         */
  /* The navy stood down — for a heading that is deliberately NOT the one you
     are meant to read yet. It exists because the order tracker had been
     saying "later" with opacity, and opacity is the one way of saying it that
     cannot be made legible: at .38 a pending step's heading measured 2.2:1
     and its description 1.7:1, and every value bright enough to pass also
     stopped reading as pending. This says the same thing in a colour —
     5.1:1 against the tracker's white card, against --notte's 12.6:1, so the
     gap between "done" and "not yet" is a contrast gap you can still read. */
  --notte-soft: #5A6E96;
  --oro:        #B08D57;   /* gold — decorative use only */
  /* Gold darkened until it can carry small type, and re-tuned 2026-09-02:
     4.9:1 on cream was the number this was set to, and the ground it missed
     was --panna-2, where the same eyebrow measured 4.43:1 — under AA on the
     one band the page puts it on most. Now 5.2 / 5.5 / 4.7 on cream,
     --white-soft and parchment respectively, which is the whole set. */
  --oro-ink:    #806434;
  --oro-soft:   #CBA86F;

  /* ---- CLAIM GOLD ----
     The claim mark's own ramp, and deliberately NOT --oro. The three
     decorative golds above were never meant to carry knockout type:
     --white-soft measures 2.2:1 on --oro-soft and 3.0:1 on --oro, so
     until 2026-08-23 the pill making the site's one scoped claim was
     the least legible thing on the card — and it is the only thing
     telling three near-identical cards apart.

     Note what the fix could NOT be. The old ramp ran from L*0.42 to
     L*0.15, and nothing survives that: white fails at the light end,
     navy (5.6:1 on --oro-soft) fails at the dark end at 4.1:1. The
     range itself was the bug, so this is the same hue held inside one
     band dark enough for white throughout — 4.8:1 at the lit corner to
     8.4:1 at the deep one, still a ~2.3x sweep, so it reads as struck
     metal and not as flat paint.

     Chroma is held high rather than let down toward brown. Dropping
     lightness the lazy way desaturates with it, and the first ramp that
     passed here (#8F6E44…) read as bronze — against the carbonara
     card's turquoise, simultaneous contrast pulled it olive. Same hue
     (~35°), same safe lightness band, more chroma: it reads as gold on
     all three flavour fields, and gained margin doing it.

     Its own tokens rather than a reach for --oro-ink, on the same rule
     the status colours follow: re-tuning gold-as-text on cream must not
     silently repaint the claim on every shop card. Deeper than the
     decorative gold also keeps the claim from reading as one more pale
     status pill next to the delivery verdict — it is product truth,
     not system feedback, and the two should not look alike. */
  --claim-lit:  #94692B;   /* 4.8:1 under --white-soft */
  --claim-mid:  #7E5822;   /* 6.2:1 */
  --claim-deep: #67461A;   /* 8.4:1 */
  --ink:        #45413A;   /* warm body text            */
  /* Body text stood down one step, and a token rather than an opacity on
     purpose. --ink at .70 and at .75 is what the account page's empty-state
     and order dates were using, which rendered 4.1:1 and 4.7:1 — one under
     AA, one a rounding error above it, and neither knowable from reading the
     rule. An alpha is contrast measured against whatever happens to be
     behind it; this is 5.2:1 on cream and 5.5:1 on white, and stays that on
     both. */
  --ink-soft:   #6D6861;
  --line:       #E6DAC0;   /* hairline on parchment     */
  --white-soft: #FFFDF7;

  /* ---- PACKAGING COLOURS (sampled from the boxes) ----
     FIELD = the box's solid stripe colour, used as big colour fields
     INK   = the type colour printed on that box                      */
  --carbonara:        #0E8A90;   /* deep teal — accent on neutral bg */
  --carbonara-field:  #74CFD0;   /* turquoise field   */
  --carbonara-ink:    #233A80;   /* navy type (box)   */
  --carbonara-deep:   #0B6E73;
  /* The teal's ink, and the last member of a family the other two colours
     already had. --oro has --oro-ink and --pomodoro has a darkened --ink of
     its own, both for the same reason: a colour sampled off a printed FIELD
     is chosen to be seen across a whole panel, and lands at 3.5–4.1:1 when
     it is asked to be 14px of type on cream instead. --carbonara never got
     one, so the current-page marker, the signed-in account link and every
     link in the Terms were all sitting at 3.9:1.

     It is NOT called --carbonara-ink, because that name is taken and means
     something else here — the navy actually printed on the carbonara box.
     Equal to --carbonara-deep by value and not by intent, exactly as --ok-ink
     is equal to --bolognese-ink: one is a dark field that white sits on, this
     is type on a light one, and re-tuning either must not silently move the
     other. The value is not a guess — held at --carbonara's own hue (200.4°)
     it sits at 96% of the in-gamut chroma ceiling for that lightness, so
     there is no darker teal that is any more teal. 5.6:1 on cream, 5.9:1 on
     --white-soft, 5.1:1 on parchment. */
  --carbonara-text:   #0B6E73;

  --bolognese:        #2A5236;   /* forest-green type (box) */
  --bolognese-field:  #F0CEDA;   /* candy-pink field  */
  --bolognese-ink:    #2A5236;
  --bolognese-deep:   #1F4B30;

  --pomodoro:         #B81E22;   /* tomato-red type (box) */
  --pomodoro-field:   #F4D24A;   /* sunny-yellow field */
  --pomodoro-ink:     #AF1B1F;   /* darkened: 4.7:1 on the candy band */
  --pomodoro-deep:    #9A1A1E;

  /* ---- STATUS / FEEDBACK ----
     The delivery verdict and the form's inline notes speak in two colours: a
     green that means "yes, we cover this" and an amber that means "not an
     answer yet / not on our routes". Each is a ground, a hairline and an ink,
     reused across the coverage panel, the order-form line, the map's dropped
     pin, the address note and the stockists redirect — so they live here once
     instead of as the same hex copied into a dozen rules. --ok-ink shares
     bolognese's green by eye, not by meaning: kept a separate token so a
     repaint of either flavour or feedback can't leak into the other. */
  --ok-bg:       #F1F7F0;   /* covered / confirmed ground */
  --ok-border:   #9CC7A6;
  --ok-ink:      #2A5236;   /* == --bolognese-ink by value, not by intent */
  --warn-bg:     #FCF5E4;   /* off-route — the answer is no */
  --warn-border: #E2C98C;
  --warn-ink:    #6B5320;
  /* A THIRD voice, and the only cool one on a warm site — which is the
     point of it. "We could not place that address" is neither a yes nor
     a no, and it was wearing the amber until 2026-08-23, so a state the
     map fixes in one tap read as a refusal. The obvious neutral is the
     cream, and the cream is what defeated it: --panna #FCF7EE against
     --warn-bg #FCF5E4 is a two-point difference in one channel, so the
     two boxes were the same warm beige at a glance. This is the brand
     navy at a tint — off the warm scale entirely, unmistakable beside
     either verdict, and still the site's own colour rather than a grey
     borrowed from somewhere else. */
  --ask-bg:      #EFF1F5;   /* not an answer yet — working, or couldn't place it */
  --ask-border:  #CBD1DE;
  --ask-ink:     #2A4A86;   /* lifted navy, so :hover to --notte still reads as a darkening */

  /* ---- FORM RHYTHM ----
     The first spacing tokens in this file, and they exist because the
     order form had exactly one interval: every label carried
     margin-bottom:14px, so "phone → e-mail" (same question) and
     "e-mail → street" (different question entirely) were separated by
     the same 14px and nothing in the spacing said which fields belonged
     together. Eight fields read as one undifferentiated ladder.

     Three steps, each double the last, so the contrast is doing the
     grouping rather than a heading:
       BIND  — a label to its own input. Was an effective 10px against a
               14px field gap: a 1.4:1 ratio, under the ~2:1 where a
               label stops looking equally attached to the box above it.
       FIELD — field to field inside one group. Unchanged, so the form
               keeps the density it was tuned at.
       GROUP — group to group. Opened at exactly two seams (after the
               e-mail, before the commit block), which is what turns one
               ladder into who-you-are / where-it-goes / agree-and-pay.
     Net height is about +4px: the eight tightened binds pay for both
     seams, on a panel that is already taller than most viewports. */
  --form-bind:  4px;    /* + ~3px leading = 7px optical */
  --form-field: 14px;
  --form-group: 28px;

  /* active flavour (overridden per section/page) */
  --accent: var(--carbonara);
  --field:  var(--carbonara-field);
  --field-deep: var(--carbonara-deep);
  /* --accent dark enough for a cream or white label to sit ON it. Every
     block that remaps --accent remaps this beside it, and that is the point:
     .btn--solid's hover swaps its background to the section's accent, so the
     one place --accent was left at its root value — a FIELD colour — the
     site's primary CTA went from 11.9:1 at rest to 3.9:1 under the pointer.
     A button getting three times harder to read the moment you reach for it
     is the wrong direction for a hover to travel. If you add a flavour or a
     new accented component, set this too; a missing one falls back to teal
     and will paint a bolognese button green-to-teal on hover. */
  --accent-deep: var(--carbonara-deep);

  /* ---- THE LABEL ROLE ----
     One typographic device does more work on this site than any other: a
     short uppercase line in tracked Montserrat, sitting above a heading or
     in front of a form field. It was being set at SEVEN sizes — .56, .58,
     .62, .64, .66, .70, .72rem — for that one job, which is 8.96px to
     11.52px, steps of a third of a pixel apart. Nothing was distinguishable
     by them; they were just the size each component happened to be given.

     Two steps, and both are the values the design already reached for most
     (nine rules at .70, nineteen at .78), so this consolidates toward the
     centre of mass rather than inventing a ramp. The floor is .70rem =
     11.2px, which is also the legibility floor for functional text, so the
     six labels that were under it come up in the same move.

     NOT applied to the printed panels on the 3D box (.boxprint__*,
     .boxstamp__*), which run from 2.5px to 5.5px on purpose: those are a
     scale reproduction of the packaging's legal print, aria-hidden and
     pointer-events:none, and setting them in a legible size would make the
     model stop being a model. See the SIDE PRINT block. */
  --label-sm: .70rem;   /* 11.2px — cards, chrome, tight scenes */
  --label:    .78rem;   /* 12.48px — form fields, section eyebrows */

  --display: "Lora", Georgia, serif;
  --body: "Montserrat", "Segoe UI", system-ui, sans-serif;
  --script: "Parisienne", "Lora", cursive;

  --maxw: 1180px;

  /* The page gutter, and a token because it was written out four times and
     one of those copies was silently winning. The safe-area block near the
     bottom of this file re-declares padding-left/right on .container and
     .header-inner as calc(<this value> + env(...)); that block is a plain
     class selector like the ≤420px rule that tightens the gutter to 16px,
     and it comes LATER in the file — so on every browser that supports
     env() (all of them since 2017) the small-phone gutter has never
     actually applied. Named once here, overridden per breakpoint, and the
     safe-area block adds the inset ON TOP of whatever this currently is,
     so a breakpoint's intent survives the addition. */
  --gutter: clamp(20px,5vw,48px);

  /* ---- motion ----
     One vocabulary for the whole site so nothing feels out of step.
     --ease      general purpose (the original curve, kept)
     --ease-out  snappy deceleration for anything the pointer triggers
     --ease-soft material-ish curve for panels sliding in and out
     Durations: hover/press feedback should land in ~.2s, entrances in
     ~.35s, full-section reveals in ~.5s. Anything slower reads as lag. */
  --ease: cubic-bezier(.2,.7,.2,1);
  --ease-out: cubic-bezier(.22,1,.36,1);
  --ease-soft: cubic-bezier(.4,0,.2,1);
  --t-fast: .18s;
  --t-med:  .3s;

  --stripe-w: 30px;   /* candy-stripe band width */

  /* "this turns" — two arcs chasing each other. Carried as a mask so
     the cue can be a plain translatable <span> on the products page and
     a bare pseudo-element on the shop cards, both inheriting the
     surrounding flavour's ink. Lives up here because those two live in
     different subtrees. */
  --b-turn-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' d='M20,12a8,8,0,0,1-13.4,5.9'/%3E%3Cpath fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' d='M4,12a8,8,0,0,1,13.4-5.9'/%3E%3Cpath fill='%23000' d='M17.9,2.4l.9,4.6-4.6.6z'/%3E%3Cpath fill='%23000' d='M6.1,21.6l-.9-4.6,4.6-.6z'/%3E%3C/svg%3E");

  /* ---- the header's two glyphs ----
     Carried as MASKS, not as images, for the same reason --b-turn-icon
     is: the shop pill and the account link each have four states
     (rest, hover, current, signed-in) and the mask is tinted by
     currentColor, so one token covers all of them and the colour is
     decided by the same rule that decides the label's. A second copy
     of the artwork per state is how one of them ends up the wrong
     navy. Drawn as strokes at the weight Montserrat 700 sets beside
     them, so the glyph reads as a letterform's sibling rather than as
     an icon pasted in from a set. */
  --nav-bag-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-width='1.9' stroke-linejoin='round' d='M4.6 8h14.8l-1.15 11.2a1.6 1.6 0 0 1-1.6 1.4H7.35a1.6 1.6 0 0 1-1.6-1.4Z'/%3E%3Cpath fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' d='M8.6 10.4V6.9a3.4 3.4 0 0 1 6.8 0v3.5'/%3E%3C/svg%3E");
  --nav-user-icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='8' r='3.6' fill='none' stroke='%23000' stroke-width='1.9'/%3E%3Cpath fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' d='M4.8 20.2a7.2 7.2 0 0 1 14.4 0'/%3E%3C/svg%3E");
}

/* ---------- reset ---------- */
*,*::before,*::after{ box-sizing:border-box; }
html{ scroll-behavior:smooth; -webkit-text-size-adjust:100%; text-size-adjust:100%; }
/* In-page anchor targets clear the fixed header instead of landing underneath
   it — #values from the hero cue, #videos from the footer, #main from the skip
   link. (Deliberately not on article[id]: the shop cards are deep-linked with
   scrollIntoView block:"center", which does its own framing.) */
:target, section[id], main[id]{ scroll-margin-top:96px; }
body{
  margin:0; background:var(--panna); color:var(--ink);
  font-family:var(--body); font-weight:400; line-height:1.6; font-size:17px;
  -webkit-font-smoothing:antialiased; overflow-x:hidden;
}
body::before{   /* paper grain over everything */
  content:""; position:fixed; inset:0; z-index:1; pointer-events:none;
  background-image:url("assets/grain.png"); background-size:160px; opacity:.5;
}
/* height:auto is what makes the width/height attributes on <img> safe: without
   it the height attribute acts as a presentational hint and every box renders
   at its literal pixel height, ignoring the width the layout actually gave it.
   With it, the attributes only supply the intrinsic ratio — which is the point
   (the browser reserves the right space before the PNG loads, no layout jump).
   .qr-card img sets its own fixed height and outranks this. */
img{ max-width:100%; height:auto; display:block; }
/* Long unbroken strings break out of their box, and on this site they are
   NOT hypothetical: Photon returns Sofia street names we did not author into
   the address dropdown, delivery_zones can hand us a district under its own
   words, and an order row carries a customer's e-mail. Bulgarian compounds
   are long, the containers are narrow, and body{overflow-x:hidden} would
   have CLIPPED the overflow rather than revealed it — so the failure mode
   was silent truncation with no scrollbar to notice it by. Scoped to the
   elements that actually receive foreign text rather than applied globally,
   because break-word on every element also breaks headings mid-syllable. */
.addr-suggest__addr, .addr-suggest__meta,
.shop-summary__list li, .track-items li, .stockist__addr,
.orders__list li, .order__items, #profileHi, .form-note, .shop-zone{
  overflow-wrap:break-word; word-break:break-word; min-width:0;
}
a{ color:inherit; text-decoration:none; }

/* branded text selection + scrollbar */
::selection{ background:var(--carbonara-field); color:var(--notte-deep); }
html{ scrollbar-color:var(--oro-soft) var(--panna-2); }
::-webkit-scrollbar{ width:12px; }
::-webkit-scrollbar-track{ background:var(--panna-2); }
::-webkit-scrollbar-thumb{ background:var(--oro-soft); border-radius:6px; border:3px solid var(--panna-2); }
::-webkit-scrollbar-thumb:hover{ background:var(--oro); }
ul,ol{ margin:0; padding:0; list-style:none; }
/* Form controls do not inherit font-family — a <button> that never sets
   one gets the UA's own stack, which in Chrome is Arial. Ten controls on
   /shop alone were rendering in it, including the − and + on every
   quantity stepper, an inch from a price set in Lora. Every component
   that remembered to set a family is unaffected by this; the point is
   that the next one no longer has to remember. */
button, input, select, textarea{ font-family:inherit; }
h1,h2,h3{ margin:0; font-family:var(--display); color:var(--notte); font-weight:600; line-height:1.04; text-wrap:balance; }
em{ font-style:italic; }

.container{ width:100%; max-width:var(--maxw); margin-inline:auto; padding-inline:var(--gutter); }

/* visually hidden, still read by screen readers and still focusable */
.sr-only{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
  clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0; }

/* ---------- skip link ----------
   First thing in the tab order; parked off-screen until it takes focus, so
   a keyboard user isn't forced through the whole header on every page. */
.skip-link{ position:absolute; left:12px; top:-64px; z-index:210;
  padding:.7em 1.3em; border-radius:0 0 12px 12px; background:var(--notte); color:var(--panna);
  font-family:var(--body); font-size:.85rem; font-weight:700; letter-spacing:.02em;
  transition:top var(--t-fast) var(--ease-out); }
.skip-link:focus{ top:0; }
#main:focus{ outline:none; }

/* ---------- header cart indicator ----------
   Injected by script.js on every page; hidden entirely while the basket is
   empty so it never reads as an empty-state nag. */
/* Solid navy since 2026-09-10 — see THE HEADER'S HIERARCHY below for
   why. It was a white chip with a --line border, which is the colour
   of the hairline under the header, so a basket with three boxes in it
   was the quietest object in a row that included a ringed sign-in
   button. It is now the loudest thing on the page, and it can afford
   to be: it is `hidden` outright while the basket is empty, so nobody
   who has not already put something in it ever sees it. */
.cart-pill{ display:inline-flex; align-items:center; gap:7px; padding:8px 9px 8px 12px;
  border:2px solid var(--notte); border-radius:999px; background:var(--notte); color:var(--panna);
  font-family:var(--body); font-weight:700; font-size:.82rem; line-height:1;
  box-shadow:0 10px 22px -14px rgba(22,48,107,.8);
  transition:background var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out),
             transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out); }
.cart-pill:hover{ background:var(--notte-deep); border-color:var(--notte-deep); color:var(--panna);
  transform:translateY(-2px); box-shadow:0 16px 30px -14px rgba(22,48,107,.9); }
.cart-pill:active{ transform:translateY(0) scale(.96); }
/* Empty. Gone above 1024px, where the nav's own "Магазин" pill is the
   way in; kept below it, where the nav is in the drawer and this is
   the only store affordance the header has. See renderCart(). */
.cart-pill.is-empty{ display:none; }
.cart-pill[hidden]{ display:none; }
/* The count is the only number in the header, so it gets the only gold
   ground. --oro-soft under --notte is 5.6:1 — the pairing the CLAIM
   GOLD note above measured when it was ruling out the lighter golds
   for knockout white; navy on it passes where white did not. */
.cart-pill__n{ min-width:1.55em; height:1.55em; padding:0 .35em; border-radius:999px;
  display:inline-flex; align-items:center; justify-content:center;
  background:var(--oro-soft); color:var(--notte); font-size:.78rem; text-align:center; }
/* The basket answers when something lands in it. One 380ms squash —
   the same gesture a physical thing makes when you drop something in
   — and it is the ONLY confirmation a visitor gets that a click at the
   bottom of the page changed something pinned to the top of it.
   script.js adds .is-bumped for the length of the animation and takes
   it off on animationend, so a second box added quickly re-plays it
   rather than being swallowed by an animation already running. */
.cart-pill.is-bumped{ animation:cartBump .38s var(--ease-out); }
.cart-pill.is-bumped .cart-pill__n{ animation:cartCount .38s var(--ease-out); }
@keyframes cartBump{
  0%{ transform:scale(1); } 32%{ transform:scale(1.12); }
  62%{ transform:scale(.97); } 100%{ transform:scale(1); } }
/* background-COLOR, not the `background` shorthand: the shorthand also
   resets background-image and the rest of the family on every keyframe,
   which is more for the compositor to reconcile than a colour needs. */
@keyframes cartCount{
  0%{ transform:scale(1); background-color:var(--oro-soft); }
  32%{ transform:scale(1.3); background-color:var(--panna); }
  100%{ transform:scale(1); background-color:var(--oro-soft); } }

/* ---------- shared bits ---------- */
.eyebrow{ font-size:var(--label-sm); letter-spacing:.32em; text-transform:uppercase; color:var(--oro-ink); font-weight:700; margin:0 0 1.1rem; }
.eyebrow--light{ color:var(--oro-soft); }
.section-title{ font-size:clamp(2rem,5vw,3.4rem); margin-bottom:.4em; }
.section-title em{ color:var(--accent); }
.section-sub{ max-width:48ch; color:var(--ink); opacity:.9; margin:0 0 2rem; font-size:1.04rem; }
.section-sub, .page-head p, .hero__lead, .cta-strip p, .feedback__lead{ text-wrap:pretty; }

.btn{ font-family:var(--body); font-weight:700; cursor:pointer; border:none; }
/* justify-content:center is a no-op while the pill is shrink-wrapped; it only bites
   when something forces width (full-width mobile buttons), which is exactly where
   text-align:center cannot reach an anonymous flex item. */
.btn--pill{ position:relative; overflow:hidden; display:inline-flex; align-items:center; justify-content:center; gap:.5em; padding:.9em 1.8em; border-radius:999px; font-size:.92rem; letter-spacing:.02em; transition:transform var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out); }
/* light sweep across every pill on hover */
.btn--pill::after{ content:""; position:absolute; top:0; left:-60%; width:40%; height:100%; background:linear-gradient(100deg, transparent, rgba(255,255,255,.4), transparent); transform:translateX(0) skewX(-20deg); pointer-events:none; }
.btn--pill:hover:not(:disabled)::after{ animation:sheen .5s var(--ease-out); }
/* translateX not left: same sweep (475% of the 40%-wide sheen == 190% of the
   button, matching the old -60%→130%), composited instead of relaid-out */
@keyframes sheen{ from{ transform:translateX(0) skewX(-20deg); } to{ transform:translateX(475%) skewX(-20deg); } }
.btn--pill:active{ transform:translateY(0) scale(.97); }
.btn--solid{ background:var(--notte); color:var(--panna); box-shadow:0 12px 26px -12px rgba(22,48,107,.7); }
/* --accent-deep, not --accent: see the token's note. The gesture is unchanged
   — the button still picks up the flavour of the section it sits in — it just
   picks up a value a cream label survives. */
.btn--solid:hover:not(:disabled){ transform:translateY(-2px); background:var(--accent-deep); box-shadow:0 16px 30px -12px rgba(0,0,0,.4); }
.btn--solid:active{ transform:translateY(0); }
.btn--field{ background:var(--field-deep); color:#fff; box-shadow:0 12px 26px -12px rgba(0,0,0,.5); }
.btn--field:hover:not(:disabled){ transform:translateY(-2px); filter:brightness(1.06); }

.link-arrow{ font-weight:700; color:var(--notte); position:relative; padding-bottom:2px;
  background-image:linear-gradient(var(--accent),var(--accent)); background-size:0% 2px; background-repeat:no-repeat; background-position:0 100%; transition:background-size var(--t-med) var(--ease-out); }
.link-arrow::after{ content:" →"; color:var(--accent); display:inline-block; transition:transform var(--t-med) var(--ease-out); }
.link-arrow:hover{ background-size:100% 2px; }
.link-arrow:hover::after{ transform:translateX(4px); }

/* ---- the candy ribbon: a thin tri-colour accent edge ---- */
.ribbon{
  height:10px;
  background:repeating-linear-gradient(90deg,
    var(--carbonara-field) 0 46px,
    var(--bolognese-field)  46px 92px,
    var(--pomodoro-field)   92px 138px);
}

/* a full candy-stripe surface (vertical, like the box) — used sparingly */
.candy{
  background:repeating-linear-gradient(90deg,
    var(--field) 0 var(--stripe-w),
    var(--white-soft) var(--stripe-w) calc(var(--stripe-w) * 2));
}

/* ============================================================ HEADER */
/* will-change is held permanently here, and that is deliberate — it is
   the one place on the site that does NOT follow the toggle-it-around-
   the-interaction rule .mobile-nav documents below, so it is worth
   saying why rather than leaving it to read as an oversight.

   The difference is layer cost against how often the layer is needed.
   .mobile-nav is a full-viewport panel that is open for seconds of a
   visit; promoting it permanently reserves a screen-sized buffer for
   something almost never on screen. The header is a thin strip that is
   ALWAYS on screen — about 375x76 at mobile, so roughly half a
   megabyte of GPU at dpr 2 — and it translates on every scroll
   direction change past 300px. Creating that layer on the first frame
   of each hide is exactly the hitch will-change exists to prevent.
   Measured: the page promotes two elements in total, this and the
   progress hairline, which script.js rewrites every frame. */
.site-header{ position:fixed; inset:0 0 auto 0; z-index:60; will-change:transform;
  transition:background var(--t-med) var(--ease-out), box-shadow var(--t-med) var(--ease-out), transform .32s var(--ease-soft); }
.site-header.is-hidden{ transform:translateY(-100%); }
/* reading-progress hairline along the header's bottom edge.
   script.js writes scaleX on every frame, so it must not also transition —
   the transition would lag a frame behind the scroll and read as rubber-banding. */
.scroll-progress{ position:absolute; left:0; bottom:-3px; height:3px; width:100%; background:linear-gradient(90deg, var(--oro), var(--oro-soft)); transform:scaleX(0); transform-origin:0 50%; pointer-events:none; will-change:transform; }
.header-top{ height:6px;
  background:repeating-linear-gradient(90deg,
    var(--carbonara-field) 0 40px,
    var(--bolognese-field)  40px 80px,
    var(--pomodoro-field)   80px 120px); }
.header-inner{ max-width:var(--maxw); margin-inline:auto; padding:14px var(--gutter); display:flex; align-items:center; gap:22px; }
.site-header.is-scrolled{ background:rgba(252,247,238,.92); -webkit-backdrop-filter:saturate(150%) blur(10px); backdrop-filter:saturate(150%) blur(10px); box-shadow:0 1px 0 var(--line); }
/* interior pages: keep the header readable from the top (the home hero stays transparent until scroll) */
body:not([data-page="home"]) .site-header{ background:rgba(252,247,238,.94); -webkit-backdrop-filter:saturate(150%) blur(10px); backdrop-filter:saturate(150%) blur(10px); box-shadow:0 1px 0 var(--line); }

.wordmark{ display:flex; flex-direction:column; align-items:center; line-height:1; margin-right:auto; }
.wordmark__name{ font-family:var(--display); font-style:italic; font-weight:600; font-size:1.55rem; color:var(--notte); }
.wordmark__sub{ font-family:var(--script); font-size:1.1rem; color:var(--oro-ink); margin-top:.1em; line-height:1; }

.nav{ display:flex; gap:26px; }
.nav a{ font-size:.86rem; font-weight:600; letter-spacing:.02em; color:var(--notte); position:relative; padding:4px 0; white-space:nowrap; }
.nav a::after{ content:""; position:absolute; left:0; bottom:-2px; height:2px; width:100%; background:var(--carbonara-field); transform:scaleX(0); transform-origin:left; transition:transform var(--t-med) var(--ease-out); }
.nav a:hover::after, .nav a:focus-visible::after{ transform:scaleX(1); }
.nav a.is-current{ color:var(--carbonara-text); }
.nav a.is-current::after{ transform:scaleX(1); background:var(--carbonara); }
.nav__account.is-authed{ color:var(--carbonara-text); font-weight:700; }
.header-inner .nav__account{ max-width:14ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

.lang{ display:inline-flex; align-items:center; gap:7px; color:var(--notte); }
/* the language switch sits apart from the nav, behind its own divider */
.lang--split{ border-left:1.5px solid var(--line); padding-left:18px; }

/* ---- the header language switch is a small dropdown ---- */
.lang-menu{ position:relative; }
.lang-menu__btn{ display:inline-flex; align-items:center; gap:6px; background:none; border:none; cursor:pointer; font-family:var(--body); font-weight:700; font-size:.78rem; letter-spacing:.06em; color:var(--notte); padding:6px 2px; }
.lang-menu__btn svg{ opacity:.55; transition:transform var(--t-fast) var(--ease-out); }
.lang-menu.is-open .lang-menu__btn svg{ transform:rotate(180deg); }
.lang-menu__list{ position:absolute; top:calc(100% + 10px); right:0; min-width:150px; background:var(--white-soft); border:1px solid var(--line); border-radius:14px; box-shadow:0 24px 48px -24px rgba(14,40,80,.45); padding:6px; z-index:70; transform-origin:100% 0; animation:menuIn .18s var(--ease-out); }
@keyframes menuIn{ from{ opacity:0; transform:translateY(-6px) scale(.97); } to{ opacity:1; transform:none; } }
.lang-menu__list[hidden]{ display:none; }
.lang-menu__list .lang__btn{ display:block; width:100%; text-align:left; padding:9px 12px; border-radius:9px; font-size:.8rem; letter-spacing:.03em; opacity:.75; text-decoration:none;
  transition:background var(--t-fast) var(--ease-out), opacity var(--t-fast) var(--ease-out); }
.lang-menu__list .lang__btn:hover{ background:var(--panna-2); opacity:1; }
.lang-menu__list .lang__btn.is-active{ opacity:1; background:var(--panna-2); text-decoration:none; }
.lang-menu__list .lang__btn.is-active::after{ content:"✓"; float:right; color:var(--oro-ink); }
/* The "/" between BG and EN in the mobile drawer, and ONLY that.
   Written as `.lang span` it also caught #langMenuLabel — the header
   dropdown's own label — because that is a <span> inside .lang too. So
   the two letters telling a visitor which language they are currently
   reading the site in were rendering at 30% opacity: 1.79:1 against the
   cream, against the 4.5:1 the rest of this file is audited to, and the
   least legible text on the site. The chevron beside it is dimmed
   deliberately and separately (.lang-menu__btn svg, opacity .55), which
   is what says this one was not meant to be. Child combinator as well
   as the drawer scope, so a future span nested inside a drawer button
   cannot inherit it by accident either. */
.lang--drawer > span{ opacity:.3; }
.lang__btn{ background:none; border:none; cursor:pointer; color:var(--notte); opacity:.42; font-family:var(--body); font-weight:700; font-size:.78rem; letter-spacing:.06em; padding:2px; transition:opacity var(--t-fast) var(--ease-out); }
.lang__btn:hover{ opacity:.8; }
.lang__btn.is-active{ opacity:1; text-decoration:underline; text-decoration-color:var(--oro-ink); text-underline-offset:4px; text-decoration-thickness:2px; }

/* ============================================================ THE HEADER'S HIERARCHY
   Until 2026-09-10 the loudest object in the header of a SHOP was the
   sign-in button. "Вход" carried a 2px navy ring and pill padding;
   "Магазин" was one of five identical text links, indistinguishable
   from "За нас"; and the basket — when it had anything in it at all —
   was a white chip with a hairline the colour of the page. The ladder
   ran account > everything > the two controls that take money.

   It runs the other way now, in three steps rather than two, because
   the header has three different jobs to rank and they change places
   depending on whether the basket is empty:

     BASKET (.cart-pill)     solid navy, gold count. Loudest, and only
                             on screen at all when something is waiting
                             in it — so it never shouts at a first-time
                             visitor, and is unmissable to a returning
                             one. It is also the ONLY one of the three
                             that survives below 1024px, where the nav
                             folds into the drawer.
     STORE  (nav → /shop)    gold-ringed pill. The strongest thing on
                             screen while the basket is empty, which is
                             most first visits.
     ACCOUNT (.nav__cta)     nav-weight text with a small figure beside
                             it. Still first in the eye's sweep to the
                             right-hand end, still the only glyph of its
                             kind, no longer competing with the till.

   The two glyphs are masks tinted by currentColor rather than <img> or
   inline SVG: currentColor is what carries them through the hover, the
   signed-in state and the current-page state without a second copy of
   the artwork per state, and it keeps the markup — which is repeated
   across twelve hand-written HTML files — untouched by all of this. */
.nav a[data-page="shop"]{
  display:inline-flex; align-items:center; gap:.45em;
  padding:.52em 1.05em .52em .92em; border-radius:999px;
  border:2px solid var(--oro-soft); background:var(--white-soft);
  color:var(--notte); font-weight:700;
  box-shadow:0 8px 18px -14px rgba(22,48,107,.55);
  transition:transform var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out),
             color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out),
             box-shadow var(--t-fast) var(--ease-out); }
.nav a[data-page="shop"]::before{
  content:""; width:1.05em; height:1.05em; flex:none;
  background:currentColor; -webkit-mask:var(--nav-bag-icon) center/contain no-repeat;
  mask:var(--nav-bag-icon) center/contain no-repeat; }
/* the sliding underline is the text links' affordance and would draw
   across the bottom of a pill; the pill has its own */
.nav a[data-page="shop"]::after{ display:none; }
.nav a[data-page="shop"]:hover, .nav a[data-page="shop"]:focus-visible{
  background:var(--notte); border-color:var(--notte); color:var(--panna);
  transform:translateY(-2px); box-shadow:0 14px 26px -14px rgba(22,48,107,.75); }
/* you are already here: filled, and it stops lifting */
.nav a[data-page="shop"].is-current{ background:var(--notte); border-color:var(--notte); color:var(--panna); }
.nav a[data-page="shop"].is-current:hover{ transform:none; }

/* ---- the account link steps down to nav weight ---- */
.nav__cta{ background:transparent; color:var(--notte); border:none; box-shadow:none;
  display:inline-flex; align-items:center; gap:.4em; padding:.5em .2em;
  font-size:.86rem; font-weight:600; letter-spacing:.02em; opacity:.82;
  transition:opacity var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out); }
.nav__cta::before{
  content:""; width:1.1em; height:1.1em; flex:none;
  background:currentColor; -webkit-mask:var(--nav-user-icon) center/contain no-repeat;
  mask:var(--nav-user-icon) center/contain no-repeat; }
/* .btn--pill lays a sheen across every pill on hover. That reads as
   "this is a button" and this one is now a link, so it is stood down
   here rather than by taking the class off twelve HTML files. */
.nav__cta::after{ display:none; }
.nav__cta:hover{ background:transparent; color:var(--notte); opacity:1; transform:none; }
.nav__cta:active{ transform:none; }
/* signed in: the link carries the visitor's own name, which is worth
   the flavour ink — it is the one personal thing in the chrome */
.nav__cta.is-authed{ color:var(--carbonara-text); font-weight:700; opacity:1; }
.nav__cta.is-authed:hover{ background:transparent; color:var(--carbonara-text); }
.nav__cta.is-current{ background:transparent; color:var(--notte); opacity:1; text-decoration:underline; text-underline-offset:5px; text-decoration-thickness:2px; text-decoration-color:var(--oro-soft); }
.nav__cta.is-current.is-authed{ background:transparent; color:var(--carbonara-text); }

/* ============================================================ THE WORDMARK PLAQUE
   and the header's three zones (2026-09-11)

   Measured before it was changed: at 1280px the header row had 1084px to
   place five flex children needing 912px, so **84px of slack across the
   whole row** — every item shoulder-to-shoulder with the next, and the
   nav alone (576px, of which „Къде да ни намерите" is 170px) more than
   half the width. Nothing overflowed; it simply read as one undifferent-
   iated run of eight things, which is what it was.

   Two changes, both CSS only — the header markup is repeated across
   twelve hand-written HTML files and the 2026-09-10 hierarchy work kept
   it untouched; this keeps that property.

   1. A SEAM between navigation and actions. There is no spare width to
      spend on a bigger gap, so the grouping is carried by a hairline:
      "where can I go" | "what can I do". Eight items become two groups,
      which is the whole of the crowding complaint.
   2. The WORDMARK becomes an object. It was two lines of unboxed type on
      a cream page — the only element in the header with no edge of its
      own, next to a gold-ringed shop pill and a solid navy basket. The
      plaque is the site's existing panel vocabulary (--white-soft ground,
      radius, soft shadow) with the shop pill's --oro-soft hairline, so
      the two ends of the row answer each other.

   The header's own vertical padding drops 14px → 10px above 1024px to
   pay for the plaque's, so the bar grows ~4px rather than ~13px. Nothing
   measures header height in JS, and the fixed header is cleared by each
   page's own top padding, so a few px is free. */
.wordmark{
  padding:.34em .82em .4em;
  background:var(--white-soft);
  border:1px solid var(--oro-soft);
  border-radius:13px;
  box-shadow:0 12px 24px -20px rgba(22,48,107,.55);
  /* .header-inner is flex-nowrap, so a squeezed row shrinks its items
     rather than wrapping them — and at 1100px that shrank the plaque
     until "La Selezione" broke over two lines and the bar grew to
     110px. The plaque is a fixed object; it does not give. */
  flex:none; white-space:nowrap;
  transition:border-color var(--t-fast) var(--ease-out),
             box-shadow var(--t-fast) var(--ease-out),
             transform var(--t-fast) var(--ease-out);
}
.wordmark:hover{
  border-color:var(--oro-ink);
  box-shadow:0 16px 30px -20px rgba(22,48,107,.7);
  transform:translateY(-1px);
}
/* The footer's brand block uses .wordmark__name / __sub directly inside
   .footer__brand and never carries .wordmark itself, so it keeps its
   naked setting on the dark band — a cream plaque down there would be a
   second, brighter logo competing with the one in the bar. */

/* Between the drawer breakpoint and ~1250px the bar is genuinely narrow:
   the wider measure above cannot help, because there is no more viewport
   to take. Everything steps down one notch instead — and the seam goes,
   since a divider is the first thing to drop when the two groups it
   separates are already touching. */
@media (min-width:1025px) and (max-width:1250px){
  .wordmark{ padding:.26em .62em .3em; border-radius:11px; }
  .wordmark__name{ font-size:1.36rem; }
  .wordmark__sub{ font-size:.98rem; }
  .nav a{ font-size:.81rem; }
  .nav::after{ display:none; }
}

@media (min-width:1025px){
  /* The header is CHROME, and it was being held to the reading column:
     max-width 1180px on a 1280-1920px screen, with 84px of slack to
     place eight things in. Letting the bar run wider than the text
     column is the one change that actually buys room — measured, it
     takes the slack at 1440px from 21px to 217px. The cost is that the
     wordmark no longer lines up with the left edge of the page content,
     which is why it became a plaque first: an object that sits IN the
     bar reads as deliberate where two lines of loose type would just
     read as misaligned. */
  .header-inner{ max-width:min(1400px, 100%); padding-block:10px; gap:clamp(15px,1.8vw,24px); }
  .nav{ gap:clamp(14px,1.65vw,26px); }
  /* the seam. A flex item on .nav rather than a border on the next
     element, because the next element is .cart-pill — which is built by
     script.js and disappears with an empty basket, taking the divider
     with it. */
  .nav::after{
    content:""; align-self:center; flex:none;
    width:1px; height:22px; background:var(--line);
    margin-left:clamp(2px,.7vw,10px);
  }
}

.nav-toggle{ display:none; flex-direction:column; gap:5px; background:none; padding:8px; border:none; cursor:pointer; }
.nav-toggle span{ width:26px; height:2.5px; background:var(--notte); transition:transform .28s var(--ease-out), opacity .16s var(--ease-out); border-radius:2px; }
.nav-toggle.is-open span:nth-child(1){ transform:translateY(7.5px) rotate(45deg); }
.nav-toggle.is-open span:nth-child(2){ opacity:0; }
.nav-toggle.is-open span:nth-child(3){ transform:translateY(-7.5px) rotate(-45deg); }

/* The closed drawer is only translated off-screen, so its links stayed in the
   tab order on every page — a keyboard user tabbed out of the header straight
   into a menu they could not see. script.js marks it `inert` while closed
   (transitioning `visibility` here instead would cut the slide animation). */
.mobile-nav{ position:fixed; inset:0; z-index:55; background:var(--panna); display:flex; flex-direction:column; justify-content:center; align-items:center; gap:6px;
  transform:translateY(-100%);
  transition:transform .34s var(--ease-soft); }
/* will-change is promoted only while the drawer is open — a full-viewport
   compositor layer held for the whole page life (this element transforms twice
   a visit, often never) is exactly what will-change is not for. setNav() adds
   .is-open, so the hint lives and dies with the panel. */
.mobile-nav.is-open{ will-change:transform; }
.mobile-nav.is-open{ transform:translateY(0); }
.mobile-nav a{ font-family:var(--display); font-size:2rem; color:var(--notte); padding:.3em; }
/* The links catch up one after another once the panel has landed. Written as
   an animation with no hidden resting state on purpose: if animations are off
   for any reason the menu is simply there, rather than invisible. */
.mobile-nav.is-open a{ animation:drawerItem .3s var(--ease-out) backwards; }
.mobile-nav.is-open a:nth-of-type(1){ animation-delay:.08s; }
.mobile-nav.is-open a:nth-of-type(2){ animation-delay:.13s; }
.mobile-nav.is-open a:nth-of-type(3){ animation-delay:.18s; }
.mobile-nav.is-open a:nth-of-type(4){ animation-delay:.23s; }
.mobile-nav.is-open a:nth-of-type(n+5){ animation-delay:.28s; }
.mobile-nav.is-open .lang--drawer{ animation:drawerItem .3s var(--ease-out) .3s backwards; }
@keyframes drawerItem{ from{ opacity:0; transform:translate3d(0,14px,0); } to{ opacity:1; transform:none; } }
.mobile-nav a.is-current{ color:var(--carbonara-text); }
.lang--drawer{ margin-top:22px; }
.lang--drawer .lang__btn{ font-size:1.05rem; }

/* page-load fade for main — opacity-led so it never nudges layout */
main{ animation:pageIn .34s var(--ease-out) both; }
@keyframes pageIn{ from{ opacity:0; transform:translate3d(0,6px,0);} to{ opacity:1; transform:none;} }

/* ============================================================ HERO (home) */
/* The hero owns the first screen. It had no min-height on desktop — the
   two ≤920px rules below still carry `min-height:auto`, which is what
   is left of one — so its height was whatever the stage happened to
   be: about 700px in a 900px window. That put the bottom edge, and the
   "виж още" cue pinned 24px above it, level with the hero's own two
   buttons, where the cue read as a third call to action rather than as
   the floor of the screen. Filling the viewport puts it back at the
   bottom edge, which is the only place a scroll cue means anything.
   svh, not vh: on a phone vh is the height with the browser chrome
   RETRACTED, so a 100vh hero is taller than the window it opens in and
   the cue starts life below the fold. Capped, because past ~880px the
   hero stops being a composition and becomes a gap — and the cap is
   what leaves a sliver of the navy band showing on a tall display,
   which says "there is more" better than the cue does. */
.hero{ position:relative; display:flex; align-items:center; padding:112px 0 72px; overflow:hidden;
  min-height:min(100vh, 880px); min-height:min(100svh, 880px); }
.hero__inner{ position:relative; z-index:2; max-width:var(--maxw); margin-inline:auto; width:100%; padding-inline:var(--gutter); display:grid; grid-template-columns:1.02fr 1.05fr; gap:40px; align-items:center; }
/* the copy column sits above the stage so a wide box never washes out the text */
.hero__copy{ position:relative; z-index:5; }
/* ---- the hero's entrance is CSS's, not the observer's ----
   Same gesture as .reveal-stagger, deliberately re-declared instead of
   reused: the shared one begins at opacity:0 and only becomes visible
   when script.js adds `is-visible`, so the headline — the largest thing
   on the first screen, and what the browser reports as LCP — could not
   paint until i18n.js, the Supabase SDK, config.js, auth.js and
   script.js had all downloaded and run. Below the fold that gate is
   free, because the scroll that trips a reveal cannot happen before the
   script watching for it. Here it was the entire measurement.

   So: opacity ONE in the static rule, and the animation supplies the
   fade with animation-fill-mode:backwards holding the from-frame
   through the delay. That ordering matters twice over — it is what
   lets the copy paint on the browser's first frame with no JS in the
   path, and it is what makes the reduced-motion block's blanket
   `animation:none !important` land on visible text rather than on
   opacity:0. A `from{}`-less transition could do neither.
   The stagger is tighter than the shared .07s: this runs at first
   paint rather than after a scroll, so the last line wants to be down
   before the eye has finished the first. */
.hero__copy--enter > *{ opacity:1; animation:heroIn .52s var(--ease-out) backwards; }
.hero__copy--enter > *:nth-child(1){ animation-delay:0s; }
.hero__copy--enter > *:nth-child(2){ animation-delay:.05s; }
.hero__copy--enter > *:nth-child(3){ animation-delay:.11s; }
.hero__copy--enter > *:nth-child(4){ animation-delay:.17s; }
.hero__copy--enter > *:nth-child(n+5){ animation-delay:.23s; }
@keyframes heroIn{ from{ opacity:0; transform:translate3d(0,16px,0); } to{ opacity:1; transform:none; } }
.hero__title{ font-size:clamp(2.8rem,7vw,5.4rem); letter-spacing:-.01em; margin:.1em 0 .4em; }
.hero__title em{ color:var(--carbonara); }
.hero__lead{ font-size:1.1rem; max-width:42ch; color:var(--ink); opacity:.95; margin:0 0 1.1rem; }
/* What a box IS, between the lead and the button. It is a pill rather
   than a third paragraph because it is a fact, not prose — the reader
   should be able to take it without reading a sentence — and because at
   this point in the page everything else is either display type or body
   copy, so a small object reads as new information rather than as more
   of the lead.
   inline-block and NOT inline-flex, which is what it was for about an
   hour: the sentence carries a <strong> in the middle of it, and under
   flex each run of text on either side of that becomes its own flex
   item — so the gap meant to sit after the dot was inserted twice more,
   mid-sentence, and on a phone the three items wrapped into a column of
   fragments. Nothing here needs flex; the dot is an inline marker. */
.hero__serves{ display:inline-block; margin:0 0 2rem;
  padding:.5em 1.05em; border-radius:999px;
  background:var(--white-soft); border:1px solid var(--line);
  font-size:.92rem; line-height:1.5; color:var(--ink);
  box-shadow:0 10px 22px -18px rgba(14,40,80,.5); }
.hero__serves::before{ content:""; display:inline-block; vertical-align:middle;
  width:7px; height:7px; border-radius:50%; margin-right:.6em; margin-top:-2px;
  background:var(--oro-soft); }
.hero__serves strong{ color:var(--oro-ink); font-weight:700; white-space:nowrap; }
.hero__actions{ display:flex; align-items:center; gap:24px; flex-wrap:wrap; }

/* bold solid panel behind the boxes — the three flavour fields, side by side */
.hero__stage{ position:relative; height:clamp(360px,42vw,520px); display:flex; justify-content:center; align-items:flex-end; perspective:1200px; }
.hero__panel{ position:absolute; inset:6% 2% 10% 2%; border-radius:26px;
  background:linear-gradient(90deg,
    var(--carbonara-field) 0 33.34%,
    var(--bolognese-field) 33.34% 66.67%,
    var(--pomodoro-field) 66.67% 100%);
  box-shadow:0 40px 70px -38px rgba(14,48,90,.55); overflow:hidden; }
.hero__panel::after{ content:""; position:absolute; inset:0;
  background:linear-gradient(160deg, rgba(255,255,255,.28), rgba(255,255,255,0) 55%); }
.hero__panel-ribbon{ position:absolute; left:0; right:0; bottom:0; height:14px; z-index:1;
  background:repeating-linear-gradient(90deg,
    var(--carbonara-field) 0 40px, var(--bolognese-field) 40px 80px, var(--pomodoro-field) 80px 120px);
  opacity:.95; }
/* No transform transition: script.js interpolates the cursor tilt / drag on
   every frame, and a 600ms transition on top of that made each mouse move
   restart its own easing — the boxes lagged the pointer and felt rubbery.
   The JS spring owns transform; CSS keeps the shadow. */
/* The wrapper is now a positioner and nothing else (2026-09-09): it
   carries the arrangement — where in the panel, how fanned, which one
   in front — and script.js's parallax translates it. Everything inside
   is the 3D model, which draws its own shadow, sets its own cursor and
   owns its own touch-action, so all three moved off this rule.
   The drop-shadow went with them and had to: a filter here would be
   recomputed over the whole rendered scene on every frame of a spin,
   and it would sit UNDER the model's own ground shadow — two shadows
   from two different lights on one box. .box3d__shadow is the real one,
   and it knows which way the box is facing. */
.box{ position:absolute; bottom:6%; will-change:transform; z-index:2;
  display:flex; justify-content:center; align-items:flex-end;
  -webkit-user-select:none; user-select:none; }
/* the no-JS / no-preserve-3d fallback photo, which box3d.js hides the
   moment it has a model to show instead */
.box img{ width:100%; }
/* --b-w is the ONE number the model's geometry derives from, so the
   hero sets it per box the way .product__media and .shop-card__media
   do. It is ~.95 of the wrapper width the flat cut-out used, because
   the front face is the printed panel and the photo included the lit
   paper edge around it — same box, same size on screen.
   --accent is the focus ring's colour; without it the scene's
   :focus-visible outline falls back to currentColor, which on this
   panel is the body ink. */
/* THE FAN IS WIDER AND THE BOXES SMALLER THAN THEY WERE AS CUT-OUTS,
   and both had to move. The flat photograph is the box seen square on,
   so it covered exactly its own front panel; the model rests at
   rotateY(-18°) and shows a side as well, which is about a third more
   width for the same --b-w. At the old spacing the middle box ate most
   of the other two and Pomodoro was a yellow sliver. So: ~.87 of the
   old width, and the outer pair pushed out to the panel's own margin
   (inset 2%) rather than 9% inside it. */
.box--bolognese{ z-index:4; width:clamp(180px,23vw,280px); transform:translateZ(60px) rotate(-1deg); }
.box--bolognese .box3d{ --b-w:clamp(150px,19vw,232px); --accent:var(--bolognese-ink); }
.box--carbonara{ z-index:3; width:clamp(160px,20vw,250px); left:3%;  transform:translateX(-26%) rotate(-8deg) scale(.9); }
.box--pomodoro{  z-index:3; width:clamp(160px,20vw,250px); right:3%; transform:translateX(26%)  rotate(8deg)  scale(.9); }
.box--carbonara .box3d{ --b-w:clamp(132px,16.6vw,206px); --accent:var(--carbonara-ink); }
.box--pomodoro  .box3d{ --b-w:clamp(132px,16.6vw,206px); --accent:var(--pomodoro-ink); }

/* the deeper teal, not --carbonara: at .62rem this is small text, and the
   accent teal on cream is 3.9:1 — under AA. --carbonara-deep is the same hue,
   5.6:1, so the cue clears the floor without leaving the flavour's colour. */
.scroll-cue{ position:absolute; bottom:24px; left:50%; transform:translateX(-50%); z-index:3; display:flex; flex-direction:column; align-items:center; gap:4px; font-size:var(--label-sm); letter-spacing:.3em; text-transform:uppercase; color:var(--carbonara-deep); will-change:opacity; }
.scroll-cue svg{ animation:bob 1.8s var(--ease) infinite; }
@keyframes bob{ 0%,100%{ transform:translateY(0);} 50%{ transform:translateY(5px);} }

/* ============================================================ VALUES (dark navy band — a rest between colour fields) */
.values{ position:relative; background:var(--notte); color:#EAF6F6; padding:clamp(56px,7vw,88px) 0; overflow:hidden; }
.values .ribbon{ position:absolute; top:0; left:0; right:0; }
/* The band has a floor, so it stops reading as a gap. A 380px-tall
   rectangle of one flat navy between two cream sections is something
   the eye crosses rather than reads, and this is the page's only dark
   act — it should feel like a room, not a rule.

   ONE gradient, and it DARKENS. There was a second one over it, a wide
   teal wash falling from the top edge, on the reasoning that this is
   the same lit alcove .product__panel builds. It came out at .16 over
   the navy, which on screen is very close to nothing, and "a saturated
   chromatic wash fading to transparent on a dark surface" is a shape
   worth being able to justify by what it does rather than by what it
   was modelled on. It could not be, so it is gone: fewer stops to
   composite, and the band keeps everything the wash was there for.
   What is left is --notte-deep pooling at the foot — a surface shifted
   DOWN from its own colour, which is a floor and not a glow. */
.values::before{ content:""; position:absolute; inset:0; pointer-events:none;
  background:radial-gradient(72% 96% at 50% 126%, rgba(15,34,71,.9), rgba(15,34,71,0) 72%); }
.values .container{ position:relative; z-index:1; }
.values__grid{ display:grid; grid-template-columns:repeat(3,1fr); margin-top:0; }
.value{ padding:6px clamp(18px,3vw,40px); position:relative; }
.value + .value{ border-left:1px solid rgba(255,255,255,.16); }
/* THE NUMERALS ARE THE GRAPHIC BEAT, not a label. At 1.2rem they were
   the same size as the body copy under them and read as list markers —
   the same fault the story page's .timeline figures had at 1.3rem, and
   fixed the same way. They are the only display-scale type in a band
   that otherwise has none, and they carry the three packaging colours
   into the one dark act on the home page, so they are what the eye
   should land on when it crosses from the hero.
   The rule under each is what stops a big numeral from floating: it
   ties the figure to the heading it belongs to, and it is set in the
   same flavour colour so the three columns are colour-coded twice. */
.value__num{ font-family:var(--display); font-size:2.5rem; font-weight:700; display:block;
  line-height:1; margin-bottom:.85rem; letter-spacing:.02em; opacity:.95; }
.value__num::after{ content:""; display:block; width:34px; height:3px; border-radius:2px;
  margin-top:.5rem; background:currentColor; opacity:.55; }
.value:nth-child(1) .value__num{ color:var(--carbonara-field); }
.value:nth-child(2) .value__num{ color:var(--bolognese-field); }
.value:nth-child(3) .value__num{ color:var(--pomodoro-field); }
.value h3{ font-size:1.45rem; margin-bottom:.45rem; color:#fff; }
.value p{ margin:0; opacity:.86; font-size:.98rem; max-width:34ch; }
.value strong{ color:var(--pomodoro-field); font-weight:700; }

/* ============================================================ TEASER: meet the three (home) — bold colour-field cards */
.trio{ padding:clamp(64px,9vw,120px) 0; }
.trio__head{ text-align:center; margin-bottom:2.6rem; }
.trio__head .section-sub{ margin-inline:auto; }
.trio__grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(18px,2.4vw,30px); }
.flavour-card{ position:relative; display:block; border-radius:22px; overflow:hidden; background:var(--white-soft); border:1px solid var(--line); box-shadow:0 24px 48px -34px rgba(14,40,80,.5); transition:transform .28s var(--ease-out), box-shadow .28s var(--ease-out); }
.flavour-card:hover{ transform:translateY(-8px); box-shadow:0 36px 60px -34px rgba(14,40,80,.6); }
.flavour-card:active{ transform:translateY(-3px) scale(.995); transition-duration:.09s; }
/* z-index:0 for the same reason .product__media and .shop-card__media
   carry it: it makes this its own stacking context and drops the model
   BELOW body::before's paper grain. Without it the box is the one clean
   CG rectangle on a grainy page, which is most of what "obviously a
   render" means. The floor gained padding so the model's own ground
   shadow is not sliced off by the card's overflow:hidden. */
.flavour-card__top{ position:relative; z-index:0; height:300px; display:flex; align-items:flex-end; justify-content:center;
  padding-bottom:26px; background:var(--field); overflow:hidden; }
.flavour-card__top::before{ content:""; position:absolute; inset:0; background:linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,0) 55%); }
.flavour-card__top::after{ content:""; position:absolute; left:0; right:0; bottom:0; height:8px;
  background:repeating-linear-gradient(90deg,var(--carbonara-field) 0 26px,var(--bolognese-field) 26px 52px,var(--pomodoro-field) 52px 78px); }
/* The no-JS / no-preserve-3d fallback photo. box3d.js hides it the
   moment it has a model to show instead, and the model's own faces are
   immune to this rule — .box3d.is-3d .box3d__face--front img resets
   width, filter and transform at a higher specificity, which is exactly
   why those resets are written the way they are. */
.flavour-card__top img{ width:80%; filter:drop-shadow(0 18px 24px rgba(14,40,80,.3)); transform:translateY(4%); transition:transform .38s var(--ease-out); position:relative; z-index:1; }
.flavour-card:hover .flavour-card__top img{ transform:translateY(0) scale(1.04); }
/* Sized off the PANEL, not off the old photo. The scene is 1.28 × --b-w
   tall, so 186px puts it at 238px inside a 300px panel — the same ~79%
   the shop card's alcove gives its box — and 208px wide inside a 341px
   card. The flat cut-out it replaces was 80% of the card width and very
   nearly filled the panel; matching that number would make a scene
   333px tall in a 300px box and the roof would be cropped. */
.flavour-card__top .box3d{ position:relative; z-index:1; --b-w:min(186px, 46vw); }
/* The card owns the hover, because the card is what you are pointing
   at: the scene takes no pointer in static mode, so .box3d:hover can
   never fire and the box would sit dead while the card lifted around
   it. Same property the interactive boxes use, driven from one level
   further out. */
.flavour-card:hover .box3d{ --b-lift:calc(var(--b-w) * -.03); }
.flavour-card__body{ padding:22px 24px 26px; }
.flavour-card__over{ font-family:var(--display); text-transform:uppercase; letter-spacing:.18em; font-size:var(--label-sm); font-weight:700; color:var(--accent); margin:0 0 .3rem; }
.flavour-card__name{ font-size:1.7rem; margin:0 0 .5rem; color:var(--accent); }
.flavour-card__desc{ margin:0 0 1rem; font-size:.95rem; opacity:.88; }
.flavour-card__cta{ font-weight:700; font-size:.86rem; color:var(--accent); }
.flavour-card__cta::after{ content:" →"; }
.flavour-card[data-accent="carbonara"]{ --accent:var(--carbonara-ink); --field:var(--carbonara-field); --accent-deep:var(--carbonara-deep); }
.flavour-card[data-accent="bolognese"]{ --accent:var(--bolognese-ink); --field:var(--bolognese-field); --accent-deep:var(--bolognese-deep); }
.flavour-card[data-accent="pomodoro"]{ --accent:var(--pomodoro-ink); --field:var(--pomodoro-field); --accent-deep:var(--pomodoro-deep); }

/* ============================================================ CTA strip */
.cta-strip{ position:relative; background:var(--panna-2); padding:clamp(60px,7.5vw,92px) 0; text-align:center; overflow:hidden; }
.cta-strip .ribbon{ position:absolute; top:0; left:0; right:0; }
/* Soft flavour-coloured light in the strip's empty corners — PAINTED,
   not blurred. These were two 360px discs each carrying
   `filter: blur(70px)`, which is a 70px-radius separable convolution
   over a 500px box, twice, on the last full-width section before the
   footer. A gradient is the same soft falloff computed by the
   rasteriser in one pass, and unlike the filter it does not have to be
   re-run when anything above it invalidates the strip's tile. The
   opacity went into the colour stops for the same reason: an `opacity`
   on a pseudo-element is a separate compositing layer to blend, and
   there is nothing here that needs to be one.

   They are also stronger than they were. At .45 over parchment the two
   glows were a wash the eye read as dirt on the cream rather than as
   light, and the strip — the page's second ask, after the hero —
   looked like the least finished band on it. Held at the corners so
   the middle stays parchment and the copy keeps its contrast. */
.cta-strip::before, .cta-strip::after{ content:""; position:absolute; inset:0; pointer-events:none; }
.cta-strip::before{ background:
  radial-gradient(46% 62% at 2% 0%, rgba(116,207,208,.62), rgba(116,207,208,0) 70%),
  radial-gradient(44% 60% at 100% 104%, rgba(244,210,74,.62), rgba(244,210,74,0) 70%); }
/* a third, small and warm, directly under the button — the one place
   on the strip the eye is being sent */
.cta-strip::after{ background:radial-gradient(38% 52% at 50% 96%, rgba(240,206,218,.5), rgba(240,206,218,0) 72%); }
.cta-strip .container{ position:relative; z-index:1; }
.cta-strip h2{ font-size:clamp(1.9rem,4.2vw,3rem); margin:0 0 .4em; }
.cta-strip h2 em{ color:var(--pomodoro); }
.cta-strip p{ max-width:46ch; margin:0 auto 1.9rem; color:var(--ink); opacity:.92; }
/* The strip's whole job is this button, and at the shared pill size it
   was the same object as the "Разгледай" link in the hero. One step up
   in every dimension a pill has. */
.cta-strip .btn--pill{ padding:1.02em 2.3em; font-size:.98rem; }

/* ============================================================ PAGE HEADER (interior pages) — solid flavour field */
.page-head{ position:relative; padding:160px 0 56px; overflow:hidden; background:var(--field); }
.page-head .candy{ position:absolute; inset:0; opacity:.14; }
/* a big script flourish drifts in the head's empty right half */
.page-head::after{ content:"Mia Famiglia"; position:absolute; z-index:1; right:clamp(24px,7vw,110px); bottom:2px;
  font-family:var(--script); font-size:clamp(4.2rem,9vw,7.5rem); line-height:1; white-space:nowrap;
  color:var(--accent); opacity:.16; rotate:-4deg; pointer-events:none; }
.page-head__inner{ position:relative; z-index:2; }
.page-head .eyebrow{ color:var(--accent); }
.page-head h1{ font-size:clamp(2.4rem,6vw,4rem); margin:.1em 0 .3em; color:var(--accent); }
.page-head h1 em{ color:var(--accent); }
/* :not(.eyebrow) — this rule was written for the page head's LEAD, and
   was silently taking the eyebrow with it, because the eyebrow is a <p>
   too and `.page-head p` (0,1,1) outranks `.eyebrow` (0,1,0). Two
   things leaked through, and both matter:

   SIZE. The eyebrow came out at 1.06rem — 17px — on all eight interior
   page heads, where THE LABEL ROLE block up top says this device has
   exactly two sizes, .70rem and .78rem, and lists consolidating it
   down from seven as a fix. The same word set as an eyebrow on the home
   page renders at 11.2px, so the site had two different eyebrows and
   nothing said which was meant.

   INK. `opacity:.92` was landing on a colour chosen to the second
   decimal. --pomodoro-ink carries the comment "darkened: 4.7:1 on the
   candy band" — that is the whole reason the token exists — and .92 of
   it on that same band measures 4.29:1, under AA. An alpha applied by
   a broader selector is exactly how a measured colour stops being the
   colour that was measured; it is the argument --ink-soft is written
   out to make, three hundred lines above.

   The sibling rule `.page-head .eyebrow{ color:var(--accent) }` is the
   evidence this was never intended: the eyebrow was already being
   handled separately here, and only its colour had been noticed. */
/* --notte, not --ink, and no opacity. Two reasons, and the second is
   the one that made it worth repainting eight page heads.

   IT LOOKED MUDDY. --ink is a warm grey-brown chosen for body copy on
   cream, and these leads are the only place on the site it sits on a
   SATURATED field. Rendered side by side at reading size on all three
   (the yellow is the worst), the warm grey reads as the field dirtying
   the text rather than as a deliberate neutral — while the heading
   directly above it is a clean flavour ink. The navy is the site's own
   structural dark, it is what the carbonara head's own h1 is already
   set in, and it reads as chosen.

   IT ALSO MEASURED WORST. --ink through this rule's own .92 was
   4.80:1 on the turquoise — the tightest text on the site outside the
   bugs fixed the same day. --notte at full strength is 5.90 / 7.27 /
   7.24 across turquoise, pink and yellow.

   Note what this could NOT be: "a darker shade of the background", the
   obvious reading of the rule, is --accent-deep, and on the turquoise
   field that is --carbonara-deep — same hue, 2.98:1. Measured before
   it was written, not after. */
.page-head p:not(.eyebrow){ max-width:52ch; font-size:1.06rem; color:var(--notte); }
.page-head[data-accent="carbonara"]{ --accent:var(--carbonara-ink); --field:var(--carbonara-field); --field-deep:var(--carbonara-deep); --accent-deep:var(--carbonara-deep); }
.page-head[data-accent="bolognese"]{ --accent:var(--bolognese-ink); --field:var(--bolognese-field); --field-deep:var(--bolognese-deep); --accent-deep:var(--bolognese-deep); }
.page-head[data-accent="pomodoro"]{ --accent:var(--pomodoro-ink); --field:var(--pomodoro-field); --field-deep:var(--pomodoro-deep); --accent-deep:var(--pomodoro-deep); }

/* ============================================================ STORY (dark dramatic act) */
.story{ position:relative; background:var(--notte-deep); color:#E9E2D2; padding:clamp(130px,15vw,180px) 0 clamp(72px,11vw,140px); overflow:hidden; }
.story .ribbon{ position:absolute; top:0; left:0; right:0; }
.story__stripes{ position:absolute; inset:0 auto 0 0; width:46%; pointer-events:none; opacity:.07;
  background:repeating-linear-gradient(90deg,#fff 0 3px, transparent 3px 30px);
  -webkit-mask-image:linear-gradient(90deg,#000,transparent); mask-image:linear-gradient(90deg,#000,transparent); }
.story__inner{ position:relative; z-index:2; padding-top:18px; }
/* no max-width: the headline carries its own break (<br> in story.title)
   and a measure on top of it turned two confident lines into four ragged
   ones. Only the leading is tightened, which display sizes want anyway. */
.story__title{ color:var(--panna); font-size:clamp(2.2rem,5.5vw,3.8rem); line-height:1.1; }
/* the half of the headline that carries the idea, in the page's gold —
   the same em treatment the recipe panels below already use, so the
   two rhyme instead of the title being the one un-accented thing */
.story__title em{ font-style:italic; color:var(--oro-soft); }

/* THE STANDFIRST. The first paragraph is not a body paragraph: it is
   the way in, so it is bigger, brighter and on a shorter measure than
   everything under it. That difference is most of what stops the page
   reading as one undifferentiated column.
   No drop cap, deliberately: the paragraph opens on "La Selezione" and
   ::first-letter would tear the L off the brand name. */
.story__standfirst{
  margin:clamp(1.8rem,3.2vw,2.6rem) 0 0; max-width:54ch;
  color:#EFE9DA; font-size:clamp(1.12rem,1.75vw,1.38rem); line-height:1.6;
}
/* a short gold rule instead — the mark that says the article starts here */
.story__standfirst::before{
  content:""; display:block; width:54px; height:3px; border-radius:2px;
  margin:0 0 1.15em; background:linear-gradient(90deg,var(--oro),var(--oro-soft));
}

/* body paragraph beside the quote, rather than two identical columns of
   grey. Asymmetric on purpose: equal halves gave the quote the same
   weight as the prose and neither one led. */
.story__cols{ display:grid; grid-template-columns:minmax(0,1.08fr) minmax(0,.92fr);
  gap:clamp(28px,4vw,56px); align-items:start; margin:clamp(2rem,3.4vw,2.8rem) 0 0; }
.story__body{ margin:0; color:#D2CBBA; font-size:1.04rem; max-width:52ch; }
.story__body strong{ color:#EFE9DA; }

.pull-quote{ font-family:var(--display); font-style:italic; font-size:clamp(1.5rem,3.5vw,2.4rem); color:var(--panna); margin:2.4rem 0 2.8rem; max-width:26ch; line-height:1.3; padding-left:26px; border-left:3px solid var(--oro); }
.pull-quote cite{ display:block; font-style:normal; font-family:var(--body); font-size:.8rem; letter-spacing:.04em; color:var(--oro-soft); margin-top:1rem; }
/* in the story's two-column band the quote is a panel, not a margin
   note: it keeps the gold rule but gains a lit ground and drops the
   top margin that used to separate it from the paragraph above */
.story__cols .pull-quote{ margin:0; max-width:none; padding:clamp(20px,2.6vw,30px) clamp(20px,2.4vw,28px);
  border-left-width:4px; border-radius:0 20px 20px 0;
  background:linear-gradient(120deg, rgba(255,255,255,.07), rgba(255,255,255,.02) 70%); }

/* THE FIGURES. These were 1.3rem — the size of a caption, which is why
   three genuinely good numbers read as a footnote. At display scale
   they become the graphic beat of the page, and they are the only
   place the three packaging colours reach this dark act. */
.timeline{ display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:clamp(12px,1.6vw,18px);
  margin:clamp(2.4rem,4vw,3.4rem) 0 0; padding:0; list-style:none; }
.timeline li{ display:flex; flex-direction:column; gap:.15em; font-size:.84rem; color:#C9C2B0; padding:clamp(18px,2.2vw,24px); border-radius:18px; background:rgba(255,255,255,.04); border:1px solid rgba(203,168,111,.3); transition:transform .26s var(--ease-out), border-color .26s var(--ease-out), background .26s var(--ease-out); }
.timeline li:hover{ transform:translateY(-4px); border-color:var(--oro); background:rgba(255,255,255,.08); }
.timeline li em{ font-style:normal; letter-spacing:.03em; }
.timeline li:nth-child(1) span{ color:var(--carbonara-field); }
.timeline li:nth-child(2) span{ color:var(--bolognese-field); }
.timeline li:nth-child(3) span{ color:var(--pomodoro-field); }
.timeline span{ font-family:var(--display); font-weight:600; line-height:1;
  font-size:clamp(2.3rem,4.6vw,3.2rem); }
/* the line the page ends on — set in the display italic so it reads as
   a closing note rather than another paragraph, and kept narrower than
   the panels above it so the section visibly comes to a point. The rule
   above it is the same mark the standfirst opens with, so the section
   is visibly bracketed top and bottom. */
.story__close{ max-width:54ch; margin:clamp(2.4rem,4vw,3.4rem) 0 0; color:#E4DDCC;
  padding-top:clamp(1.4rem,2.4vw,2rem); border-top:1px solid rgba(203,168,111,.28);
  font-family:var(--display); font-style:italic; font-size:clamp(1.1rem,2.2vw,1.45rem); line-height:1.45; }

/* ============================================================ PRODUCTS (solid flavour field per selection) */
/* the bottom padding is shorter than it was: the section now ENDS on a
   signpost pointing at the band below, and a screen of empty cream
   between the two undoes most of what the signpost is for */
.lineup{ position:relative; padding:clamp(40px,6vw,72px) 0 clamp(44px,6vw,80px);
  --accent:var(--carbonara-ink); --field:var(--carbonara-field); --field-deep:var(--carbonara-deep); --accent-deep:var(--carbonara-deep); }
.lineup[data-accent="carbonara"]{ --accent:var(--carbonara-ink); --field:var(--carbonara-field); --field-deep:var(--carbonara-deep); --accent-deep:var(--carbonara-deep); }
.lineup[data-accent="bolognese"]{ --accent:var(--bolognese-ink); --field:var(--bolognese-field); --field-deep:var(--bolognese-deep); --accent-deep:var(--bolognese-deep); }
.lineup[data-accent="pomodoro"]{ --accent:var(--pomodoro-ink); --field:var(--pomodoro-field); --field-deep:var(--pomodoro-deep); --accent-deep:var(--pomodoro-deep); }

.lineup__tabs{ display:flex; justify-content:center; flex-wrap:wrap; gap:12px; margin-bottom:2.6rem; }
.tab{ background:var(--white-soft); font-family:var(--body); font-weight:700; font-size:.95rem; color:var(--notte); padding:.72em 1.6em; border-radius:999px; border:2px solid var(--line); cursor:pointer;
  transition:transform var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out); }
.tab:hover{ transform:translateY(-2px); }
.tab:active{ transform:translateY(0) scale(.97); }
.tab[data-target="p-carbonara"]:hover{ border-color:var(--carbonara); color:var(--carbonara-text); }
.tab[data-target="p-bolognese"]:hover{ border-color:var(--bolognese); color:var(--bolognese); }
.tab[data-target="p-pomodoro"]:hover{ border-color:var(--pomodoro); color:var(--pomodoro); }
.tab.is-active{ color:#fff; border-color:transparent; }
/* Hovering the SELECTED tab used to erase its own label: the three rules above
   are (0,3,0) and so is .tab.is-active, so on a tie the later one wins — and
   they were painting the flavour's colour onto the flavour's own background.
   1.0:1, on all three, and the label simply vanished under the pointer.
   Restated after them so source order settles it the right way round. */
.tab.is-active:hover{ color:#fff; border-color:transparent; }
/* the deep teal, where the other two flavours use their base colour: white
   on --carbonara is 4.2:1 and this is 6.0:1, which is also what makes the
   three selected tabs read at one weight instead of the teal one reading
   lighter than the green and the red beside it */
.tab[data-target="p-carbonara"].is-active{ background:var(--carbonara-text); }
.tab[data-target="p-bolognese"].is-active{ background:var(--bolognese); }
.tab[data-target="p-pomodoro"].is-active{ background:var(--pomodoro); }

.lineup__stage{ border-radius:28px; padding:clamp(28px,4vw,56px); position:relative; overflow:hidden; scroll-margin-top:92px;
  background:var(--field); border:1px solid rgba(255,255,255,.45);
  box-shadow:0 40px 80px -50px rgba(14,40,80,.5); transition:background .38s var(--ease-out); }
.lineup__stage::before{ content:""; position:absolute; top:0; left:0; right:0; height:12px;
  background:repeating-linear-gradient(90deg, var(--carbonara-field) 0 30px, var(--bolognese-field) 30px 60px, var(--pomodoro-field) 60px 90px); opacity:.95; }

.product{ display:grid; grid-template-columns:.92fr 1.08fr; gap:clamp(24px,4vw,56px); align-items:center; }
.product[hidden]{ display:none; }
.product.is-active{ animation:fadeUp .34s var(--ease-out); }
@keyframes fadeUp{ from{ opacity:0; transform:translate3d(0,12px,0);} to{ opacity:1; transform:none;} }

/* ---- the alcove the box stands in ----
   The panel used to be one flat rectangle of 42% white, which gave the
   3D model nothing to be three-dimensional *against*: an object with
   careful lighting sitting on a surface with none reads as a cut-out.
   It is now a shallow lit niche — a wall, a floor, a lamp above and a
   shelf lip along the front — built entirely from gradients, so it
   still costs no image and still re-tints itself per flavour.

   z-index:0 is load-bearing and easy to mistake for tidying up. It
   makes the media its own stacking context, which drops the box below
   body::before — the paper grain laid over the whole site. Without it
   the box3d model was the one thing on the page NOT wearing the grain,
   and a clean CG object on grainy paper is exactly the giveaway this
   work set out to remove. */
/* The padding is the room. Below the box it is floor: stood any lower
   there is nothing in front of it for the shadow to fall across, and a
   shadow you cannot see is a box that is not standing anywhere. Above
   it is headroom for the script flourish to run across the back wall.
   Both are padding rather than min-height because the ≤920px rule
   drops min-height for the single-column layout, and the alcove has to
   survive that. */
.product__media{ position:relative; z-index:0; display:flex; justify-content:center; align-items:flex-end;
  min-height:clamp(400px,41vw,520px);
  /* the floor, as a variable rather than inline: the open state (see
     "THE BOX, OPEN") has to work out where the box's bottom edge is in
     order to lift it to the top of a taller alcove, and a second copy
     of this clamp is a second thing to remember to change */
  --alcove-pad:clamp(72px,6.5vw,92px);
  padding:clamp(30px,4.5vw,54px) 0 var(--alcove-pad); }
.product__panel{ position:absolute; inset:2% 3% 2% 3%; border-radius:24px; overflow:hidden;
  background:
    /* the lamp, hung above and a little in front */
    radial-gradient(56% 38% at 50% 17%, rgba(255,255,255,.62), rgba(255,255,255,0) 74%),
    /* which reaches the floor again *behind* the box — in front of it
       is where the shadow goes, and the two were cancelling out */
    radial-gradient(46% 12% at 50% 71%, rgba(255,255,255,.4), rgba(255,255,255,0) 78%),
    /* wall into floor, no hard seam — the horizon is the flattening
       between the second stop and the third */
    linear-gradient(180deg, rgba(255,255,255,.18) 0%, rgba(255,255,255,.44) 52%,
                             rgba(255,255,255,.3) 68%, rgba(255,255,255,.1) 100%),
    /* and somewhere for all that light to fall off to */
    radial-gradient(112% 80% at 50% 36%, rgba(255,255,255,0) 48%, rgba(14,40,80,.18) 100%);
  box-shadow:0 28px 54px -30px rgba(14,40,80,.5), inset 0 1px 0 rgba(255,255,255,.55);
  transition:background .38s var(--ease-out);
  /* the wall takes no pointer — only the word on it does (its ::before
     opts back in). See "the signature catches the light" below. */
  pointer-events:none;
  --word-o:.5; }
/* the flavour, signed on the back wall in the house script, angled so
   the box's roof cuts across it.
   --word-o is the one knob for how present the signature is, because
   it needs two answers: on a desktop panel it has to read as sign-
   writing rather than a watermark, and a phone shrinks the panel and
   not the surrounding light, so it is pushed further again below. The
   text-shadow stack is what lets it sit this high without going muddy
   — a highlight under and a hairline over reads as paint on the wall
   rather than a grey wash across it. */
.product__panel::before{
  content:attr(data-word); position:absolute; left:5%; top:11%;
  transform:rotate(-4deg); transform-origin:left top;
  font-family:var(--script); font-size:clamp(2.25rem,4.2vw,3.2rem); line-height:1;
  white-space:nowrap; color:var(--accent); opacity:var(--word-o);
  /* the first shadow is the weight: Parisienne ships one weight, and
     asking for a bolder one gets a synthetic smear across a script
     face, so the strokes are thickened with a hairline of the word's
     own colour laid under them instead */
  text-shadow:0 0 .8px currentColor, 0 1px 0 rgba(255,255,255,.5), 0 -1px 0 rgba(14,40,80,.07);
  /* the word is the hit target the panel isn't: hit-testing a
     pseudo-element hovers the element it belongs to, so
     .product__panel:hover is true over these glyphs and nowhere else */
  pointer-events:auto;
  /* one duration for everything that changes. Three different ones on
     an object this small (colour in .18s under an opacity still
     crawling through .45s) is most of what read as unsettled. */
  transition:opacity var(--t-med) var(--ease-out), color var(--t-med) var(--ease-out),
             text-shadow var(--t-med) var(--ease-out),
             /* the open state moves and shrinks it — on the BOX's clock,
                not the hover clock, so the two travel together */
             top .62s var(--ease-out), left .62s var(--ease-out),
             font-size .62s var(--ease-out);
}
/* the front lip of the shelf. The candy stripe that used to sit here
   went grey the moment it was shaded, and the stage already wears one
   along its top edge — so the lip is just an edge now: dark underside,
   light catching the top of it. */
.product__panel::after{ content:""; position:absolute; left:0; right:0; bottom:0; height:16px;
  background:linear-gradient(180deg,
    rgba(14,40,80,.16) 0, rgba(255,255,255,.4) 22%, rgba(255,255,255,.16) 55%, rgba(14,40,80,.12) 100%); }

/* ---- the signature catches the light ----
   Shared with .shop-card__panel (SHOP section), which paints the same
   word on the same kind of wall — the geometry differs per page, the
   behaviour doesn't, and duplicating the @supports block below to keep
   them in separate sections would be how they drift apart.

   What is hovered is the word, and only the word. It used to be the
   whole panel here and the whole card in the shop, which lit the
   signature every time the pointer crossed the alcove on its way to
   somewhere else — and worse, the 3D box sits over that panel, so
   moving onto the box dropped the hover and moving off it restarted
   the sweep. The word flickered at people who were not looking at it.
   A pseudo-element can be a hit target even though it isn't in the
   DOM: the panel is pointer-events:none, its ::before is
   pointer-events:auto, and hitting a pseudo-element hovers the element
   it belongs to — so :hover here means the pointer is on the glyphs.

   Nothing moves on hover, for the same reason: the word is now its own
   hit area, and lifting it 2px out from under the pointer is a hover
   that switches itself off and on again along its own bottom edge.
   hover:hover keeps it off touch screens, where :hover sticks after a
   tap and the gold would just stay on. */
@media (hover:hover){
  .product__panel:hover::before,
  .shop-card__panel:hover::before{
    opacity:.82; color:var(--oro-soft);
    text-shadow:0 0 16px rgba(203,168,111,.45), 0 1px 0 rgba(255,255,255,.3);
  }

  /* the glint itself, where the browser can clip a gradient to the
     glyph shapes. Outside this @supports the colour change above is
     the whole effect — which is why it lives outside and not in here.
     The sweep runs 100%→0% and no further: the gradient is 2.6× the
     text box, so those two positions are exactly the range over which
     it still covers every glyph, and overshooting either end leaves
     part of the word with no background at all, i.e. invisible.
     Filled `both`, so it settles on solid gold and stays there rather
     than snapping back mid-hover. The curve is the soft one and not
     --ease-out: --ease-out spends four fifths of the travel in the
     first fifth of the time, which on a band this wide is a flash
     followed by a crawl. */
  @supports ((-webkit-background-clip:text) or (background-clip:text)){
    .product__panel:hover::before,
    .shop-card__panel:hover::before{
      color:transparent; -webkit-text-fill-color:transparent; text-shadow:none;
      /* the band is a *brighter* gold, not a lighter one. Both earlier
         tries (near-white, then pale cream) blanched the word at peak
         sweep — on these pale fields lightness is the one axis with no
         room left, so the glint travels on saturation instead */
      background-image:linear-gradient(105deg,
        var(--oro-soft) 0 42%, #E8CE8C 50%, var(--oro-soft) 58% 100%);
      background-size:260% 100%; background-repeat:no-repeat;
      -webkit-background-clip:text; background-clip:text;
      animation:word-glint 1.05s var(--ease-soft) both;
    }
  }
}
/* reduced motion kills this animation globally (MOTION / A11Y block),
   which is why the gold is the animation's *end* state and not
   something only the sweep delivers */
@keyframes word-glint{
  from{ background-position:100% 0; }
  to{   background-position:0% 0; }
}

.product__media img{ width:min(340px,82%); filter:drop-shadow(0 26px 34px rgba(14,40,80,.32)); position:relative; z-index:2; }

.product__over{ font-family:var(--display); text-transform:uppercase; letter-spacing:.2em; font-size:var(--label); font-weight:700; color:var(--accent); margin:0 0 .5rem; transition:color .35s var(--ease-out); }
.product__name{ font-size:clamp(2rem,4.5vw,3rem); margin:0 0 .7rem; color:var(--accent); transition:color .35s var(--ease-out); }
/* The panel is where a flavour is actually described, and since the
   comparison strip went (2026-09-09) it is the ONLY place — so the
   paragraph is set a step up from body copy and given the leading to
   go with it, rather than reading as caption under the name. */
/* --notte, for the reason .page-head p carries in full: this paragraph
   sits ON the flavour field, and --ink is a warm grey-brown chosen for
   cream. Against the turquoise it reads as the field dirtying the text,
   under a heading that is already a clean flavour ink. Measured 5.0:1
   as --ink, 5.9:1 as --notte. */
.product__desc{ margin:0 0 1.35rem; max-width:46ch; color:var(--notte);
  font-size:1.06rem; line-height:1.72; }
/* No opacity fade: at --label-sm this small label needs 4.5:1, and the
   accent inks clear it only at full strength (pomodoro is 4.72:1, and .85
   dropped it to 3.92:1). The eyebrow reads as secondary from its size, not
   from a fade that greys it against the field. */
.product__heart{ font-size:var(--label-sm); letter-spacing:.28em; text-transform:uppercase; color:var(--accent); font-weight:700; margin:0 0 .8rem; }
.chips{ display:flex; flex-wrap:wrap; gap:8px; margin-bottom:1.8rem; }
.chips li{ font-size:.84rem; font-weight:600; padding:.45em 1em; border-radius:999px; background:#fff; border:1.5px solid rgba(255,255,255,.9); color:var(--notte); display:inline-flex; align-items:center; gap:.5em; box-shadow:0 6px 16px -10px rgba(14,40,80,.4); }
.chips li::before{ content:""; width:8px; height:8px; border-radius:50%; background:var(--accent); transition:background .35s var(--ease-out); }
.badges{ display:flex; flex-wrap:wrap; gap:8px 18px; }
.badges li{ font-size:.74rem; letter-spacing:.04em; color:var(--notte); position:relative; padding-left:18px; }
.badges li::before{ content:""; position:absolute; left:0; top:50%; transform:translateY(-50%); width:9px; height:9px; border-radius:50%; background:var(--accent); transition:background .35s var(--ease-out); }

/* ---- the one claim that tells the flavours apart ----
   Raised out of the old five-badge list (four of which repeated the same
   universal claims on every flavour) into the gold register the shop card
   already uses for exactly this. All three flavours carry one — imported
   Italian ingredients on Carbonara/Pomodoro, the house recipe on Bolognese
   — so the difference between the panels is the words, never the presence
   of a mark. Type, not a seal: the claim has always been text on this site. */
.product__claim{
  display:inline-block; margin:0 0 1.9rem; padding:.5em 1.15em .54em;
  border-radius:999px; font-size:var(--label-sm); font-weight:700;
  letter-spacing:.08em; text-transform:uppercase; color:var(--white-soft);
  background:linear-gradient(150deg, var(--claim-lit), var(--claim-mid) 55%, var(--claim-deep));
  /* the same lift as .shop-card__italian — the two pages carry the one
     claim mark, and a heavier shadow made this read as a floating card
     rather than the pill it is */
  box-shadow:0 6px 14px -8px rgba(14,40,80,.55);
}

/* ---- what every box carries, said once ----
   The four universal claims, lifted out of all three panels into one strip
   at the foot of the stage. The label is doing real work, not decoration:
   it is what makes the consolidation honest — these four are true of every
   flavour, so they are stated for the lineup rather than three times over.
   A hairline off the field divides it from the product above; the dots and
   label take --accent, so the strip reads as whichever flavour is showing. */
.lineup__claims{
  margin-top:clamp(26px,3.4vw,40px);
  padding-top:clamp(20px,2.6vw,28px);
  border-top:1px solid rgba(255,255,255,.5);
  display:flex; flex-wrap:wrap; align-items:center; gap:10px 28px;
}
.lineup__claims__label{
  margin:0; font-family:var(--display); text-transform:uppercase;
  letter-spacing:.2em; font-size:var(--label-sm); font-weight:700;
  color:var(--accent); transition:color .35s var(--ease-out);
}

/* ---- the handoff to the videos ----
   The products section used to end on a comparison strip that repeated
   what the panel above already says; it ends on a signpost instead. The
   flavour comparison lived here until 2026-09-09 (the three cards said
   the name, the hook and the ingredients — all three of which the open
   panel above states in full), and the band below it was being scrolled
   straight past because a change of background colour is not a signpost.
   So the last thing in the section names what comes next.

   It takes --accent, so it belongs to the flavour showing above rather
   than reading as page chrome; and the section's own bottom padding was
   shortened with it, because a signpost with a screen of empty cream
   between it and the thing it points at is not pointing at anything. */
.lineup__next{
  margin:clamp(26px,3.4vw,40px) auto 0; width:fit-content;
  /* block-level flex, not inline-flex: auto margins only centre a
     block-level box, and as an inline one this sat against the gutter */
  display:flex; align-items:center; gap:.65em;
  font-family:var(--display); font-weight:700; text-transform:uppercase;
  letter-spacing:.16em; font-size:var(--label); line-height:1;
  color:var(--accent); text-decoration:none;
  padding:.9em 1.5em; border-radius:999px;
  background:var(--white-soft); border:2px solid rgba(255,255,255,.9);
  box-shadow:0 16px 30px -22px rgba(14,40,80,.55);
  transition:transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out),
             border-color var(--t-fast) var(--ease-out), color .35s var(--ease-out);
}
.lineup__next:hover{ transform:translateY(-3px); border-color:currentColor;
  box-shadow:0 22px 34px -22px rgba(14,40,80,.5); }
.lineup__next:active{ transform:translateY(-1px) scale(.99); }
.lineup__next:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }
/* the arrow keeps nodding at the section below it, on the site's own
   easing — it stops for anyone who has asked motion to stop, and it is
   decoration either way: the label is the link */
.lineup__next svg{ animation:nudge 1.8s var(--ease-soft) infinite; }
@keyframes nudge{ 0%,72%,100%{ transform:translateY(0); } 86%{ transform:translateY(3px); } }
@media (prefers-reduced-motion:reduce){ .lineup__next svg{ animation:none; } }
@media (hover:none){
  .lineup__next:hover{ transform:none; border-color:rgba(255,255,255,.9); }
}

/* ---- order buttons (products page → shop) ---- */
/* the value anchor, and the action it anchors, are one group. What a box
   costs and that it feeds two now sit where the decision is made rather
   than only in /shop; the number wears the shop card's price treatment so
   €8.90 reads the same wherever the customer meets it. The generous gap
   above the offer is what sets this action group apart from the ingredients. */
.product__offer{ margin:2.2rem 0 0; display:flex; flex-wrap:wrap; align-items:baseline; gap:.45em;
  font-family:var(--display); font-size:1.25rem; font-weight:700; color:var(--notte); }
/* full --ink, de-emphasised by size not opacity: .7 greyed this suffix to
   ~3.1-3.5:1 on the coloured fields (it borrowed the shop card's fade, but
   the shop card sits on a lighter ground). Full ink is 5.6-7.0:1 here. */
.product__offer span{ font-family:var(--body); font-size:.78rem; font-weight:600; color:var(--notte); }

/* the page's primary action, weighted like it: a step larger than the
   supporting type (1rem on solid accent, against the tab's .95rem and the
   chips' .84rem), and tight under the price it commits to. Shadow is the
   site's navy elevation — offset+blur, not a glow. */
.btn-order{ display:inline-flex; align-items:center; gap:.6em; margin-top:.8rem; padding:.95em 2em; border-radius:999px; color:#fff; background:var(--accent); font-family:var(--body); font-weight:700; font-size:1rem; letter-spacing:.01em; transition:transform var(--t-fast) var(--ease-out), filter var(--t-fast) var(--ease-out), background .35s var(--ease-out); box-shadow:0 15px 28px -15px rgba(14,40,80,.55); }
.btn-order:hover{ filter:brightness(1.08); transform:translateY(-2px); }
.btn-order:active{ transform:translateY(0) scale(.97); }
.btn-order svg{ transition:transform var(--t-med) var(--ease-out); }
.btn-order:hover svg{ transform:translateY(-2px) rotate(-8deg); }
.btn-order svg{ flex:0 0 auto; }
.page-head__cta{ margin-top:1.4rem; }

/* ============================================================ SHOP */
.shop{ padding:clamp(40px,6vw,72px) 0 clamp(64px,10vw,128px); }
.shop__inner{ display:grid; grid-template-columns:1.15fr .85fr; gap:clamp(28px,4vw,56px); align-items:start; }
.shop__cards{ display:grid; gap:clamp(18px,2.4vw,28px); }

.shop-card{ display:grid; grid-template-columns:.82fr 1.18fr; border-radius:22px; overflow:hidden; background:var(--white-soft); border:1px solid var(--line); box-shadow:0 24px 48px -34px rgba(14,40,80,.5); transition:transform .28s var(--ease-out), box-shadow .28s var(--ease-out);
  --accent:var(--carbonara-ink); --field:var(--carbonara-field); --accent-deep:var(--carbonara-deep); }
.shop-card[data-accent="bolognese"]{ --accent:var(--bolognese-ink); --field:var(--bolognese-field); --accent-deep:var(--bolognese-deep); }
.shop-card[data-accent="pomodoro"]{ --accent:var(--pomodoro-ink); --field:var(--pomodoro-field); --accent-deep:var(--pomodoro-deep); }
.shop-card:hover{ transform:translateY(-4px); box-shadow:0 36px 60px -34px rgba(14,40,80,.6); }
.shop-card.is-flash{ outline:3px solid var(--accent); outline-offset:3px; }

/* Same alcove as the products page, at a tenth the size: a lamp, a
   pool of light on the floor and a vignette. z-index:0 for the same
   reason it is there — it puts the card's box under the site's grain.
   The extra floor room is what stops the shadow being sliced off by
   the card's own overflow:hidden. */
.shop-card__media{ position:relative; z-index:0; display:flex; align-items:flex-end; justify-content:center;
  min-height:238px; padding-bottom:22px; overflow:hidden;
  background:
    radial-gradient(62% 42% at 50% 22%, rgba(255,255,255,.42), rgba(255,255,255,0) 76%),
    radial-gradient(40% 13% at 50% 86%, rgba(255,255,255,.4), rgba(255,255,255,0) 78%),
    /* wall into floor, the same horizon the products alcove has: the
       flattening between the middle two stops is where one becomes
       the other, and without it the field is a flat colour chip */
    linear-gradient(180deg, rgba(255,255,255,.1) 0%, rgba(255,255,255,.3) 54%,
                            rgba(255,255,255,.19) 70%, rgba(14,40,80,.06) 100%),
    radial-gradient(112% 78% at 50% 40%, rgba(255,255,255,0) 48%, rgba(14,40,80,.16) 100%),
    var(--field);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.5); }
.shop-card__media::before{ content:""; position:absolute; inset:0; background:linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,0) 55%); }

/* The flavour signed across the back wall, and the shelf lip under it
   — the two things the products page alcove had that this one didn't.
   A real element rather than more pseudo-elements because both of
   .shop-card__media's are spoken for (::before is the top sheen,
   ::after the turn glyph), and z-index:0 rather than auto so it paints
   over that sheen and under the box (z-index:1) without depending on
   where it happens to sit in the markup. pointer-events:none because
   the panel covers the box and half the card with it; the word inside
   opts back in, and is the whole hover target. */
.shop-card__panel{ position:absolute; inset:0; z-index:0; pointer-events:none; --word-o:.46; }
.shop-card__panel::before{
  content:attr(data-word); position:absolute; left:6%; top:8%;
  transform:rotate(-4deg); transform-origin:left top;
  font-family:var(--script); font-size:clamp(1.4rem,3.4vw,1.85rem); line-height:1;
  white-space:nowrap; color:var(--accent); opacity:var(--word-o);
  /* weight, then the highlight and the hairline — see the products
     alcove for why the weight is a shadow and not a font-weight */
  text-shadow:0 0 .7px currentColor, 0 1px 0 rgba(255,255,255,.45), 0 -1px 0 rgba(14,40,80,.06);
  pointer-events:auto;
  transition:opacity var(--t-med) var(--ease-out), color var(--t-med) var(--ease-out),
             text-shadow var(--t-med) var(--ease-out);
}
.shop-card__panel::after{ content:""; position:absolute; left:0; right:0; bottom:0; height:11px;
  background:linear-gradient(180deg,
    rgba(14,40,80,.14) 0, rgba(255,255,255,.36) 24%, rgba(255,255,255,.14) 56%, rgba(14,40,80,.1) 100%); }
/* the turn-me glyph, kept to a corner: the card has no room for the
   worded hint the products page carries, and no room to spare */
.shop-card__media::after{
  content:""; position:absolute; right:12px; top:12px; z-index:3;
  width:17px; height:17px; opacity:.34;
  background:var(--accent);
  -webkit-mask:var(--b-turn-icon) center/contain no-repeat;
          mask:var(--b-turn-icon) center/contain no-repeat;
  transition:opacity var(--t-med) var(--ease-out), transform var(--t-med) var(--ease-out);
}
.shop-card:hover .shop-card__media::after{ opacity:.6; transform:rotate(38deg); }
.shop-card__media img{ width:76%; filter:drop-shadow(0 16px 22px rgba(14,40,80,.3)); transform:translateY(5%); position:relative; z-index:1; transition:transform .38s var(--ease-out); }
.shop-card:hover .shop-card__media img{ transform:translateY(2%) scale(1.03); }

.shop-card__body{ padding:20px 24px 22px; display:flex; flex-direction:column; align-items:flex-start; gap:.15rem; }
/* .68rem put this at 10.88px, under the floor small functional type
   stops being readable at — the same one the claim badge and the
   optional marker were lifted past. .7rem is 11.2px and the line is
   short enough that the extra width costs nothing. */
.shop-card__over{ font-family:var(--display); text-transform:uppercase; letter-spacing:.18em; font-size:var(--label-sm); font-weight:700; color:var(--accent); margin:0; }
.shop-card__name{ font-size:1.65rem; margin:0; color:var(--accent); }
.shop-card__price{ margin:.2rem 0 .9rem; font-family:var(--display); font-size:1.25rem; font-weight:700; color:var(--notte); }
/* .75, not .65: at 12.16px this unit label needs 4.5:1 and --notte at
   .65 on the card's white measures 4.36 — the kind of miss that only
   shows up when you compute it, since the number beside it is the same
   navy at full strength and reads perfectly. .75 puts it at 5.85 and
   still sits a clear step below the price. */
.shop-card__price span{ font-family:var(--body); font-size:.76rem; font-weight:600; opacity:.75; margin-left:.35em; }
/* The price is an <em>, so it takes Lora's italic — and the row above
   asks for 700, a weight we load upright but NOT italic (the request
   carries ital 400/500/600). Chrome was slanting and smearing the 600
   to fake it, on all three cards. 600 is the heaviest italic Lora
   actually ships here, and a real one beats a synthesised heavier one.
   The alternative was adding ital 700 to the font request: a tenth
   file on a page that already pulls ten, for a difference this size. */
.price-num{ font-weight:600; }


.shop-summary{ position:sticky; top:110px; background:#fff; border:1px solid var(--line); border-radius:24px; padding:clamp(24px,4vw,36px); box-shadow:0 36px 70px -40px rgba(14,40,80,.4); overflow:hidden; }
.shop-summary::before{ content:""; position:absolute; top:0; left:0; right:0; height:8px;
  background:repeating-linear-gradient(90deg,var(--carbonara-field) 0 22px,var(--bolognese-field) 22px 44px,var(--pomodoro-field) 44px 66px); }
.shop-summary__list{ display:grid; gap:8px; margin:0 0 1.1rem; }
.shop-summary__list li{ display:flex; justify-content:space-between; gap:14px; font-size:.95rem; color:var(--notte); padding:8px 0; border-bottom:1px solid var(--line); }
.shop-summary__list li em{ font-style:normal; font-weight:700; }
/* --ink-soft rather than an opacity, the same swap the four rules
   above took on 2026-09-10: --notte at .65 on the panel's white is
   4.36:1, and this is the line a visitor reads when the basket panel
   has nothing else in it to read. 5.43:1, and it still sits a clear
   step below the panel's own headings. */
.shop-summary__empty{ color:var(--ink-soft); font-size:.9rem; border-bottom:none !important; }
.shop-summary__total{ display:flex; justify-content:space-between; align-items:baseline; margin:0 0 1.4rem; font-size:var(--label); letter-spacing:.16em; text-transform:uppercase; color:var(--oro-ink); font-weight:700; }
/* font-weight is explicit because <strong>'s UA default is `bolder`,
   which resolves against this row's 700 and lands on 900 — a weight
   Lora does not ship, so the browser was synthesising it. The total is
   the most-read number on the page and it was the one set in a smeared
   fake bold. 700 is the heaviest Lora we actually load. */
.shop-summary__total strong{ display:inline-block; font-family:var(--display); font-size:1.7rem; font-weight:700; letter-spacing:0; color:var(--notte); }
.shop-summary__total strong.bump{ animation:bump .32s var(--ease-out); }
@keyframes bump{ 0%{ transform:scale(1);} 38%{ transform:scale(1.12);} 100%{ transform:scale(1);} }
.shop-summary label{ display:block; font-size:var(--label); letter-spacing:.08em; text-transform:uppercase; color:var(--ink); margin-bottom:var(--form-field); font-weight:700; }
.shop-summary input{ width:100%; margin-top:var(--form-bind); font-family:var(--body); font-size:1rem; color:var(--notte); padding:12px 14px; border:1.5px solid var(--line); border-radius:12px; background:var(--panna); transition:border-color var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out); }
.shop-summary input:hover{ border-color:var(--oro-soft); }
/* The ring is the system's, not this form's. This used to be outline:none
    plus a 3px rgba(116,207,208,.3) halo, which measures 1.17:1 against the
    field — so the checkout's only keyboard-focus indicator was a 1.5px
    border changing colour. The border still changes; the outline is what
    you can actually see. */
.shop-summary input:focus{ border-color:var(--carbonara); }
/* ---- the field that is wrong ----
   Every refusal this form makes names ONE field and puts the caret in it
   (fail() in script.js). Focus alone is not the mark, though: the caret
   is where the customer is looking anyway, so a form refused twice for
   two different reasons looked identical both times. aria-invalid is
   already set for assistive tech; this is the same fact drawn.

   --warn-border, not a new red: the site has one "the answer is no"
   colour and the coverage verdict beside this field already wears it, so
   a second, louder red would make a blank name look more serious than an
   address we cannot deliver to. The halo is what carries at a glance —
   the 1.5px border alone is the same weight as the resting state.

   The halo and the focus ring occupy the SAME band and must not both be
   drawn. Measured: the halo is a 3px spread (0-3px outside the border
   box) and the system's focus ring is 2px at 1px offset (1-3px outside)
   — laid over each other they read as one muddy edge rather than as two
   facts. So a focused invalid field keeps the amber BORDER, which is
   what says "still wrong", and gives the band to the ring, which is
   what says "you are here". Nothing is lost: the two signals were
   always carried by different parts of the field. */
.shop-summary input[aria-invalid="true"],
.shop-summary input[aria-invalid="true"]:hover{
  border-color:var(--warn-border);
  box-shadow:0 0 0 3px rgba(226,201,140,.34);
}
.shop-summary input[aria-invalid="true"]:focus-visible{ box-shadow:none; }
/* The terms box is a field this form refuses too, and it is the one
   control here that is not an <input> in a label of its own — so the
   mark goes on the box itself rather than on a border it doesn't have. */
.shop-terms input[aria-invalid="true"]{
  outline:2px solid var(--warn-border); outline-offset:2px;
}
.shop-summary .btn{ margin-top:6px; width:100%; justify-content:center; }
/* Promoted out of .shop-summary: it was the only place a disabled button
    was styled in CSS, so auth.js was setting an inline opacity of its own
    (a fourth value, unthemeable, and invisible to this file) to do the same
    job on the account forms. One rule, one value, every button. */
.btn:disabled{ opacity:.45; cursor:not-allowed; transform:none; box-shadow:none; }
/* ---- BUSY, which is not UNAVAILABLE ----
   The rule above is right for a button you cannot press yet — an empty
   basket's "send". It is wrong for the two seconds after somebody has
   committed money: the same .45 fade said "unavailable" at the exact
   moment the honest message is "this is happening", and a greyed button
   with no motion is what makes a person tap it a second time.
   Still `disabled`, because a second submit must not land — this only
   changes what it LOOKS like. Full opacity, a spinner in the label's
   own colour, and `wait` rather than `not-allowed`, which is the
   difference between "no" and "hold on". script.js swaps the label to
   the same words the note above it is announcing. */
.btn.is-busy:disabled{ opacity:1; cursor:wait; }
.btn.is-busy::before{
  content:""; width:1em; height:1em; flex:none; border-radius:50%;
  border:2px solid currentColor; border-top-color:transparent;
  animation:btnSpin .7s linear infinite;
}
@keyframes btnSpin{ to{ transform:rotate(1turn); } }
/* The sheen is a hover flourish and has no business sweeping a button
   that is mid-request. */
.btn.is-busy::after{ display:none; }
/* Reduced motion kills the spin globally, which would leave a static
   ring pretending to be a spinner. The label already carries the state
   in words, so the ring simply goes. */
@media (prefers-reduced-motion:reduce){
  .btn.is-busy::before{ display:none; }
}
/* .7 measured 4.3:1 on the card's white — under AA at 12.8px, and the
   same miss .shop-hint carried at .66. Both sit at .78 now. */
.shop-summary__hint{ font-size:.8rem; opacity:.78; margin:1.1rem 0 0; }
/* The way out of the checkout that isn't the order. Quieter than the
   payment note above it — it is for the visit that went wrong, and
   should not compete with the one that is going right — but the link
   itself takes the same navy underline the terms link uses, because
   the form should have one way of saying "this is a link". */
.shop-help{ font-size:.78rem; line-height:1.5; opacity:.78; margin:.5rem 0 0; color:var(--ink); }

/* ============================================================ BASKET
   The shop's buy control, the basket's line items, and the bar that
   pins the total to the bottom of a phone. Everything here landed on
   2026-09-09, when the order form moved to /checkout and this panel
   stopped being a form with a total on top of it. */

/* ---- the card's buy control ----
   Nothing to fiddle with until there is something to fiddle with: at
   zero the card offers one button, and the stepper takes its place
   once the box is in the basket. script.js toggles [hidden] on the
   two; the min-height is what stops the card resizing as they swap
   (the pill and the stepper differ by 4px, which reads as a twitch on
   a row of three cards). */
.shop-card__buy{ display:flex; align-items:center; min-height:52px; }
/* the add button takes the row it is alone on, so the label fits on one
   line at the width a card body actually has */
.shop-card__add{ flex:1 1 auto; justify-content:center; padding-inline:1.2em; }
/* [hidden] has to be said out loud for every one of these. The UA rule
   is `[hidden]{display:none}` at specificity (0,0,1), and every element
   script.js toggles here sets a display of its own at (0,1,0) or more —
   .btn's inline-flex, .qty's inline-flex, this file's flex — all of
   which outrank it. Same fix, same reason, as #modalVideo below. */
.shop-card__add[hidden],
.qty[hidden],
.shop-summary__go[hidden],
.shop-summary__serves[hidden],
.checkout__empty[hidden]{ display:none; }

/* The count is a <span> now, not an <input> — see the note in
   shop.html. It keeps the input's measure so the stepper does not
   change width between 9 and 10. */
.qty{ display:inline-flex; align-items:center; gap:14px; border:2px solid var(--line); border-radius:999px; padding:5px 8px; background:#fff; }
.qty__btn{ width:40px; height:40px; border-radius:50%; border:none; background:var(--accent); color:#fff; font-size:1.2rem; line-height:1; font-weight:700; cursor:pointer; transition:filter var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out); display:grid; place-items:center; touch-action:manipulation; }
.qty__btn:hover{ filter:brightness(1.1); transform:scale(1.06); }
.qty__btn:active{ transform:scale(.94); }
.qty__val{ min-width:3.5ch; text-align:center; font-weight:700; font-size:1.05rem;
  color:var(--notte); font-family:var(--body); font-variant-numeric:tabular-nums; }
.qty__val.pop{ animation:bump .3s var(--ease-out); }

/* ---- a line in the basket ----
   Name, stepper, money, and the way out. The ✕ is the answer to the
   thing this basket was worst at: taking a box back out meant
   scrolling to the card it came from and pressing "−" as many times
   as you had pressed "+". */
.shop-summary__list li.cart-line{
  display:grid; grid-template-columns:1fr auto auto auto; align-items:center;
  gap:6px 12px; padding:10px 0; border-bottom:1px solid var(--line);
}
.cart-line__name{ font-size:.95rem; color:var(--notte); font-weight:600; min-width:0; }
.cart-line__qty{ display:inline-flex; align-items:center; gap:2px;
  border:1.5px solid var(--line); border-radius:999px; padding:2px; background:var(--panna); }
/* 32px, which is under the 44px target the cards' stepper meets — the
   basket is a correction surface sitting beside a total, and three
   44px circles a line would make it taller than the products. The ✕
   next to them is the full-size target for "take it out", and the
   card's own stepper is the full-size one for "how many". */
.cart-line__btn{
  width:32px; height:32px; border-radius:50%; border:none; background:transparent;
  color:var(--notte); font-size:1.05rem; line-height:1; font-weight:700; cursor:pointer;
  display:grid; place-items:center; touch-action:manipulation;
  transition:background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.cart-line__btn:hover{ background:var(--notte); color:#fff; }
.cart-line__btn:active{ transform:scale(.92); }
.cart-line__n{ min-width:2ch; text-align:center; font-weight:700; font-size:.95rem;
  color:var(--notte); font-variant-numeric:tabular-nums; }
.cart-line__sum{ font-style:normal; font-weight:700; color:var(--notte);
  font-variant-numeric:tabular-nums; }
.cart-line__rm{
  width:30px; height:30px; border-radius:50%; border:none; background:transparent;
  color:var(--ink-soft); cursor:pointer; display:grid; place-items:center;
  transition:background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
/* Literals rather than a reach for --warn-bg: that token means "this
   address is off our routes" and is a coverage verdict, not a colour
   ramp. Borrowing it would make a delete button and a refused delivery
   the same colour, and repainting one would silently repaint the other.
   7.6:1, so the glyph is still a glyph and not a smudge. */
.cart-line__rm:hover{ background:#F7E7E1; color:#8C2B12; }
.cart-line__btn:focus-visible,
.cart-line__rm:focus-visible{ outline:2px solid var(--notte); outline-offset:2px; }

/* how many people the basket feeds — the reason anybody buys a second
   box. inline rule rather than a flex item, for the reason spelled out
   on .hero__serves above: the sentence has a <strong> inside it. */
.shop-summary__serves{ margin:-.7rem 0 1.2rem; font-size:.86rem; color:var(--ink); }
.shop-summary__serves::before{ content:""; display:inline-block; vertical-align:middle;
  width:18px; height:2px; margin-right:.6em; background:var(--oro-soft); }
.shop-summary__serves strong{ color:var(--oro-ink); white-space:nowrap; }

.shop-summary__go{ width:100%; justify-content:center; }
/* The basket panel has no form in it any more, so the padding that was
   sized for seven fields now sits under four lines. */
.shop-summary--cart{ padding-bottom:clamp(20px,3vw,28px); }

/* ---- the pinned basket bar (phones) ----
   Below 920px .shop__inner is one column and the basket falls under
   all three cards, so the running total is a scroll away from every
   control that changes it. Above the breakpoint the panel is already
   sticky beside the cards and this would be a second copy of it, so it
   is display:none there — script.js only ever manages `hidden`, which
   is the "is there anything in it" half of the question. */
.cart-bar{ display:none; }
@media (max-width:920px){
  .cart-bar{
    position:fixed; left:0; right:0; bottom:0; z-index:60;
    display:flex; align-items:center; justify-content:space-between; gap:14px;
    padding:12px var(--gutter);
    padding-bottom:calc(12px + env(safe-area-inset-bottom));
    background:var(--white-soft); border-top:1px solid var(--line);
    box-shadow:0 -14px 30px -22px rgba(14,40,80,.55);
  }
  .cart-bar[hidden]{ display:none; }
  .cart-bar__sum{ display:flex; flex-direction:column; line-height:1.15;
    flex:0 1 auto; min-width:0; }
  /* both lines stay on their own line: the count wrapping to two rows
     was pushing the total under the button at 375px */
  .cart-bar__n{ font-size:var(--label-sm); letter-spacing:.12em; text-transform:uppercase;
    color:var(--oro-ink); font-weight:700; white-space:nowrap; }
  .cart-bar strong{ font-family:var(--display); font-size:1.3rem; font-weight:700;
    color:var(--notte); font-variant-numeric:tabular-nums; white-space:nowrap; }
  .cart-bar strong.bump{ animation:bump .32s var(--ease-out); }
  /* "Към данните за доставка" is 24 characters and does not fit beside a
     price on a 375px phone — the bar carries the short label instead
     (shop.next), and the full sentence stays on the panel above, where
     there is a column to say it in. */
  .cart-bar .btn{ flex:0 0 auto; padding-inline:1.35em; }
  /* the bar covers the foot of the page, so the page grows a foot */
  body[data-page="shop"] .site-footer{ padding-bottom:96px; }
}

/* ============================================================ CHECKOUT
   /checkout: the delivery form on the left, the basket on the right.
   The panels are both .shop-summary — that class is where this site's
   form furniture lives — so this block only has to say where they go. */
.checkout{ padding:clamp(40px,6vw,72px) 0 clamp(64px,10vw,128px); }
.checkout__inner{ display:grid; grid-template-columns:1.1fr .9fr;
  gap:clamp(28px,4vw,56px); align-items:start; }
/* The form is the tall column; sticking it would be sticking the whole
   page. The basket beside it is short enough to follow. */
.checkout__form{ position:static; }
/* sticky lives on .checkout__side now — see below */
.checkout__recap{ position:static; }
.checkout__empty{
  grid-column:1 / -1; display:flex; flex-wrap:wrap; align-items:center; gap:18px;
  justify-content:center; text-align:center;
  background:var(--white-soft); border:1px solid var(--line); border-radius:24px;
  padding:clamp(28px,5vw,48px); color:var(--ink);
}
.checkout__inner.is-empty > .checkout__form,
.checkout__inner.is-empty > .checkout__side,
.checkout__inner.is-empty > .checkout__recap{ display:none; }

/* ---- the sticky side column: basket, then coverage map ----
   Sticky moved from .checkout__recap to this wrapper. Two separately
   sticky panels in one grid cell pin to the same top offset and land on
   top of one another; one sticky parent carries both. */
.checkout__side{ position:sticky; top:110px; display:grid; gap:clamp(16px,2vw,24px); align-content:start; }

.checkout__map{ background:var(--white-soft); border:1px solid var(--line); border-radius:24px;
  padding:clamp(16px,2vw,22px); }
.checkout__map .form-eyebrow{ margin-top:0; }
/* Concise: about a third of the height the /stockists map gets. That map
   is the content of its page; this one is a confirmation of one address,
   and it sits under a basket that must stay on screen with it. */
.checkout__mapCanvas{
  position:relative; z-index:0;
  height:clamp(210px,26vh,280px);
  border:1px solid var(--line); border-radius:16px; overflow:hidden;
  background:var(--panna-2);
}
.checkout__mapHelp{ margin:.75rem 0 0; font-size:.82rem; color:var(--ink-soft); }

@media (max-width:920px){
  /* display:contents dissolves the wrapper so the three panels are grid
     items again and can be ordered: basket (what they built), form (what
     they came to do), map (the confirmation). Without this the map
     rides above the form on a phone. */
  .checkout__side{ display:contents; }
  .checkout__recap{ order:1; }
  .checkout__form{ order:2; }
  .checkout__map{ order:3; }
  .checkout__mapCanvas{ height:clamp(200px,34vh,260px); }
}

/* ---- where you are in the order ----
   Three steps and the customer is on the last one. It is a list of
   states, not a control: only the step you can usefully go back to is
   a link.
   NOT `.steps` — that name is taken, by the numbered cooking steps on
   /products, and it is a column of 38px coloured discs. A second
   meaning for one class name is a bug that only shows up on whichever
   page you happen not to be looking at. */
.ordersteps{ display:flex; flex-wrap:wrap; align-items:center; gap:10px 8px;
  list-style:none; margin:1.4rem 0 0; padding:0;
  font-size:var(--label-sm); letter-spacing:.12em; text-transform:uppercase; font-weight:700; }
/* ---- measured on the band it actually sits on (2026-09-11) ----
   This list lives inside .page-head, which on /checkout is the full
   carbonara field rgb(116,207,208) — not a pale tint of it. Two things
   were wrong there and neither was visible in the source:

     · the completed steps were --oro-ink, gold, at 11.2px: 3.05:1,
       under the 4.5:1 AA asks of text that size. Exactly the bug this
       file already recorded for .page-head__note strong, which was the
       same token on the pomodoro band. --accent-deep was the fix THERE
       and is NOT the fix here: on the turquoise it resolves to
       --carbonara-deep and measures 3.32:1, still short. --notte is
       6.94:1, and it is also what the 2026-09-10 sweep settled on for
       text on a flavour field.
     · the ● marking the CURRENT step was --carbonara on --carbonara-
       field: 2.29:1, under the 3:1 WCAG 2.4.11/1.4.11 asks of a
       non-text indicator. The one mark telling somebody which of three
       steps they are on was the least visible thing in the list.

   With every step now one ink, the state is carried by a MARK instead
   of a tint — which is the better answer regardless of contrast, since
   "gold means done" is a convention the customer has to be taught and
   a ✓ is not. The current step takes the solid navy pill the rest of
   the site already uses for its primary control (--panna on --notte,
   11.79:1), so the strongest object on the strip is where you are. */
.ordersteps__i{ display:flex; align-items:center; gap:8px; color:var(--notte); }
.ordersteps__i + .ordersteps__i::before{ content:""; width:18px; height:1.5px; background:var(--notte); opacity:.32; }
.ordersteps__i a{ color:inherit; text-decoration:none; border-bottom:1px solid transparent; }
.ordersteps__i a:hover{ border-bottom-color:currentColor; }
/* Pseudo-elements, so applyLang() — which replaces the innerHTML of the
   [data-i18n] child on every language switch — cannot destroy them.

   DRAWN, not typed. This was a "✓" character for an hour, which is a
   glyph from whatever face happens to resolve it standing in for an
   icon — it changes shape with the font stack and has no relationship
   to the two ticks this site already draws (.paynote__facts li::before
   and .shop-zone.is-ok::after, both a rotated corner of two borders).
   Same construction here, in currentColor so it stays in step with the
   step's ink rather than needing a token of its own. */
.ordersteps__i.is-done > a,
.ordersteps__i.is-done > span{ position:relative; padding-left:1.35em; }
.ordersteps__i.is-done > a::before,
.ordersteps__i.is-done > span::before{
  content:""; position:absolute; left:0;
  width:.5em; height:.26em;
  border-left:2px solid currentColor; border-bottom:2px solid currentColor;
  /* top:50% + translateY(-50%) rather than the hand-tuned em offset the
     site's other two ticks use: those sit against text whose line box
     they do not share, and this one does — measured 2.69px high of the
     line centre at 11.2px before this, which at tick size is most of
     the mark. Rotating about the default centre origin leaves the box
     centre where the translate put it, so the two agree at any size. */
  top:50%; transform:translateY(-50%) rotate(-45deg);
}
.ordersteps__i.is-now > span{
  background:var(--notte); color:var(--panna);
  padding:.36em .8em; border-radius:999px;
}

/* the one-line fact under a page-head paragraph — same inline
   construction, same reason */
/* same field, same argument as the lead above */
.page-head__note{ margin:.9rem 0 0; font-size:.95rem; color:var(--notte); }
.page-head__note::before{ content:""; display:inline-block; vertical-align:middle;
  width:22px; height:2px; margin-right:.65em; background:var(--oro-soft); }
/* --accent-deep, NOT --oro-ink. The gold is right for this emphasis
   everywhere else it appears — .hero__serves strong says the same
   "2 порции" in it, on cream, where --oro-ink is one of the three
   grounds that token was audited against at 5.2:1. This note is the
   one place it lands on the CANDY BAND, which was never in that set:
   gold on the pomodoro yellow measures 3.74:1 at full strength and
   3.29:1 under the .92 this paragraph carries. Two warm yellows an
   octave apart is not an emphasis, it is a stain.
   The page head already remaps --accent-deep per flavour (every
   .page-head[data-accent] sets it), and the eyebrow and the h1's <em>
   already take their colour from that family — so this follows the
   rule the block was written with instead of inventing one. On the
   shop's pomodoro head it resolves to 4.96:1 through the .92. */
.page-head__note strong{ color:var(--accent-deep); white-space:nowrap; }

@media (max-width:920px){
  .checkout__inner{ grid-template-columns:1fr; }
  .checkout__recap{ position:static; }
  /* the basket first on a phone: it is what the customer just built,
     and starting a seven-field form with no reminder of what it is for
     is how a checkout loses somebody */
  .checkout__form{ order:2; }
  .checkout__recap{ order:1; }
}
/* Padding on an INLINE element overflows its line box without moving
   it, so this grows the hit area from the 15px the text alone gave to
   past the 24px WCAG 2.5.8 asks — without opening a gap in the
   sentence it sits inside. Same standard the verdict's recovery link
   was held to. */
.shop-help a{ font-weight:700; color:var(--notte); text-decoration:underline; text-underline-offset:2px;
  padding:.42em .1em; }
.shop-help a:hover{ color:var(--carbonara-text); }

/* ============================================================ PAYMENT & TRADER
   The panel on /shop and /checkout carrying the acquirer's required
   disclosures. It is fine print by function and must not read as fine
   print you are meant to skip — so it gets a band of its own rather than
   a grey paragraph under the basket.

   Ground is --panna-2, the site's parchment, which is one of the three
   neutrals the ink tokens were audited against: --ink measures 8.63:1 on
   it, --ink-soft 4.70:1, --notte 10.71:1. The global focus ring is
   --notte, so it clears 3:1 here with nothing to override.

   NO opacity anywhere in this block. Every colour below is a token
   chosen against this ground; an inherited opacity is what turned six
   hand-picked colours into AA failures in the 2026-09-10 sweep. */
.paynote{ background:var(--panna-2); border-top:1px solid var(--line);
  padding:clamp(32px,5vw,56px) 0; position:relative; z-index:0; }
.paynote__title{ font-family:var(--display); font-size:clamp(1.15rem,2.2vw,1.4rem);
  color:var(--notte); margin:0 0 1rem; }
/* ---- which method is live TODAY ----
   Everything else in this panel is a standing declaration the acquirer
   requires; this one line is the state of play, and it has to read as a
   different KIND of statement or it is simply a fifth fact among four.
   So it takes the --ask ground — the site's "not an answer yet" colour,
   already carrying exactly this meaning under the address field two
   panels up — rather than a warning amber, which would make a payment
   route we have not switched on yet look like something going wrong.

   Built as the SAME OBJECT as .shop-zone: tinted ground, a 1px border
   of that state's own colour, 11px radius. This shipped for an hour as
   a 3px rule down the left edge instead, on the reasoning that the
   ground alone was too faint — and that reasoning was simply not
   measured. --ask-bg #EFF1F5 against this panel's --panna-2 #F5ECD8 is
   cool grey against warm parchment, a whole hue apart; it is
   .shop-zone.is-ok on --panna that was the two-point shift, which is a
   different pair entirely. With the ground doing the work there was
   nothing left for the rule to do but be a coloured left border, and
   this site does not have those.

   --ink for the body (not an alpha) and --notte for the <strong>: this
   is a panel where every colour was measured against its own ground,
   and an inherited opacity is what broke six of them in the 2026-09-10
   sweep. */
.paynote__status{
  margin:0 0 1.5rem; padding:.72em 1em; border-radius:11px;
  border:1px solid var(--ask-border); background:var(--ask-bg);
  color:var(--ink); font-size:.95rem; line-height:1.55; max-width:74ch;
}
.paynote__status strong{ color:var(--notte); }
.paynote__cols{ display:grid; grid-template-columns:1.05fr .95fr;
  gap:clamp(24px,4vw,56px); align-items:start; }
.paynote__lead{ margin:0 0 1rem; font-size:1rem; color:var(--ink); }
.paynote__sub{ font-family:var(--display); font-size:var(--label); letter-spacing:.1em;
  text-transform:uppercase; color:var(--oro-ink); margin:0 0 .7rem; }

/* ---- the card acceptance marks ----
   Drawn rather than imported: Mastercard's mark is two circles and their
   intersection, which is geometry and reproduces exactly; Visa's is a
   wordmark, which is type. Both sit in the same white chip at the same
   height so the row reads as one set rather than two borrowed images. */
.paynote__marks{ display:flex; flex-wrap:wrap; gap:10px; list-style:none; margin:0 0 1.1rem; padding:0; }
.paynote__mark{ display:grid; place-items:center; min-width:62px; height:38px; padding:0 12px;
  background:#fff; border:1px solid var(--line); border-radius:7px; }
.paynote__mark svg{ display:block; }
/* Visa's mark is the word. #1A1F71 is the brand's own blue and measures
   13.9:1 on the white chip. */
.paynote__mark--visa{ font-family:var(--body); font-weight:700; font-style:italic;
  font-size:1.02rem; letter-spacing:.06em; color:#1A1F71; }

.paynote__secure{ margin:0 0 1rem; font-size:.95rem; color:var(--ink); }
.paynote__facts{ list-style:none; margin:0; padding:0; display:grid; gap:.6rem; }
.paynote__facts li{ position:relative; padding-left:1.5em; font-size:.95rem; color:var(--ink); }
/* A tick rather than a bullet: every line here is a commitment, and four
   of them are the ones a cardholder is entitled to read before paying. */
.paynote__facts li::before{ content:""; position:absolute; left:.15em; top:.42em;
  width:.5em; height:.26em; border-left:2px solid var(--oro-ink);
  border-bottom:2px solid var(--oro-ink); transform:rotate(-45deg); }

.paynote__trader{ list-style:none; margin:0 0 1rem; padding:0; display:grid; gap:.45rem;
  font-size:.95rem; color:var(--ink); }
.paynote__trader strong{ color:var(--notte); }
.paynote__resp{ margin:0 0 1rem; font-size:.9rem; color:var(--ink-soft); }
.paynote__link{ margin:0; font-size:.95rem; }
.paynote a{ color:var(--notte); font-weight:600; text-decoration:underline; text-underline-offset:2px; }
.paynote a:hover{ color:var(--carbonara-text); }

@media (max-width:860px){
  .paynote__cols{ grid-template-columns:1fr; }
}
/* Touch targets live here and only here — a narrow desktop window does
   not match hover:none, so measuring at 375px in a desktop browser
   reports the ungrown sizes. Padding on an inline element overflows its
   line box without moving it, so this grows the target without opening
   gaps in the sentences these links sit inside. */
@media (hover:none){
  /* .66em, not .5em: measured on a 375px phone the phone-number and
     e-mail links came out 39.5px tall at .5em, and this block's whole
     job is ~44px. inline-block rather than inline here on purpose — the
     grown box participates in layout, so two links stacked in the
     trader list cannot end up with overlapping hit areas. */
  .paynote a{ display:inline-block; padding:.66em .12em; }
}

/* ============================================================ EXPERIENCE / VIDEOS */
.experience{ position:relative; background:var(--panna-2); padding:clamp(64px,10vw,120px) 0; overflow:hidden; }
.experience .ribbon{ position:absolute; top:0; left:0; right:0; }
/* ---- the band has to announce itself ----
   Parchment under cream is half a tone of difference and the eye reads
   it as more of the same page: people were scrolling straight past the
   videos. Three quiet things answer that, none of them a louder colour.
   A wash of gold down from the top edge, so the band STARTS somewhere
   instead of merely being a shade darker; the gold rule over the
   eyebrow, the same mark the story page opens on; and the title's em in
   gold rather than the inherited carbonara, because nothing on this
   band belongs to a flavour. The signpost at the foot of the products
   section (.lineup__next) is the fourth and does the actual work. */
.experience::before{ content:""; position:absolute; top:0; left:0; right:0;
  height:clamp(44px,7vw,80px); pointer-events:none;
  background:linear-gradient(180deg, rgba(176,141,87,.16), rgba(176,141,87,0)); }
.experience__inner{ display:grid; grid-template-columns:1.2fr .8fr; gap:clamp(32px,6vw,72px); align-items:center; }
.steps{ display:flex; flex-direction:column; gap:18px; margin-top:1rem; }
.steps li{ display:flex; align-items:flex-start; gap:16px; font-size:1.04rem; color:var(--ink); }
.steps li em{ font-style:normal; }
.steps span{ flex:0 0 auto; width:38px; height:38px; border-radius:50%; display:grid; place-items:center; font-family:var(--display); font-weight:700; color:#fff; font-size:1rem; }
.steps li:nth-child(1) span{ background:var(--carbonara-text); }
.steps li:nth-child(2) span{ background:var(--bolognese); }
.steps li:nth-child(3) span{ background:var(--pomodoro); }
.experience__qr{ display:flex; justify-content:center; }
.qr-card{ background:#fff; border:1px solid var(--line); border-radius:24px; padding:28px; text-align:center; box-shadow:0 36px 70px -34px rgba(14,40,80,.45); transform:rotate(2deg); transition:transform .3s var(--ease-out); }
.qr-card::before{ content:""; display:block; height:10px; border-radius:5px; margin:-8px -8px 18px;
  background:repeating-linear-gradient(90deg,var(--carbonara-field) 0 22px,var(--bolognese-field) 22px 44px,var(--pomodoro-field) 44px 66px); }
.qr-card:hover{ transform:rotate(0); }
.qr-card img{ width:208px; height:208px; margin:0 auto 14px; }
.qr-card p{ margin:0; font-size:var(--label-sm); letter-spacing:.26em; text-transform:uppercase; color:var(--oro-ink); font-weight:700; }

/* ---- videos section (bottom of the products page) ---- */
#videos{ scroll-margin-top:92px; }
.exp-head{ text-align:center; margin-bottom:clamp(2rem,4vw,2.8rem); }
.exp-head .eyebrow{ position:relative; padding-top:1.5rem; }
.exp-head .eyebrow::before{ content:""; position:absolute; top:0; left:50%;
  width:58px; height:3px; border-radius:2px; transform:translateX(-50%);
  background:linear-gradient(90deg, var(--oro-soft), var(--oro)); }
.exp-head .section-title em{ color:var(--oro-ink); }
.exp-head .section-sub{ margin-inline:auto; }
.video-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(16px,2.4vw,28px); margin-bottom:clamp(40px,6vw,64px); }
.video-card{ font-family:var(--body); cursor:pointer; padding:0; text-align:left; background:var(--white-soft); border:1px solid var(--line); border-radius:20px; overflow:hidden; box-shadow:0 24px 48px -34px rgba(14,40,80,.5); transition:transform .28s var(--ease-out), box-shadow .28s var(--ease-out);
  --accent:var(--carbonara-ink); --field:var(--carbonara-field); --accent-deep:var(--carbonara-deep); }
.video-card[data-accent="bolognese"]{ --accent:var(--bolognese-ink); --field:var(--bolognese-field); --accent-deep:var(--bolognese-deep); }
.video-card[data-accent="pomodoro"]{ --accent:var(--pomodoro-ink); --field:var(--pomodoro-field); --accent-deep:var(--pomodoro-deep); }
.video-card:hover{ transform:translateY(-6px); box-shadow:0 36px 60px -34px rgba(14,40,80,.6); }
.video-card__media{ position:relative; display:flex; align-items:flex-end; justify-content:center; height:210px; background:var(--field); overflow:hidden; }
.video-card__media::before{ content:""; position:absolute; inset:0; background:linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,0) 55%); }
.video-card__media img{ width:64%; filter:drop-shadow(0 16px 22px rgba(14,40,80,.3)); transform:translateY(6%); position:relative; z-index:1; transition:transform .38s var(--ease-out); }
.video-card:hover .video-card__media img{ transform:translateY(2%) scale(1.04); }
.video-card__play{ position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); z-index:2; width:58px; height:58px; border-radius:50%; background:rgba(255,255,255,.94); display:grid; place-items:center; box-shadow:0 12px 26px -8px rgba(0,0,0,.45); transition:transform var(--t-fast) var(--ease-out); }
.video-card__play::after{ content:""; position:absolute; inset:0; border-radius:50%; border:2px solid rgba(255,255,255,.85); opacity:0; }
.video-card:hover .video-card__play{ transform:translate(-50%,-50%) scale(1.09); }
.video-card:hover .video-card__play::after{ animation:ping 1.15s var(--ease-out) infinite; }
@keyframes ping{ 0%{ transform:scale(1); opacity:.75; } 100%{ transform:scale(1.55); opacity:0; } }
.video-card__play svg{ margin-left:3px; color:var(--accent); }
.video-card__label{ display:block; font-family:var(--display); font-size:1.3rem; color:var(--accent); padding:14px 20px 18px; }

/* ============================================================ FEEDBACK PROMPT (foot of the products page)
   The page's closing note, and the only thing on it that asks rather than
   tells — so it is deliberately quieter than the home page's CTA strip:
   cream under the parchment videos band, a faded gold hairline instead of a
   candy ribbon, and the house script drifting behind the words. Gold rather
   than one of the three flavour accents because this is us talking, not a
   box: an accent here would read as a fourth product. */
.feedback{ position:relative; background:var(--panna); overflow:hidden; scroll-margin-top:92px;
  padding:clamp(54px,8vw,88px) 0 clamp(60px,9vw,100px); }
/* the rule the section hangs from. Faded at both ends so it reads as the
   end of the band above rather than a border drawn round this one. */
.feedback::before{ content:""; position:absolute; top:0; left:0; right:0; height:1px;
  background:linear-gradient(90deg, transparent, var(--line) 14%, var(--line) 86%, transparent); }
/* the same script flourish the interior page heads carry — answering the
   ask rather than naming the brand again, and out of the pointer's way.
   It sits further off the bottom edge than it looks like it needs to: the
   section clips its own overflow and Parisienne hangs a long descender off
   the z, which an em-box-tight offset cuts straight through. */
.feedback::after{ content:"Grazie"; position:absolute; z-index:0; right:clamp(-10px,1.5vw,52px); bottom:clamp(10px,1.6vw,26px);
  font-family:var(--script); font-size:clamp(4rem,10vw,7.5rem); line-height:1; white-space:nowrap;
  color:var(--oro); opacity:.15; rotate:-4deg; pointer-events:none; }
.feedback__inner{ position:relative; z-index:1; max-width:58ch; margin-inline:auto; text-align:center; }
.feedback .eyebrow{ margin-bottom:.9rem; }
.feedback__title{ font-size:clamp(1.7rem,3.8vw,2.6rem); margin:0 0 .55em; }
/* italic like every other em in a heading here, gold like the eyebrow above it */
.feedback__title em{ font-style:italic; color:var(--oro-ink); }
.feedback__lead{ margin:0 0 1.9rem; color:var(--ink); font-size:1.04rem; }
.feedback__alt{ margin:1.1rem 0 0; font-size:.9rem; color:var(--ink-soft); }
/* the address is the path with no JS behind it, so it is written out and
   underlined rather than left to carry itself on colour alone */
.feedback__alt a{ color:var(--notte); font-weight:700; text-decoration:underline; text-underline-offset:2px;
  transition:color var(--t-fast) var(--ease-out); }
.feedback__alt a:hover{ color:var(--carbonara-text); }

/* ============================================================ CONTACT */
.contact{ padding:clamp(40px,6vw,72px) 0 clamp(64px,10vw,128px); }
.contact__inner{ display:grid; grid-template-columns:1fr 1fr; gap:clamp(36px,6vw,80px); }
.contact__details{ margin-top:1.4rem; display:grid; gap:2px; }
.contact__details > div{ display:flex; flex-wrap:wrap; gap:6px 14px; padding:14px 0; border-bottom:1px solid var(--line); }
.contact__details dt{ font-size:var(--label-sm); letter-spacing:.16em; text-transform:uppercase; color:var(--oro-ink); width:150px; padding-top:5px; font-weight:700; }
.contact__details dd{ margin:0; font-size:1.12rem; color:var(--notte); font-family:var(--display); }
.contact__details a{ transition:color var(--t-fast) var(--ease-out); }
.contact__details a:hover{ color:var(--carbonara-text); }

/* ---------- legal (terms / disclaimer) ---------- */
.legal{ padding:clamp(40px,6vw,72px) 0 clamp(64px,10vw,128px); }
.legal__updated{ font-size:.85rem; color:var(--ink-soft); margin:0 0 2.4rem; }
.legal__inner{ max-width:78ch; }
.legal__block{ margin-bottom:2.2rem; scroll-margin-top:110px; }
/* clauses are h3 because the two PART headings are the h2s — the page is
   one document with two parts, so the outline has to say so */
.legal__block h3{ font-size:1.34rem; margin:0 0 .8rem; color:var(--notte); }
.legal__block p{ margin:0 0 1em; }
.legal__block ul{ margin:0 0 1em; padding-left:1.3em; list-style:disc; }
.legal__block li{ margin-bottom:.5em; }
.legal__block a{ color:var(--carbonara-text); text-decoration:underline; text-underline-offset:2px; transition:color var(--t-fast) var(--ease-out); }
.legal__block a:hover{ color:var(--notte); }
.legal__block strong{ color:var(--notte); }
.legal__id{ margin:1.2rem 0 0; padding:1.2rem 1.4rem; background:var(--white-soft); border:1px solid var(--line); border-radius:14px; list-style:none; }
.legal__id li{ margin-bottom:.55em; font-size:.98rem; }
.legal__id li:last-child{ margin-bottom:0; }

/* ---- the two parts of the merged document ----
   The disclaimer stopped being its own page on 2026-08-14. A rule and a
   scale step are what stop the join reading as "two documents pasted
   together": the part heading is bigger than any clause under it, so the
   hierarchy is legible before a word is read. */
.legal__part{
  font-family:var(--display); font-size:clamp(1.5rem,3vw,1.9rem); color:var(--notte);
  margin:0 0 1.4rem; padding-top:1.4rem; border-top:2px solid var(--oro-soft);
  scroll-margin-top:110px;
}
.legal__part:not(:first-of-type){ margin-top:3.4rem; }
.legal__partLead{ margin:-.7rem 0 2.2rem; font-size:.95rem; opacity:.8; }

/* the contents list, built by script.js from the headings themselves */
.legal__toc{
  margin:0 0 2.8rem; padding:clamp(18px,2.4vw,26px) clamp(18px,2.6vw,28px);
  background:var(--white-soft); border:1px solid var(--line); border-radius:16px;
}
.legal__tocTitle{
  margin:0 0 .8rem; font-family:var(--display); font-weight:700; color:var(--notte);
  font-size:var(--label); letter-spacing:.16em; text-transform:uppercase;
}
.legal__toc ol{ margin:0; padding:0; list-style:none;
  display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:.35em 1.6em; }
.legal__toc li{ font-size:.9rem; line-height:1.45; }
.legal__toc li.is-part{ grid-column:1/-1; margin-top:.7em; font-weight:700; }
.legal__toc li.is-part:first-child{ margin-top:0; }
.legal__toc a{ color:var(--notte); text-decoration:none; }
.legal__toc a:hover{ color:var(--carbonara-text); text-decoration:underline; text-underline-offset:2px; }
.legal__toc li.is-part a{ color:var(--oro-ink); }
.legal__nav{ display:flex; flex-wrap:wrap; gap:.6em 1.4em; margin-top:2.6rem; padding-top:1.6rem; border-top:1px solid var(--line); }
.legal__nav a{ font-weight:700; color:var(--notte); text-decoration:underline; text-underline-offset:2px; }

/* a box drifts in the quiet space beneath the details (two-column layout only) */
@media (min-width:761px){
  .contact__intro::after{ content:""; display:block; height:clamp(200px,24vw,280px); margin-top:clamp(28px,4vw,52px);
    background:url("assets/bolognese.webp") center/contain no-repeat;
    filter:drop-shadow(0 24px 30px rgba(14,40,80,.22)); rotate:-6deg;
    pointer-events:none; }
}
.contact__form{ position:relative; background:#fff; border:1px solid var(--line); border-radius:24px; padding:clamp(24px,4vw,40px); box-shadow:0 36px 70px -40px rgba(14,40,80,.4); overflow:hidden; }
.contact__form::before{ content:""; position:absolute; top:0; left:0; right:0; height:8px;
  background:repeating-linear-gradient(90deg,var(--carbonara-field) 0 22px,var(--bolognese-field) 22px 44px,var(--pomodoro-field) 44px 66px); }
/* These are the section headings of the site's inner pages — "Пишете
   ни", "Вашата поръчка", "Статус на доставката" — and as of 2026-08-23
   they are <h2>, not <p>. Five of six pages had literally no <h2> at
   all, so a screen reader browsing by heading got one landmark for the
   whole page while a sighted visitor got a 1.9rem script heading.

   Both declarations below exist BECAUSE of that tag change, and
   removing either undoes it visibly:
     font-weight  h1,h2,h3 set 600, and Parisienne ships one weight
                  (400). At 600 the browser synthesises the bold and
                  smears a script face.
     line-height  the same rule sets 1.04. As a <p> this inherited the
                  body's 1.6 — measured 48.6px against 31.6px, and at
                  31.6px Parisienne's descenders are clipped outright
                  (scrollHeight 36 in a 31.6px box) by the overflow:
                  hidden on .contact__form, .shop-summary and
                  .track-card. 1.6 reproduces the <p> exactly. */
.form-eyebrow{ font-family:var(--script); font-size:1.9rem; color:var(--notte);
  font-weight:400; line-height:1.6; margin:.4rem 0 1.2rem; }
.contact__form label{ display:block; font-size:var(--label); letter-spacing:.08em; text-transform:uppercase; color:var(--ink); margin-bottom:14px; font-weight:700; }
.contact__form input, .contact__form textarea{ width:100%; margin-top:7px; font-family:var(--body); font-size:1rem; color:var(--notte); padding:12px 14px; border:1.5px solid var(--line); border-radius:12px; background:var(--panna); transition:border-color .25s, box-shadow .25s; }
.contact__form input:hover, .contact__form textarea:hover{ border-color:var(--oro-soft); }
.contact__form input:focus, .contact__form textarea:focus{ border-color:var(--carbonara); }
.contact__form textarea{ resize:vertical; }
.contact__form .btn{ margin-top:6px; width:100%; justify-content:center; }
.form-note{ font-size:.85rem; margin:1rem 0 0; min-height:1.2em; color:var(--bolognese); font-weight:600; }
/* On the order form the note sits ABOVE the send button (see shop.html),
   so its margin has to hug the button rather than push away from a
   control above it. Scoped to .shop-summary because contact.html's copy
   still sits after its button, where the original margin is right. */
.shop-summary .form-note{ margin:2px 0 8px; }

/* ============================================================ FOOTER */
.site-footer{ background:var(--notte-deep); color:#D0C9B7; }
.footer-stripe{ height:6px;
  background:repeating-linear-gradient(90deg, var(--carbonara-field) 0 60px, var(--bolognese-field) 60px 120px, var(--pomodoro-field) 120px 180px); }
.footer__inner{ display:flex; flex-wrap:wrap; align-items:center; gap:18px 40px; padding-top:48px; padding-bottom:20px; }
.footer__brand{ display:flex; flex-direction:column; align-items:center; line-height:1; }
.footer__brand .wordmark__name{ color:var(--panna); font-size:1.5rem; font-family:var(--display); font-style:italic; }
.footer__brand .wordmark__sub{ font-family:var(--script); color:var(--oro-soft); font-size:1.15rem; }
.footer__nav{ display:flex; flex-wrap:wrap; gap:8px 22px; }
.footer__nav a{ font-size:.82rem; letter-spacing:.04em; color:#C9C2B0; transition:color var(--t-fast) var(--ease-out);
  display:inline-flex; align-items:center; min-height:32px; padding:2px 0; }
.footer__nav a:hover{ color:var(--carbonara-field); }
.footer__tag{ font-family:var(--display); font-style:italic; font-size:1.05rem; color:#E9E2D2; margin:0 auto 0 0; }
/* #8F8A78 was 4.53:1 on the deep navy — over AA, but by 0.03 at .74rem fine
   print, close enough that rounding could tip it under. Lifted to a taupe that
   clears it with room (5.8:1) and stays recessive against the footer body. */
/* The copyright line had `margin:0`, so it sat 20px under a tagline set
   in 1.05rem italic display — close enough that the two read as one
   wrapped paragraph, with the quieter half of it looking like a
   rendering fault rather than fine print. A hairline and real space
   above make it what it is: the bottom of the page, below everything
   else, rather than the last line of the sentence over it. */
.footer__legal{ font-size:.74rem; letter-spacing:.06em; color:#A39D89;
  margin:0 auto; padding-top:22px; padding-bottom:34px;
  border-top:1px solid rgba(255,255,255,.09); }

/* ============================================================ VIDEO MODAL */
.modal{ position:fixed; inset:0; z-index:200; display:flex; align-items:center; justify-content:center; padding:24px; }
.modal[hidden]{ display:none; }
/* script.js holds the modal for one beat on close so it can fade instead of
   blinking out; the video is torn down when the fade finishes */
.modal.is-closing{ pointer-events:none; animation:modalOut .2s var(--ease-out) forwards; }
@keyframes modalOut{ to{ opacity:0; } }
.modal__backdrop{ position:absolute; inset:0; background:rgba(10,18,38,.82); -webkit-backdrop-filter:blur(4px); backdrop-filter:blur(4px); animation:backdropIn .22s var(--ease-out); }
@keyframes backdropIn{ from{ opacity:0; } to{ opacity:1; } }
@keyframes modalIn{ from{ opacity:0; transform:translate3d(0,14px,0) scale(.97); } to{ opacity:1; transform:none; } }
.modal__panel{ position:relative; z-index:1; width:min(560px,100%); background:#000; border-radius:16px; overflow:hidden; box-shadow:0 40px 90px -30px rgba(0,0,0,.7); animation:modalIn .26s var(--ease-out); }
/* Shown in place of the <video> when the third-party host cannot be reached.
   On the panel's black, so the link takes the packaging turquoise rather than
   the teal ink the light pages use — 11.6:1 here, where --carbonara-text
   would be 2.6:1. Padded to clear the close button in the corner. */
.modal__fallback{ margin:0; padding:48px 24px 28px; text-align:center; color:var(--panna);
  font-size:.95rem; line-height:1.6; }
.modal__fallback[hidden]{ display:none; }
.modal__fallback a{ display:inline-block; margin-top:10px; font-weight:700;
  color:var(--carbonara-field); text-decoration:underline; text-underline-offset:3px; }
.modal__fallback a:hover{ color:#fff; }
/* Not redundant with the hidden attribute: .modal__panel video below sets
    display:block at (0,1,1), which outranks the UA's own [hidden] rule — so
    without this the video stays visible behind the fallback message. */
#modalVideo[hidden]{ display:none; }
.modal__panel video{ width:100%; display:block; max-height:82vh; background:#000; }
.modal__close{ position:absolute; top:10px; right:10px; z-index:2; width:40px; height:40px; border-radius:50%; border:none; background:rgba(255,255,255,.94); color:var(--notte-deep); font-size:1.6rem; line-height:1; cursor:pointer; transition:transform var(--t-fast) var(--ease-out); }
.modal__close:hover{ transform:scale(1.08) rotate(90deg); }

/* ============================================================ REVEAL */
/* Entrances are deliberately short: a reveal that outlasts the scroll that
   triggered it reads as the page struggling to keep up. ~.45s in, ~.07s
   between siblings — a three-card grid is fully settled in under .6s. */
.reveal{ opacity:0; transform:translate3d(0,18px,0); transition:opacity .45s var(--ease-out), transform .45s var(--ease-out); }
.reveal.is-visible{ opacity:1; transform:none; }

/* Staggered reveal for card grids: children follow one another in.
   This is an ANIMATION, not a transition, on purpose. The old
   `.reveal-stagger > * { transition: … }` sat later in the file than the card
   rules and therefore replaced them — every .flavour-card / .shop-card /
   .video-card was doing its hover lift on the reveal's timing and never
   transitioning its box-shadow at all. animation-fill-mode:backwards holds the
   start frame through the delay and then hands the element back to its own
   styles, so hover and press stay under the component's control. */
.reveal-stagger > *{ opacity:0; }
.reveal-stagger.is-visible > *{ opacity:1; animation:staggerIn .42s var(--ease-out) backwards; }
.reveal-stagger.is-visible > *:nth-child(2){ animation-delay:.07s; }
.reveal-stagger.is-visible > *:nth-child(3){ animation-delay:.14s; }
.reveal-stagger.is-visible > *:nth-child(4){ animation-delay:.21s; }
.reveal-stagger.is-visible > *:nth-child(n+5){ animation-delay:.26s; }
@keyframes staggerIn{ from{ opacity:0; transform:translate3d(0,14px,0); } to{ opacity:1; transform:none; } }

/* ============================================================ RESPONSIVE */
/* mid widths: compact the header so nav + account pill + language all fit */
/* Two steps of tightening rather than one, since 2026-08-30: the nav's
   fifth item is long enough that the old single step ran out of room
   before the drawer took over. The worst case the header has to hold on
   one line is a full nav, the cart pill AND a 14-character first name in
   the account pill — that is what these were sized against. */
@media (max-width:1200px){
  .header-inner{ gap:16px; }
  .nav{ gap:18px; }
  .nav__cta{ font-size:.85rem; }
  .lang--split{ padding-left:13px; }
}
@media (max-width:1080px){
  .header-inner{ gap:12px; }
  .nav{ gap:14px; }
  .nav a{ font-size:.82rem; }
  /* the store pill gives up its padding before the nav gives up its
     gaps: it is the widest item in the row and the only one carrying
     any, so it is the cheapest place to find the last 20px before the
     whole header folds into the drawer at 1024px */
  .nav a[data-page="shop"]{ padding:.5em .8em .5em .68em; gap:.36em; }
  .lang--split{ padding-left:10px; }
}
/* The header collapses into the drawer below 1024px. It was 760px until
   2026-08-30, when "Къде да ни намерите" joined the nav: at 19 characters
   it measures ~160px, and five items plus the wordmark, the cart pill, the
   account button and the language switch stop fitting somewhere around
   1000px — measured, not guessed. A nav item is `white-space:nowrap`, so
   the alternative to collapsing early is a row that clips rather than one
   that wraps; the drawer already carries the same links (auth.js puts the
   account one in it), at a size worth tapping. */
@media (max-width:1024px){
  .nav, .nav__cta, .header-inner > .lang{ display:none; }
  .nav-toggle{ display:flex; }
  /* The basket stays even with nothing in it here — it is the header's
     only door to the shop once the nav has folded away. Padded even on
     both sides, because with the count suppressed it is a lone glyph
     and the 12/9 asymmetry that balances "trolley + number" leaves a
     single icon sitting off-centre in its own pill. */
  .cart-pill.is-empty{ display:inline-flex; padding:9px 11px; }
  .cart-pill.is-empty .cart-pill__n{ display:none; }
}
/* narrow page heads read better centred, without the script flourish */
@media (max-width:760px){
  .page-head__inner{ text-align:center; }
  .page-head p{ margin-inline:auto; }
  .page-head::after{ display:none; }
  /* and the feedback band's flourish goes for the same reason, one step
     sharper: below this the band is narrow enough that "Grazie" lands on
     the line carrying the email address, and decoration is what gives way */
  .feedback::after{ display:none; }
}
@media (max-width:920px){
  .hero__inner{ grid-template-columns:1fr; gap:10px; }
  .hero{ padding-top:130px; min-height:auto; }
  /* single-column hero: centre the copy over the stage below it */
  .hero__copy{ text-align:center; }
  .hero__lead{ margin-inline:auto; }
  .hero__actions{ justify-content:center; }
  .hero__stage{ height:clamp(330px,72vw,460px); margin-top:14px; }
  /* the quote stops being a column and becomes the break under the
     paragraph again — squeezed into half of a narrow page it was about
     nine characters a line, which is not a pull quote, it's a stack */
  .story__cols{ grid-template-columns:minmax(0,1fr); gap:clamp(22px,4vw,30px); }
  .story__cols .pull-quote{ border-radius:0 18px 18px 0; }
  .trio__grid{ grid-template-columns:1fr; max-width:460px; margin-inline:auto; }
  .video-grid{ grid-template-columns:1fr; max-width:460px; margin-inline:auto; }
  .product{ grid-template-columns:1fr; text-align:center; }
  /* The alcove has the whole page width here and the box does not grow
     with it, so the room around it — generous on a desktop panel where
     it is one of two columns — becomes distance to scroll past. Floor
     and headroom both come in; the box keeps its size, and the floor
     stays deep enough for the shadow it stands on (that is what makes
     it a box standing somewhere rather than a cut-out). Written as
     --alcove-pad, not as padding, because the open state works its
     retreat out from the floor and a second number would drift. */
  .product__media{ min-height:auto; --alcove-pad:clamp(46px,7vw,66px);
    padding-top:clamp(18px,3vw,34px); }
  /* the panel shrinks here, the light around the phone doesn't: the
     desktop weight washes out on a handset held in daylight. There is
     also no hover to answer on a handset, so this is the only state
     the signature has and it carries the whole effect. */
  .product__panel{ --word-o:.6; }
  /* the box is a bigger share of a phone-width panel, so at 11% the
     roof stopped cutting across the signature and started swallowing
     it — this restores the desktop relationship between the two */
  .product__panel::before{ top:6%; }
  .product__desc{ margin-inline:auto; }
  .chips, .badges{ justify-content:center; }
  .product__offer{ justify-content:center; }
  /* the shared claims strip centres with the rest of the single-column
     panel; its own hairline still divides it from the product above */
  .lineup__claims{ justify-content:center; text-align:center; }
  .shop__inner{ grid-template-columns:1fr; }
  .shop-summary{ position:static; }
  .experience__inner, .contact__inner{ grid-template-columns:1fr; }
  .contact__details dt{ width:auto; }
}
@media (max-width:680px){
  .values__grid{ grid-template-columns:1fr; gap:4px; }
  .value + .value{ border-left:none; border-top:1px solid rgba(255,255,255,.16); padding-top:24px; margin-top:8px; }
  /* The outer two used to be pushed far enough out that they were sliced by
     the viewport edge (body has overflow-x:hidden, so it read as a crop, not
     as a bleed). Narrower boxes + a smaller offset keeps all three whole and
     inside the colour panel, the way they sit on desktop. */
  .box{ width:clamp(140px,40vw,196px); }
  /* same .87 as the desktop rule above, for the same reason */
  .box--bolognese .box3d{ --b-w:clamp(122px,34vw,168px); }
  .box--carbonara .box3d,
  .box--pomodoro  .box3d{ --b-w:clamp(112px,31vw,152px); }
  .box--carbonara{ left:4%;  transform:translateX(-20%) rotate(-8deg) scale(.84); }
  .box--pomodoro{  right:4%; transform:translateX(20%)  rotate(8deg)  scale(.84); }
  .hero__title{ font-size:clamp(2.6rem,12vw,3.6rem); }
  .shop-card{ grid-template-columns:1fr; }
  /* still shorter than desktop, but the 3D box is 1.28 × --b-w tall and
     the media clips — 180px sliced the bottom off it */
  .shop-card__media{ min-height:212px; }
  /* full card width now, so the signature can be read at the size the
     wall actually gives it — same reason .product__panel gains weight */
  .shop-card__panel{ --word-o:.56; }
  .shop-card__panel::before{ font-size:1.9rem; left:7%; }
  .shop-card__body{ align-items:center; text-align:center; }
  .pull-quote{ padding-left:18px; }
  /* one step further in on a phone: the box is 60vw here, so the alcove
     is already most of a screen before any padding is added to it */
  .product__media{ --alcove-pad:44px; padding-top:16px; }
  .lineup__stage{ padding:26px 18px; }
  /* Eight lines of centred ragged text is a paragraph you have to hunt
     the start of on every line. The headings above it stay centred with
     the rest of the single-column panel — it is the running copy, and
     only the running copy, that gets an edge to come back to. */
  .product__desc{ text-align:left; }
  /* and the gaps between the panel's parts come in with everything else:
     these were spaced for a two-column panel where the copy is only half
     the width, and stacked they add a screen of nothing to scroll */
  .product__claim{ margin-bottom:1.4rem; }
  .chips{ margin-bottom:1.4rem; }
  .product__offer{ margin-top:1.6rem; }
  .footer__inner{ flex-direction:column; align-items:center; text-align:center; gap:18px; }
  .footer__nav{ justify-content:center; }
  .footer__tag{ margin:0; }
}

/* small phones (≤420px): tighter chrome, same hierarchy */
@media (max-width:420px){
  /* the gutter itself, so the safe-area block below adds its inset to THIS
     rather than re-imposing the wide clamp — which is what it was doing */
  :root{ --gutter:16px; }
  .header-inner{ padding-block:12px; gap:14px; }
  .wordmark__name{ font-size:1.35rem; }
  .wordmark__sub{ font-size:.95rem; }
  .page-head{ padding:128px 0 44px; }
  .lineup__stage{ padding:22px 14px; border-radius:20px; }
  /* the three flavours are the page's primary control and they were
     wrapping onto two rows here, which puts the third one below the fold
     of the tab strip; tighter keeps all three side by side */
  .lineup__tabs{ gap:8px; margin-bottom:1.8rem; }
  .tab{ font-size:.82rem; padding:.62em .9em; white-space:nowrap; }
  .qr-card{ padding:20px; }
  .qr-card img{ width:164px; height:164px; }
  .footer__inner{ gap:16px; }
  .shop-summary{ padding:20px 16px; }
  .btn-order{ width:100%; justify-content:center; }
}

/* One row of flavour tabs, but only where three of them fit. Measured at
   the tighter size above: the three come to 318px including their gaps,
   against the 343px a 375px viewport leaves inside the gutter — and 288px
   at 320px wide, where they do not fit and wrapping is the honest answer.
   361px is where the arithmetic turns over. */
@media (max-width:420px) and (min-width:361px){
  .lineup__tabs{ flex-wrap:nowrap; }
}

/* short landscape phones: don't let fixed chrome eat the screen */
@media (max-height:540px) and (orientation:landscape){
  .hero{ min-height:auto; padding:120px 0 60px; }
  .page-head{ padding:118px 0 36px; }
  .mobile-nav{ gap:0; }
  .mobile-nav a{ font-size:1.4rem; padding:.2em; }
  .lang--drawer{ margin-top:12px; }
}

/* Notched devices: keep content clear of the safe areas.

   These rules were written correctly and did nothing at all until
   2026-09-02, because env(safe-area-inset-*) resolves to ZERO unless the
   viewport meta opts in with viewport-fit=cover — and it did not. Every
   .html now carries it, which is what switches this block on.

   The manifest is display:"browser", so there is no standalone mode and
   no top inset to handle: iOS puts its own chrome above the page in
   portrait, and in landscape the insets are left/right. What the page
   does now cover is the home-indicator strip at the bottom, which is why
   the footer and the cookie banner appear here — the alternative is a
   dark footer ending in a cream stripe under the indicator. */
@supports (padding: env(safe-area-inset-left)){
  .header-inner, .container, .hero__inner{
    padding-left:calc(var(--gutter) + env(safe-area-inset-left));
    padding-right:calc(var(--gutter) + env(safe-area-inset-right));
  }
  .mobile-nav{ padding-top:env(safe-area-inset-top); padding-bottom:env(safe-area-inset-bottom); }
  /* on the footer itself, not on .footer__legal, so the dark ground
     extends under the indicator instead of the body's cream showing */
  .site-footer{ padding-bottom:env(safe-area-inset-bottom); }
  /* both are fixed to an edge the notch reaches in landscape */
  .cookie-banner{
    padding-left:calc(clamp(16px,4vw,32px) + env(safe-area-inset-left));
    padding-right:calc(clamp(16px,4vw,32px) + env(safe-area-inset-right));
  }
  .modal{
    padding-left:calc(24px + env(safe-area-inset-left));
    padding-right:calc(24px + env(safe-area-inset-right));
    padding-bottom:calc(24px + env(safe-area-inset-bottom));
  }
}

/* touch screens: no lift-on-hover states left "stuck" after a tap */
@media (hover:none){
  .flavour-card:hover, .video-card:hover, .shop-card:hover{ transform:none; box-shadow:0 24px 48px -34px rgba(14,40,80,.5); }
  .tab:hover{ transform:none; }
  .qty__btn:hover{ transform:none; filter:none; }
  .timeline li:hover{ transform:none; }
  .video-card:hover .video-card__play::after{ animation:none; }
  .flavour-card:hover .flavour-card__top img{ transform:translateY(4%); }
  .video-card:hover .video-card__media img{ transform:translateY(6%); }
  .shop-card:hover .shop-card__media img{ transform:translateY(5%); }
  .video-card:hover .video-card__play{ transform:translate(-50%,-50%); }
  .qr-card:hover{ transform:rotate(2deg); }
  .box:hover{ filter:drop-shadow(0 30px 40px rgba(14,40,80,.34)); }
}

/* touch screens: grow every control to a thumb-sized hit area (~44px, per Apple
   HIG / WCAG 2.5.5) without redrawing anything. Two techniques do all the work:
   min-width/min-height widens a flex button around unchanged artwork, and
   vertical padding on an *inline* link extends its hit region while leaving the
   line box — and therefore the paragraph's rhythm — exactly as it was. */
@media (hover:none){
  /* the hamburger is the most-tapped control on the site; it was 42×33.5 */
  .nav-toggle{ min-width:44px; min-height:44px; align-items:center; justify-content:center; }

  /* language switch — dropdown options in the header, BG/EN pair in the drawer */
  .lang-menu__list .lang__btn{ padding:12px; }
  .lang--drawer .lang__btn{ min-height:44px; padding:8px 16px; }

  /* shop quantity steppers, tapped repeatedly while building an order */
  .qty__btn{ width:44px; height:44px; }

  /* (.pw-toggle and .auth-link are grown in account.css — it loads after this
     file, so an override here would lose on source order at equal specificity) */

  .footer__nav a{ padding:12px 0; }

  /* flavour tabs on the products page and the standalone CTAs */
  .tab{ min-height:44px; }
  .link-arrow{ padding-block:9px; }
  .payfail-contact{ padding-block:11px; }

  /* phone/email/site on the contact page and the phone number on the order
     tracker are those pages' primary actions, so they get real hit areas */
  .contact__details a{ padding-block:11px; }
  .track-mock a{ padding-block:15px; }

  /* the map's zoom / recentre controls (stockists + shop picker) and the video
     modal's close — both tapped on a phone, both were under the 44px floor
     (38 and 40). The centred artwork is unchanged; only the hit area grows. */
  /* .smap__controls, not a bare .smap__btn, and the extra class is
     load-bearing: the map's own rules live ~1800 lines BELOW this block,
     so at equal specificity `width:38px` down there won the cascade and
     this rule did nothing at all — measured 38x38 under real device
     emulation on 2026-09-11, three years of this line looking correct in
     the source. (A narrow desktop window would not have caught it
     either: hover:none does not match there, which is what the note at
     the head of this block is about.) The parent class is the cheapest
     honest way to win without moving the rule out of the one block where
     touch targets are supposed to be findable. */
  .smap__controls .smap__btn{ width:44px; height:44px; }
  .modal__close{ width:44px; height:44px; }

  /* The consent buttons were never in this pass and are the first thing a
     phone is asked to tap on the site: 39px stacked in portrait, and 30px
     side-by-side under the landscape rule above. min-height rather than a
     padding change, so neither layout moves. */
  .cookie-banner__btn{ min-height:44px; }

  /* The basket pill, which this pass predates — it was 39px, and below
     1024px it is the header's ONLY way into the shop (the nav is in the
     drawer), so it is the most consequential control on the strip. The
     wordmark was 38px. Both are free: the header measures 68px on a
     phone either way, so there is slack above and below both and
     nothing moves. */
  .cart-pill{ min-height:44px; }
  .wordmark{ min-height:44px; justify-content:center; }

  /* THE BASKET'S ✕, and it is the hit area that grows, not the button.
     The rule up at .cart-line__btn reasons the − and + down to 32px on
     purpose — the basket is a correction surface beside a total, and
     three 44px circles a line would make it taller than the products —
     and then says "the ✕ next to them is the full-size target for take
     it out". It was 30px, the SMALLEST control in the basket, so the
     file's own stated intent was the one thing it did not do.
     Sizing it to 44 would cost 11px a line, measured, which is the
     objection the original comment raised. So the artwork stays 30px
     and an inset:-7px pseudo-element carries a 44x44 hit area around
     it: the layout is byte-for-byte what it was, and the destructive
     control is finally the easiest thing in the row to hit on purpose.
     Same technique the header of this block describes for inline links,
     applied to a grid item. */
  .cart-line__rm{ position:relative; }
  .cart-line__rm::after{ content:""; position:absolute; inset:-7px; border-radius:50%; }
}

/* ---------- order status / payment failed (post-checkout pages) ---------- */
.track{ padding:clamp(48px,7vw,90px) 0; }
.track__inner{ max-width:560px; }
.track-card{ position:relative; background:#fff; border:1px solid var(--line); border-radius:24px;
  padding:clamp(24px,4vw,40px); box-shadow:0 36px 70px -40px rgba(14,40,80,.4); overflow:hidden; }
.track-card::before{ content:""; position:absolute; top:0; left:0; right:0; height:8px;
  background:repeating-linear-gradient(90deg,
    var(--carbonara-field) 0 22px, var(--bolognese-field) 22px 44px, var(--pomodoro-field) 44px 66px); }

.track-steps{ list-style:none; margin:0 0 20px; padding:0; }
/* State is carried by the dot and by the heading's ink — never by fading the
   text out. This ladder used to sit at opacity:.38 until its step was reached,
   which put a pending heading at 2.2:1 and its description at 1.7:1 on the
   white card: the four things a customer has not reached yet were the four
   least readable things on the page they land on after paying. No opacity
   fixes it either, and that is the useful part — the ACTIVE description
   already renders at 4.9:1, so every value bright enough to pass AA was also
   bright enough to stop reading as "not yet". Two inks and the dot say it
   instead, at 5.1:1 and 5.5:1. */
.track-step{ position:relative; display:flex; gap:16px; padding-bottom:26px; }
.track-step:last-child{ padding-bottom:0; }
.track-step::before{ content:""; position:absolute; left:10px; top:24px; bottom:2px; width:2px; background:var(--line); }
.track-step:last-child::before{ display:none; }
.track-step__dot{ position:relative; flex:none; width:22px; height:22px; margin-top:1px; border-radius:50%;
  border:2px solid var(--line); background:#fff; z-index:1; transition:background .26s var(--ease-out), border-color .26s var(--ease-out); }
.track-step.is-done .track-step__dot{ background:var(--bolognese); border-color:var(--bolognese); }
.track-step.is-current .track-step__dot{ border-color:var(--carbonara); }
/* THE PULSE IS A RING THAT GROWS, NOT A BOX-SHADOW THAT GROWS.
   Same mark on screen; different thing for the browser to do. An
   animated box-shadow is a paint property — every frame re-rasterises
   the dot and the ground under the spreading shadow — and this is the
   one animation on the site with `infinite` on it that is not waiting
   for anything: the order tracker is a page somebody leaves open while
   their dinner is driven across Sofia, so it runs for half an hour at
   a time. As a pseudo-element the ring is transform and opacity only,
   which the compositor animates off the main thread with no repaint at
   all.
   Drawn from -2px so it starts on the dot's own border rather than
   inside it, and scaled rather than spread so the growth is a single
   composited matrix. */
.track-step.is-current .track-step__dot::after{
  content:""; position:absolute; inset:-2px; border-radius:50%;
  border:2px solid var(--carbonara); pointer-events:none;
  animation:trackPulse 1.8s var(--ease-out) infinite; }
@keyframes trackPulse{
  0%{ transform:scale(1); opacity:.45; }
  60%{ transform:scale(1.85); opacity:0; }
  100%{ transform:scale(1.85); opacity:0; } }
.track-step strong{ font-family:var(--display); font-size:1.05rem; color:var(--notte-soft);
  transition:color .32s var(--ease-out); }
.track-step.is-done strong, .track-step.is-current strong{ color:var(--notte); }
/* one ink in every state: the description is supporting text, and a third
   grey for it would be noise the dot and the heading are already carrying */
.track-step p{ margin:2px 0 0; font-size:.84rem; color:var(--ink-soft); }

.track-eta{ font-weight:700; color:var(--notte); margin:0 0 6px; }
.track-done{ font-weight:700; color:var(--bolognese); margin:0 0 6px; }
/* The sibling of .track-done, and it had no rule at all — so a delivery that
   ended WITHOUT arriving said so in unstyled body copy, directly beside a
   bold green "Доставена! Добър апетит!". The one line on this site that asks
   the customer to pick up the phone was the quietest thing in the card. It
   speaks in the red the account page's errors already use. */
.track-failed{ font-weight:700; color:var(--pomodoro); margin:0 0 6px; }
/* The --ask- voice, and it is the exact one this needs: the file already
   defines that trio as "neither a yes nor a no — working, or we could not
   place it", which is precisely what an unreachable tracker is. Reusing it
   also keeps this from looking like the red failure above, which means
   something entirely different and much worse. */
.track-offline{ margin:0 0 6px; padding:10px 14px; border-radius:12px;
  background:var(--ask-bg); border:1px solid var(--ask-border); color:var(--ask-ink);
  font-size:.86rem; font-weight:600; }
.track-items{ list-style:none; margin:14px 0 0; padding:12px 0 0; border-top:1px solid var(--line); font-size:.9rem; color:var(--notte); }
.track-items li{ display:flex; justify-content:space-between; gap:12px; padding:2px 0; }
.track-items li em{ font-style:normal; font-weight:700; }
.track-items__total{ border-top:1px dashed var(--line); margin-top:6px; padding-top:8px !important; font-weight:700; }
/* The opacity is gone and the hex with it, and the LINK is why. An
   alpha on a paragraph applies to everything inside it, so the .75 here
   was also being applied to the one thing in the block that carries its
   own carefully-picked colour: #0C767B measures 5.39:1 on the card's
   white on its own and 3.35:1 seen through this paragraph's .75 — under
   AA, on the phone number the site asks somebody to ring when their
   delivery has gone wrong. --ink-soft says what the .75 was reaching
   for (5.52:1, and the same on cream) without reaching THROUGH the
   link to say it, and --carbonara-text is the audited token the loose
   hex was a near-miss copy of. */
.track-mock{ font-size:.74rem; color:var(--ink-soft); margin:16px 0 18px; }
.track-mock a{ color:var(--carbonara-text); font-weight:700; text-decoration:underline; text-underline-offset:3px; }

/* order reference — the only thing a guest walks away with, so make it
   selectable, legible and obviously a value worth keeping */
.track-ref{ display:flex; flex-wrap:wrap; align-items:baseline; gap:6px 10px; margin:0 0 6px;
  font-size:var(--label-sm); letter-spacing:.16em; text-transform:uppercase; color:var(--oro-ink); font-weight:700; }
.track-ref code{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:.95rem; letter-spacing:.08em; text-transform:none; color:var(--notte);
  background:var(--panna-2); border:1px solid var(--line); border-radius:8px; padding:3px 9px;
  user-select:all; }
.track-emailed{ font-size:.8rem; color:var(--ink); opacity:.85; margin:0 0 18px; }
.track-card .btn{ width:100%; justify-content:center; }

.payfail-msg{ margin:0 0 18px; color:var(--ink); }
.payfail-contact{ display:block; margin-top:14px; text-align:center; font-weight:600; font-size:.85rem;
  color:var(--carbonara-text); text-decoration:underline; text-underline-offset:3px; }
.payfail-contact:hover{ color:var(--notte); }

/* ---------- shop: guest checkout ---------- */
/* A notice about the WHOLE form, so it must not sit closer to the first
   field than the fields sit to each other. At 12px against a 14px field
   gap it read as a caption belonging to Име и фамилия. */
.shop-guest-note{ margin:0 0 var(--form-group); font-size:.82rem; font-weight:600; color:var(--notte); opacity:.75; }
.shop-guest-note[hidden]{ display:none; }

/* ---- the two seams that make three groups ----
   Adjacent-sibling margins collapse, so `max(14, 28)` = 28 and each
   seam costs one declaration. There is deliberately no heading on
   either: the panel already opens with a 1.9rem script "Вашата
   поръчка", and a second and third of those inside the form would
   compete with it — while a quieter pair of labels would still add two
   rows of height to a panel whose total is already ~800px from its own
   send button. Space is the cheaper signal and the correct one.
   The second seam is set on .shop-terms' own margin shorthand below,
   which would otherwise win on source order. */
.shop-summary label.form-seam{ margin-top:var(--form-group); }
.shop-captcha{ margin:2px 0 12px; min-height:65px; }
.shop-captcha[hidden]{ display:none; min-height:0; }

/* The amount, restated at the point it is agreed to. Deliberately NOT a
   second .shop-summary__total: that one is the heading of the cart and
   is set in 1.7rem display type, and repeating it at that size would
   read as a second cart rather than as a confirmation. This is the same
   figure at field weight — enough to check a number against, quiet
   enough that the send button stays the loudest thing in the group.
   Sits inside the seam opened by .shop-terms' margin-top, so it groups
   with the agreement and the button rather than with the address. */
.shop-commit{
  display:flex; justify-content:space-between; align-items:baseline; gap:14px;
  margin:var(--form-group) 0 10px; padding-top:12px;
  border-top:1px solid var(--line);
  font-size:var(--label); letter-spacing:.08em; text-transform:uppercase;
  font-weight:700; color:var(--ink);
}
.shop-commit strong{
  font-family:var(--display); font-size:1.25rem;
  /* same `bolder` → 900 synthesis as the cart total above; see there */
  font-weight:700;
  letter-spacing:0; color:var(--notte);
}
/* the same acknowledgement the cart's own total gets, from the same
   keyframe — one gesture for "this number just changed", wherever the
   number is shown */
.shop-commit strong.bump{ animation:bump .32s var(--ease-out); }

/* The terms agreement. Laid out as a row rather than inheriting the
   form's stacked label{display:block}, which would put the box on its own
   line above the sentence and break the "tick this statement" reading.
   align-items:flex-start keeps the box on the first line when the
   sentence wraps to two or three.
   Selector scoped to `.shop-summary` on purpose: `.shop-summary label`
   is a FIELD label — uppercase, letterspaced, bold — which is right for
   "ПОЩЕНСКИ КОД" and shouts when it lands on a sentence. A bare
   `.shop-terms` loses to it on specificity and the whole agreement
   renders in caps, which is what it did on the first attempt. */
.shop-summary .shop-terms{
  display:flex; align-items:flex-start; gap:.65em;
  text-transform:none; letter-spacing:normal; font-weight:400;
  /* No top margin. The second group seam — where "where it goes" ends
     and "agree and pay" begins — is carried by .shop-commit, which
     always sits directly above this and owns the whole block's spacing
     (see FORM RHYTHM). The agreement should hug the amount it agrees
     to, not stand a group-gap away from it. */
  margin:0 0 16px; font-size:.84rem; line-height:1.45; cursor:pointer;
}
.shop-terms input{
  flex:0 0 auto; width:18px; height:18px; margin:.12em 0 0;
  accent-color:var(--notte); cursor:pointer;
}
.shop-terms a{ font-weight:700; color:var(--notte); text-decoration:underline; text-underline-offset:2px; }
.shop-terms a:hover{ color:var(--carbonara-text); }

/* ---------- cookie consent banner ---------- */
.cookie-banner{ position:fixed; left:0; right:0; bottom:0; z-index:80; padding:16px clamp(16px,4vw,32px);
  background:var(--white-soft); border-top:1px solid var(--line); box-shadow:0 -20px 40px -24px rgba(14,40,80,.35);
  transform:translateY(100%); opacity:0; transition:transform .36s var(--ease-soft), opacity .28s var(--ease-out); }
.cookie-banner.is-visible{ transform:translateY(0); opacity:1; }
.cookie-banner.is-hidden{ transform:translateY(100%); opacity:0; }
.cookie-banner__inner{ width:100%; max-width:var(--maxw); margin-inline:auto; display:flex; flex-wrap:wrap;
  align-items:center; justify-content:space-between; gap:14px 24px; }
.cookie-banner__msg{ margin:0; flex:1 1 340px; font-size:.86rem; color:var(--ink); opacity:.9; }
.cookie-banner__msg a{ font-weight:700; color:#0C767B; text-decoration:underline; text-underline-offset:3px; }
.cookie-banner__actions{ display:flex; align-items:center; gap:10px; flex:none; }
.cookie-banner__btn{ padding:.7em 1.4em; font-size:.84rem; white-space:nowrap; }
.cookie-banner__btn--ghost{ background:none; border:1px solid var(--line); border-radius:999px; color:var(--ink);
  font-family:var(--body); font-weight:700; cursor:pointer; transition:border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out); }
.cookie-banner__btn--ghost:hover{ border-color:var(--accent); color:var(--accent); }
@media (max-width:680px){
  .cookie-banner{ padding:14px 16px; border-radius:18px 18px 0 0; box-shadow:0 -12px 30px -18px rgba(14,40,80,.4); }
  .cookie-banner__inner{ flex-direction:column; align-items:stretch; gap:12px; }
  .cookie-banner__msg{ flex:none; font-size:.82rem; text-align:center; }
  .cookie-banner__actions{ flex-direction:column; align-items:stretch; justify-content:flex-start; gap:8px; width:100%; }
  .cookie-banner__btn{ width:100%; text-align:center; padding:.85em 1.2em; }
}
@media (max-width:380px){
  .cookie-banner{ padding:12px 14px; }
  .cookie-banner__msg{ font-size:.78rem; }
  .cookie-banner__btn{ font-size:.82rem; padding:.8em 1em; }
}
/* Short and wide — a phone turned on its side. Has to sit AFTER the two
   width queries above rather than with the page's other landscape rules
   (which are ~280 lines earlier): all three are one class deep, so the
   later block is the one that wins, and up there this did nothing at all. */
@media (max-height:540px) and (orientation:landscape){
  /* The consent banner stacks into a column — message, then two full-width
     buttons — at ≤680px. That is a decision about WIDTH being scarce, and
     it was reaching landscape phones, where width is the one thing there is
     plenty of and height is what has run out. Measured at 640×360 it stood
     ~215px tall: 60% of the viewport, with the hero headline behind it and
     nothing above the fold but the header. Back to a row here, which is the
     shape the desktop banner already has. */
  .cookie-banner{ padding:10px 16px; border-radius:16px 16px 0 0; }
  .cookie-banner__inner{ flex-direction:row; flex-wrap:nowrap; align-items:center; gap:16px; }
  .cookie-banner__msg{ flex:1 1 auto; text-align:left; font-size:.76rem; }
  .cookie-banner__actions{ flex-direction:row; width:auto; flex:none; gap:8px; }
  .cookie-banner__btn{ width:auto; padding:.62em 1.1em; font-size:.78rem; }
}
@supports (padding: env(safe-area-inset-bottom)){
  .cookie-banner{ padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
  @media (max-width:680px){
    .cookie-banner{ padding-bottom:calc(14px + env(safe-area-inset-bottom)); }
  }
}

/* ============================================================ DELIVERY ZONES
   The verdict on an address: green = we deliver there, amber = we do not,
   and the cool --ask- ground for "we could not place it", which is neither.

   There used to be a second, larger rendering of this — the #zoneCheck
   panel above the shop cards, with the coverage map beside it. Both went
   on 2026-08-25 (the map to /stockists, the question into the order form
   itself, since asking for the address twice was one question wearing two
   hats). Its ~70 lines of .zone-check__* styling outlived it here by a
   week and are gone as of 2026-09-02; what follows is the live one.
   ============================================================ */

/* the same verdict, compact, inside the order form */
.shop-zone{
  margin:-6px 0 12px; padding:.6em .85em; border-radius:11px;
  font-size:.82rem; line-height:1.45; border:1px solid transparent;
  /* The three grounds carry the answer, and since the slot stopped
     moving (see #shopForm.is-zoned below) the box no longer appears —
     it CHANGES, in place. Cross-fading the ground is what makes that a
     change rather than a hard cut, and it is the whole animation: no
     movement, because nothing here moved. */
  transition:background-color var(--t-med) var(--ease-out),
             border-color var(--t-med) var(--ease-out),
             color var(--t-med) var(--ease-out);
}
/* "Yes" gets a solid mark, not just a pale ground. Measured: --ok-bg
   #F1F7F0 differs from the page's own --panna #FCF7EE by only a couple
   of points in one channel, so the panel that answers the single most
   reassuring question in the checkout was reading as barely-there next
   to the --ask ground beside it (#EFF1F5, a genuine hue shift from warm
   cream to cool grey, which is why THAT one reads as a distinct state
   and this one didn't). The fix is a solid badge, not a stronger tint:
   the map's own "confirmed" pin already answers this exact question in
   the same solid #2F6B42, so the two objects now share one unmistakable
   colour for one meaning instead of the map alone carrying it. */
.shop-zone.is-ok{
  background:var(--ok-bg); border-color:var(--ok-border); color:var(--ok-ink);
  padding-left:2.6em; position:relative;
}
.shop-zone.is-ok::before{
  content:""; position:absolute; left:.75em; top:.68em;
  width:17px; height:17px; border-radius:50%;
  background:#2F6B42; box-shadow:0 1px 2px rgba(42,82,54,.4);
}
.shop-zone.is-ok::after{
  content:""; position:absolute; left:calc(.75em + 4.5px); top:calc(.68em + 5.5px);
  width:8px; height:4.5px;
  border-left:2px solid var(--panna); border-bottom:2px solid var(--panna);
  transform:rotate(-48deg);
}
.shop-zone.is-warn{ background:var(--warn-bg); border-color:var(--warn-border); color:var(--warn-ink); }
/* Neither a yes nor a no takes the green or the amber. This is the copy
   that decides an order, so the three answers are three grounds and the
   customer never has to read the sentence to know which one they got.
   The message itself stays --ink: the verdicts tint their whole box,
   but a sentence set in the link colour reads as a link. */
.shop-zone.is-ask,
.shop-zone.is-checking{ background:var(--ask-bg); border-color:var(--ask-border); color:var(--ink); }
.shop-zone.is-ask .linkish{ color:var(--ask-ink); }
.shop-zone.is-ask .linkish:hover{ color:var(--notte); }
.shop-zone.is-checking{ opacity:.72; }
/* "Working on it", and the site already had a word for that: the order
   tracker marks its current step with a ring breathing out of a dot
   (trackPulse). This is the only other place we make a customer wait,
   so it says it the same way rather than importing a rotating arc —
   which is the category default and would be the one mark on the page
   that came from somewhere else.

   The rhythm is faster than the tracker's 1.8s on purpose: a tracker
   step lasts half an hour, this lookup lasts about a second (700ms
   debounce, then Photon, then Nominatim behind it). At 1.8s the
   customer would often see less than one ring; at 1.1s a full breath
   lands inside even a fast resolve. It is a pulse and not a
   determinate spinner because we genuinely cannot say how far along a
   two-geocoder lookup is, and a bar that lies is worse than none.

   ::before, so renderZone()'s innerHTML can't wipe it. */
.shop-zone.is-checking::before{
  content:""; display:inline-block; vertical-align:.08em;
  width:7px; height:7px; margin-right:.62em; border-radius:50%;
  background:var(--ask-ink);
  /* linear, and the stops below carry the shape. The site's --ease
     curves are all front-loaded (that is what makes them right for
     pointer feedback), and on a ring that fades as it grows they raced
     the alpha to nothing by a fifth of the way through: measured 0.45
     → 0.08 in 220ms, so the ring was gone before it was a ring and the
     whole thing read as a static dot. */
  animation:zoneWorking 1.1s linear infinite;
}
@keyframes zoneWorking{
  0%  { box-shadow:0 0 0 0   rgba(42,74,134,.55); }
  35% { box-shadow:0 0 0 3px rgba(42,74,134,.34); }
  70% { box-shadow:0 0 0 6px rgba(42,74,134,.12); }
  100%{ box-shadow:0 0 0 8px rgba(42,74,134,0); }
}
/* The site's global reduced-motion rule is a blunt
   `*{animation:none !important}`, which would leave this as a plain
   dot — a bullet, saying nothing. box-shadow is neither animation nor
   transition, so it survives that rule: the ring stops breathing and
   stands still instead, which still reads as a live marker rather than
   punctuation. Reduced motion means less movement, not less meaning.
   The ground cross-fade above is deliberately NOT rescued the same
   way: losing it costs nothing, because the colour still arrives, just
   instantly. */
@media (prefers-reduced-motion:reduce){
  .shop-zone.is-checking::before{ box-shadow:0 0 0 3px rgba(42,74,134,.28); }
}
/* The reserved slot: script.js flags the form the first time the
   address field is focused, and from then on the verdict's space is
   held whether or not there is a verdict yet. Sized to the one-line
   answer, so the "yes" path — the one that ends in an order — lands
   with no movement at all, and the longer refusals grow by a line
   instead of arriving out of nowhere 700ms after typing stopped.
   Reserving the TALLEST state instead would have made every "yes"
   shrink the form upward, which is the more disorienting direction. */
#shopForm.is-zoned .shop-zone[hidden]{
  display:block; visibility:hidden; min-height:37px;
}
/* The way out of a state, and on a phone it was a 16px-tall strip of
   text — half the 24px WCAG 2.5.8 asks for and a quarter of a thumb.
   Given its own line and real vertical padding it measures ~35px, which
   is a target rather than a dare; the line of its own also stops a
   two-line message ending mid-sentence in a link. */
.shop-zone .linkish{
  display:inline-block; margin-top:.35em; padding:.62em .2em;
  border:0; background:none; cursor:pointer;
  font-family:var(--body); font-size:inherit; font-weight:700; color:inherit;
  text-decoration:underline; text-underline-offset:3px;
}
.shop-zone .linkish:hover{ color:var(--notte); }

/* the example under the street field, and the flash that says a pin
   just filled it in — the same cue a deep-linked shop card gets */
.shop-hint{
  margin:-8px 0 14px; font-size:.76rem; line-height:1.5;
  /* .66 measured 3.85:1 on the summary card's white — under AA for text
     this size, and this is where the form explains itself */
  color:var(--ink); opacity:.78;
}

/* The form's one marker, and it marks what is OPTIONAL: a customer who
   misses it fills in a field we did not need, where a missed "required"
   star costs them the order. Subordinated by case, weight and size
   rather than by fading it — at .76rem there is no contrast left to
   spend (5.3:1 here, against the 3.3:1 that .6 opacity would have
   given). Sibling of the translated <span>; see shop.html. */
.field-opt{
  margin-left:.5em;
  /* .9 of a .78rem label — 11.2px. .86 put it at 10.7px, under the
     floor where small functional type stops being readable at all. */
  font-size:.9em; font-weight:400; letter-spacing:.04em;
  text-transform:none; color:var(--ink); opacity:.78;
}
.field-opt[hidden]{ display:none; }
#shopAddress.is-flash{
  outline:2px solid var(--accent); outline-offset:2px;
  transition:outline-color var(--t-med) var(--ease-out);
}

/* ============================================================ ADDRESS FIELD
   The predictive street list (script.js `attachSuggest`, fed by
   LS_DELIVERY.suggest) and the вход / етаж / апартамент row beside it.

   `.addr-field` ships in the markup rather than being created by the
   script, because the list is positioned against it: a wrapper that
   only exists once JS has run is a wrapper the field jumps inside of.
   With no JS it is an inert span around a plain text input.
   ============================================================ */
.addr-field{ display:block; position:relative; }

/* The list floats over whatever is under it — on the order form that is
   the вход/етаж row, and a list that pushed the form down would move
   the row out from under the finger reaching for it. */
.addr-suggest{
  position:absolute; z-index:20; left:0; right:0; top:calc(100% + 6px);
  margin:0; padding:6px; list-style:none;
  max-height:min(46vh,300px); overflow-y:auto; overscroll-behavior:contain;
  background:var(--white-soft); border:1px solid var(--line); border-radius:14px;
  box-shadow:0 26px 50px -26px rgba(14,40,80,.55);
}
.addr-suggest[hidden]{ display:none; }

.addr-suggest__row{
  display:block; padding:.55em .7em; border-radius:10px; cursor:pointer;
  font-family:var(--body); font-size:.92rem; line-height:1.35;
  text-transform:none; letter-spacing:normal;
}
/* One highlight for both ways of moving through the list — the mouse
   sets it on hover and the arrow keys set it directly, so the pointer
   and the keyboard never disagree about which row Enter would take. */
.addr-suggest__row.is-on{ background:var(--panna-2); }
.addr-suggest__addr{ display:block; font-weight:700; color:var(--notte); }
.addr-suggest__meta{ display:block; margin-top:1px; font-size:.78rem; color:var(--ink-soft); }
/* A street with no house number on it. Marked rather than hidden: it is
   how you find the road before you know the number is still owed, and
   picking it says so in words too (addr.sug.needNum). */
.addr-suggest__tag{
  display:inline-block; margin-top:4px;
  font-size:var(--label-sm); font-weight:700; letter-spacing:.1em; text-transform:uppercase;
  color:var(--oro-ink); opacity:.9;
}
.addr-suggest__empty{
  padding:.6em .7em; font-size:.84rem; line-height:1.45;
  color:var(--ink); opacity:.75; text-transform:none; letter-spacing:normal;
}

/* "add the building number" — under the field, in the amber the shop's
   own not-yet-an-answer line uses, so the two read as one voice */
.addr-note{
  margin:8px 0 0; padding:.5em .75em; border-radius:10px;
  background:var(--warn-bg); border:1px solid var(--warn-border); color:var(--warn-ink);
  font-family:var(--body); font-size:.78rem; font-weight:600; line-height:1.45;
  text-transform:none; letter-spacing:normal;
}
.addr-note[hidden]{ display:none; }

/* ---- вход / етаж / апартамент ----
   Three narrow fields on one line: they are short values, and stacking
   them would make the optional part of the address the tallest thing in
   the form. They collapse to two columns before they collapse to one,
   because "Апартамент" is the long label and gets the full width. */
.shop-unit{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:0 10px; }
/* Subordinated, because inheriting the field label's uppercase/700/
   letterspaced treatment made ВХОД ЕТАЖ АПАРТАМЕНТ three caps clusters
   on one line — measurably the heaviest horizontal band between the
   total and the send button, and it is the one row nobody has to fill
   in. Sentence case at .72rem (11.5px, above the readable floor) hands
   the weight back to the required spine. The input boxes are left
   alone: the fields are still the same size, only the labels step back. */
.shop-unit label{
  margin-bottom:var(--form-field);
  font-size:.72rem; font-weight:600;
  letter-spacing:normal; text-transform:none;
}
@media (max-width:400px){
  .shop-unit{ grid-template-columns:repeat(2,minmax(0,1fr)); }
  .shop-unit label:last-child{ grid-column:1 / -1; }
}

/* ============================================================ CLAIM MARKS
   The two things a flavour can be marked with — imported Italian
   ingredients (Carbonara, Pomodoro) or the house recipe (Bolognese).
   Gold, because on this site gold is the decorative register and these
   are the only decorative claims made about the food. On the shop card
   the mark is the whole statement: that media column is ~240px, too
   small for anything but type to read in.                            */
.shop-card__italian{
  display:inline-block; margin:.45rem 0 0; padding:.34em .85em .38em;
  /* .66rem put the claim at 10.56px — under the floor where small
     functional type stops being readable, on the one line that tells
     the three cards apart. .7rem is 11.2px and costs ~12px of width in
     a 350px column, so both claims still set on one line in both
     languages (measured: BG 218/227px, EN 207/232px). */
  border-radius:999px; font-size:var(--label-sm); font-weight:700;
  letter-spacing:.09em; text-transform:uppercase; color:var(--white-soft);
  background:linear-gradient(150deg, var(--claim-lit), var(--claim-mid) 55%, var(--claim-deep));
  box-shadow:0 6px 14px -8px rgba(14,40,80,.55);
}
.shop-card__italian + .shop-card__price{ margin-top:.5rem; }

/* The three panels the story page closes on: how the food keeps, and
   what each recipe is. Equal columns on purpose — sized as one, so no
   panel reads as a footnote to the one beside it. auto-fit rather than
   a fixed three, because at 240px a panel stops holding a paragraph.

   They used to be three identical grey rectangles holding three
   identical grey paragraphs, which is the densest part of the page and
   the least reason to look at it. What's added is numbering and a lit
   top edge — applied to ALL THREE IDENTICALLY, because the moment one
   panel is dressed differently it starts reading as the important one,
   which is exactly what the equal sizing exists to prevent. */
.story__recipes{ display:grid; grid-template-columns:repeat(auto-fit, minmax(248px, 1fr));
  gap:clamp(14px,2vw,20px); margin:clamp(2.6rem,4.4vw,3.6rem) 0 0;
  counter-reset:recipe; }
.story__recipe{ position:relative; overflow:hidden; counter-increment:recipe;
  padding:clamp(22px,2.6vw,30px) clamp(18px,2.6vw,28px) clamp(20px,2.4vw,26px);
  border-radius:20px; background:rgba(255,255,255,.045);
  border:1px solid rgba(203,168,111,.3);
  transition:transform .26s var(--ease-out), border-color .26s var(--ease-out), background .26s var(--ease-out); }
.story__recipe:hover{ transform:translateY(-4px); border-color:var(--oro); background:rgba(255,255,255,.075); }
/* the numeral, set large and low behind the heading — a count, not a
   label, so it needs no words and stays out of the way of the ones
   that are there */
/* Sits fully inside the panel: the panel clips (overflow:hidden, for the
   top edge below), so a negative top doesn't bleed out of the corner —
   it gets sliced, which reads as a mistake rather than a flourish.
   Its size and the heading's padding-right are one decision: at this
   size the numeral occupies about 64px of the top-right corner, which
   is exactly what the h2 is kept out of. A heading running through a
   ghosted numeral looked like a rendering fault on a narrow panel. */
.story__recipe::before{
  content:"0" counter(recipe); position:absolute; right:.18em; top:.06em;
  font-family:var(--display); font-weight:700; font-size:2.9rem; line-height:1;
  color:var(--oro-soft); opacity:.16; pointer-events:none;
}
/* a lit top edge, brightest at the left where the eye enters */
.story__recipe::after{
  content:""; position:absolute; left:0; right:0; top:0; height:2px;
  background:linear-gradient(90deg, var(--oro) 0, rgba(203,168,111,.28) 55%, transparent 100%);
}
.story__recipe h2{ position:relative; font-size:clamp(1.15rem,2.4vw,1.5rem); color:var(--panna);
  margin:0 0 .6rem; padding-right:3.7rem; line-height:1.22; }
.story__recipe h2 em{ font-style:italic; color:var(--oro-soft); }
.story__recipe p{ margin:0; color:#CFC8B7; font-size:.98rem; }

@media (max-width:560px){
  .story__recipes{ grid-template-columns:minmax(0,1fr); }
}
/* ============================================================ 3D PRODUCT BOX
   The interactive model built by box3d.js. Everything here is
   derived from ONE number — --b-w, the box width — so a context
   only ever has to say how wide the box should be.

   The packaging is a gable box with a handle, so the model is:
     body      front · back · two pentagon sides · bottom
     roof      two panels sloping up from the walls to a ridge
     handle    two tabs above the ridge with the grip cut out

   PROPORTIONS come from the source photo (assets/<flavour>.png,
   644×700). The printed front panel is the rectangle x 11…632,
   y 256…674 — 621×418, hence --b-h = .6731 × --b-w. Depth, roof
   rise and tab height are the packaging's own proportions.

   The x range matters as much as the y: the photo's outer few
   pixels are the lit paper edge of the real box, and including
   them put a bright white seam down the model's front corner. The
   crop stops at the last full stripe instead.

   The front and back faces are that photo, scaled and offset
   inside an overflow:hidden face so only the print shows:
     width  644/621 = 103.70%      left -11/621 = -1.771%
     height 700/418 = 167.46%      top  -256/418 = -61.244%
   No second copy of the artwork to download, and the box on screen
   is the box in the photograph.

   Every other face is the candy stripe measured off the same photo
   (81.5px period, 45px of colour, first stripe at x=15 — i.e. of
   the 621px panel: period 13.124%, colour 7.246%, first stripe at
   .644%). Expressed as fractions of --b-w, the print carries on
   round the corners in register instead of restarting.
   ============================================================ */
.box3d{
  --b-h:  calc(var(--b-w) * .6731);   /* front panel height   */
  --b-d:  calc(var(--b-w) * .56);     /* depth                */
  --b-r:  calc(var(--b-w) * .20);     /* roof rise to the ridge */
  --b-t:  calc(var(--b-w) * .21);     /* handle tab height    */
  --b-l:  calc(var(--b-w) * .3441);   /* roof slant = hypot(--b-d/2, --b-r) */
  --b-tw: calc(var(--b-w) * .84);     /* handle tab width     */
  --b-tz: calc(var(--b-w) * .006);    /* half the gap between the two tabs */
  --b-sw: calc(var(--b-w) * .13124);  /* stripe period        */
  --b-sf: calc(var(--b-w) * .07246);  /* coloured part of it  */
  --b-paper:#FBF9F4;
  --b-field:var(--carbonara-field);
  --b-ink:var(--carbonara-ink);       /* the type colour printed on this box */
  /* the handle plate: rounded top, grip punched out with evenodd */
  --b-tab-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 25' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0,25V10Q0,0,12,0H88Q100,0,100,10V25ZM29,5H71A7,7,0,0,1,71,19H29A7,7,0,0,1,29,5Z'/%3E%3C/svg%3E");

  position:relative; display:flex; align-items:center; justify-content:center; padding:0;
}
.box3d[data-box3d="carbonara"]{ --b-field:var(--carbonara-field); --b-ink:var(--carbonara-ink); }
.box3d[data-box3d="bolognese"]{ --b-field:var(--bolognese-field); --b-ink:var(--bolognese-ink); }
.box3d[data-box3d="pomodoro"] { --b-field:var(--pomodoro-field);  --b-ink:var(--pomodoro-ink);  }

/* --b-print is the ink the SIDE PANELS are set in, and it is not
   always --b-ink. --b-ink is the site's accent for the flavour, taken
   off the arch of type on the product photograph; --b-print is what
   the current print run actually uses. They agreed until bolognese
   was reprinted (2026-08-18): its front went from "SPAGHETTI AL
   BOLOGNESE" in bottle green to "SPAGHETTI ALLA BOLOGNESE" in a
   near-black, while the site — and the photograph the front face is
   cropped from — is still green. Two tokens rather than one so
   repainting the box does not repaint every bolognese heading,
   button and badge on the site with it. */
.box3d{ --b-print:var(--b-ink); }
.box3d[data-box3d="bolognese"]{ --b-print:#161513; }

/* until box3d.js has built the model, the page keeps the photo */
.box3d.is-3d > img{ display:none; }
.box3d__hint{ display:none; }

/* ---- state written by box3d.js ----
   --b-opacity fades the box up, --b-scale pops it, --b-rise (a
   fraction of --b-w) drops it a few pixels so it settles onto its
   shadow. --b-shw / --b-shy tell the shadow which way the box is
   facing (see the shadow block below). All of them are plain numbers
   so the geometry stays in CSS and the animation stays a property
   write per frame. They default to "landed and facing front", so a
   box that never animates — no JS, reduced motion — looks right.
   --b-lift is the one CSS owns: the hover rise. */
.box3d{ --b-opacity:1; --b-scale:1; --b-rise:0; --b-shw:1; --b-shy:0; --b-lift:0px; }

.box3d__scene{
  position:relative; width:calc(var(--b-w) * 1.12); height:calc(var(--b-w) * 1.28);
  perspective:calc(var(--b-w) * 3.4); perspective-origin:50% 46%;
  cursor:grab; -webkit-tap-highlight-color:transparent;
  opacity:var(--b-opacity);
  /* a horizontal drag spins the box, a vertical swipe still scrolls
     the page — the model must never trap a phone user mid-scroll */
  touch-action:pan-y;
  /* The hover rise lives on the scene, not on the pivot: box3d.js
     rewrites the pivot's transform on every frame of the entrance, and
     a transition sitting on top of that would smooth each frame into
     the next and turn a 760ms landing into a rubber band. The scene's
     transform is CSS's alone, so it can afford a curve. */
  /* translateY, not translate3d: the 3d form would promote every scene
     on the page to its own compositor layer for good, which is the bill
     box3d.js goes out of its way not to run up */
  transform:translateY(var(--b-lift));
  transition:transform .46s var(--ease-out);
}
/* Picking the box up: it rises, and the shadow it left behind spreads
   and softens the way a real one does as the gap under it opens. */
.box3d.is-3d:hover,
.box3d.is-3d:focus-within{ --b-lift:calc(var(--b-w) * -.03); }
.box3d.is-3d.is-dragging{ --b-lift:calc(var(--b-w) * -.014); }
.box3d.is-dragging .box3d__scene{ cursor:grabbing; transition-duration:.18s; }
/* A static box (data-box3d-static) is scenery inside something else
   that owns the click. pointer-events:none keeps it out of hit-testing
   entirely, so a press anywhere over the model reaches the card behind
   it, and the cursor stays the card's rather than promising a grab that
   will not happen. */
.box3d[data-box3d-static] .box3d__scene{ cursor:inherit; pointer-events:none; }
.box3d__scene:focus-visible{ outline:2px solid var(--accent); outline-offset:8px; border-radius:14px; }

/* drops the whole model so the model's visual centre — not the body
   centre the faces are hung from — sits in the middle of the scene */
.box3d__pivot{ position:absolute; inset:0; transform-style:preserve-3d;
  transform:translateY(calc(var(--b-w) * (.19 + var(--b-rise)))) scale(var(--b-scale)); }
.box3d__model{ position:absolute; inset:0; transform-style:preserve-3d; }

.box3d__face{
  position:absolute; left:50%; top:50%;
  backface-visibility:hidden; -webkit-backface-visibility:hidden;
  background-color:var(--b-paper);
  background-image:repeating-linear-gradient(90deg,
    var(--b-field) 0, var(--b-field) var(--b-sf),
    var(--b-paper) var(--b-sf), var(--b-paper) var(--b-sw));
  /* the print's first stripe starts .644% in — matching it here keeps
     the roof's stripes continuous with the photographed front */
  background-position-x:calc(var(--b-w) * .00644);
}

/* ---- the light ----
   Every face used to carry one flat tint (`inset 0 0 0 999px rgba(…)`),
   which is most of why the model read as a diagram of a box rather than
   a box: real board is brighter at the top of a panel than at its foot,
   the fold at each corner is a bright line rather than a step between
   two solid colours, and no light at all reaches the last centimetre
   above the table. This overlay paints one key light — above, slightly
   in front, slightly to the left — across all nine faces.

   It is a pseudo-element rather than more background layers because the
   stripe pattern owns background-position on every face, and adding
   layers would drag the shading along with the print's phase. z-index
   puts it over the photograph on the two printed faces; clip-path and
   the handle mask both clip descendants, so the sides and tabs keep
   their silhouettes.

   The shading is fixed to the faces, not to the room, so it turns with
   the box — the same bargain the photograph itself already makes, its
   own lighting having been baked in by whoever shot the packaging. */
.box3d__face::after{
  content:""; position:absolute; inset:0; z-index:2; pointer-events:none;
}

/* ---- front + back: the printed panel, cropped out of the photo ---- */
.box3d__face--front,
.box3d__face--back{
  width:var(--b-w); height:var(--b-h);
  margin-left:calc(var(--b-w) / -2); margin-top:calc(var(--b-h) / -2);
  overflow:hidden; background-image:none;
}
.box3d__face--front{ transform:translateZ(calc(var(--b-d) / 2)); }
.box3d__face--back { transform:rotateY(180deg) translateZ(calc(var(--b-d) / 2)); }
/* The crop deliberately stops short of the lit paper edge in the photo
   (see the note above), so the corner fold has to be drawn back on —
   without it the print runs into the side panel with nothing between
   them and the two faces read as one bent picture. */
.box3d__face--front::after,
.box3d__face--back::after{
  background:
    linear-gradient(180deg, rgba(255,255,255,.18), rgba(255,255,255,0) 26%),
    linear-gradient(0deg, rgba(46,32,14,.22), rgba(46,32,14,0) 17%),
    linear-gradient(90deg, rgba(255,255,255,.16), rgba(255,255,255,0) 26%,
                            rgba(255,255,255,0) 74%, rgba(46,32,14,.10));
  box-shadow:inset 2px 0 0 -1px rgba(255,255,255,.6), inset -2px 0 0 -1px rgba(255,255,255,.4);
}
.box3d__face--back::after{ box-shadow:inset 2px 0 0 -1px rgba(255,255,255,.34), inset -2px 0 0 -1px rgba(255,255,255,.34); }
/* Deep on purpose: this has to beat .product__media img and
   .shop-card:hover .shop-card__media img, which would otherwise
   resize the texture and slide it out of its face. Scoped to the two
   printed faces because it is the PHOTO CROP — the QR on the side
   panel is an image on this model too, and being dragged 61% up its
   own face and blown up to 167% is not what it wants. */
.box3d.is-3d .box3d__face--front img,
.box3d.is-3d .box3d__face--back img{
  position:absolute; display:block; left:-1.771%; top:-61.244%;
  width:103.70%; height:167.464%; max-width:none;
  filter:none; transform:none; transition:none;
  pointer-events:none; user-select:none; -webkit-user-drag:none;
}
.box3d.is-3d .box3d__face--back img{ filter:brightness(.92); }

/* ---- sides: pentagon — wall plus the gable shoulder ---- */
.box3d__face--left,
.box3d__face--right{
  width:var(--b-d); height:calc(var(--b-h) + var(--b-r));
  margin-left:calc(var(--b-d) / -2); margin-top:calc((var(--b-h) + var(--b-r)) / -2);
  /* 22.91% = --b-r / (--b-h + --b-r): where the shoulder starts */
  clip-path:polygon(0% 100%, 0% 22.91%, 50% 0%, 100% 22.91%, 100% 100%);
  /* the front panel ends on a colour stripe, so the side has to open
     with the white gap — otherwise the print jumps at the corner */
  background-position-x:calc(var(--b-w) * -.08046);
}
/* rotateY(±90°) takes each side's own +x with it: on the RIGHT face
   local x runs front→back, on the LEFT face back→front. So the two
   shading gradients are mirror images even though both are lit from
   the same corner of the room. */
.box3d__face--right{
  transform:translateY(calc(var(--b-r) / -2)) rotateY(90deg) translateZ(calc(var(--b-w) / 2));
  box-shadow:inset 1px 0 0 rgba(255,255,255,.5);
}
.box3d__face--right::after{
  background:
    linear-gradient(90deg, rgba(255,255,255,.20), rgba(255,255,255,0) 22%,
                            rgba(56,40,18,.10) 62%, rgba(56,40,18,.26)),
    linear-gradient(0deg, rgba(40,28,12,.24), rgba(40,28,12,0) 20%),
    linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,0) 24%);
}
.box3d__face--left{
  transform:translateY(calc(var(--b-r) / -2)) rotateY(-90deg) translateZ(calc(var(--b-w) / 2));
  box-shadow:inset -1px 0 0 rgba(255,255,255,.34);
}
.box3d__face--left::after{
  background:
    linear-gradient(270deg, rgba(255,255,255,.10), rgba(255,255,255,0) 26%,
                             rgba(48,34,16,.20) 60%, rgba(48,34,16,.40)),
    linear-gradient(0deg, rgba(40,28,12,.26), rgba(40,28,12,0) 20%);
}

/* ---- underside: plain board, no print ---- */
.box3d__face--bottom{
  width:var(--b-w); height:var(--b-d);
  margin-left:calc(var(--b-w) / -2); margin-top:calc(var(--b-d) / -2);
  transform:rotateX(-90deg) translateZ(calc(var(--b-h) / 2));
  background-image:none; background-color:#E4D6BC;
}
/* The base is the one panel the key light never reaches at all, so it
   darkens towards the middle rather than towards an edge — that, and
   not a flat grey, is what tells you you are looking underneath. */
.box3d__face--bottom::after{
  background:
    radial-gradient(120% 140% at 50% 0%, rgba(48,34,16,.16), rgba(48,34,16,.42)),
    linear-gradient(180deg, rgba(255,255,255,.16), rgba(255,255,255,0) 30%);
}

/* ---- roof: two panels meeting at the ridge ----
   35.54° = atan(--b-r / (--b-d/2)); the panels are pitched off
   horizontal (90°) by that much, one forward and one back. */
.box3d__face--roof-front,
.box3d__face--roof-back{
  width:var(--b-w); height:var(--b-l);
  margin-left:calc(var(--b-w) / -2); margin-top:calc(var(--b-l) / -2);
}
/* The roof is the only surface pointed anywhere near the light, and the
   ridge is a fold in card, so it takes a highlight along its length.
   Which edge that is differs per panel: on roof-front the ridge is the
   face's top edge, on roof-back — rotated past vertical — it is the
   bottom one. */
.box3d__face--roof-front{
  transform:translate3d(0, calc((var(--b-h) + var(--b-r)) / -2), calc(var(--b-d) / 4)) rotateX(54.46deg);
}
.box3d__face--roof-front::after{
  background:
    linear-gradient(180deg, rgba(255,255,255,.62), rgba(255,255,255,.10) 34%,
                             rgba(255,255,255,0) 62%, rgba(56,40,18,.10)),
    linear-gradient(90deg, rgba(255,255,255,.16), rgba(255,255,255,0) 34%,
                            rgba(56,40,18,.07));
}
.box3d__face--roof-back{
  transform:translate3d(0, calc((var(--b-h) + var(--b-r)) / -2), calc(var(--b-d) / -4)) rotateX(125.54deg);
}
.box3d__face--roof-back::after{
  background:
    linear-gradient(0deg, rgba(255,255,255,.34), rgba(255,255,255,0) 40%),
    linear-gradient(180deg, rgba(56,40,18,.20), rgba(56,40,18,.06) 60%);
}

/* ---- handle: two tabs above the ridge, grip masked out ---- */
.box3d__face--tab-front,
.box3d__face--tab-back{
  width:var(--b-tw); height:var(--b-t);
  margin-left:calc(var(--b-tw) / -2); margin-top:calc(var(--b-t) / -2);
  /* the tab is inset .08 × --b-w from the box edge, so shift the
     stripe phase by that much to stay in register with the walls */
  background-position-x:calc(var(--b-w) * -.07356);
  -webkit-mask-image:var(--b-tab-mask); mask-image:var(--b-tab-mask);
  -webkit-mask-size:100% 100%; mask-size:100% 100%;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
}
/* sunk 2.5% into the ridge so no hairline of background shows through */
.box3d__face--tab-front{
  transform:translate3d(0,
    calc(var(--b-h) / -2 - var(--b-r) - var(--b-t) / 2 + var(--b-w) * .025),
    var(--b-tz));
}
.box3d__face--tab-back{
  transform:translate3d(0,
    calc(var(--b-h) / -2 - var(--b-r) - var(--b-t) / 2 + var(--b-w) * .025),
    calc(var(--b-tz) * -1)) rotateY(180deg);
}
/* The two tabs are the highest thing on the box and stand almost
   upright, so they catch the light along their rounded top edge and
   shade into the ridge below. */
.box3d__face--tab-front::after{
  background:linear-gradient(180deg, rgba(255,255,255,.34), rgba(255,255,255,0) 42%,
                                      rgba(56,40,18,.14));
}
.box3d__face--tab-back::after{
  background:linear-gradient(180deg, rgba(56,40,18,.10), rgba(56,40,18,.22));
}

/* ============================================================ SERVING STAMP
   The "2 порции" cartouche on the front of the box. box3d.js builds the
   markup and explains why it is drawn rather than photographed; this is
   the geometry, and every number in it was measured off the 2026-08-18
   packaging photographs against the printed panel, using the "SelezionE"
   wordmark as the ruler because it appears on both the new photographs
   and the old cut-out the front face is made of.

   ONE NAVY, THREE FLAVOURS. --b-stamp does not follow --b-ink or
   --b-print: the stamp stays this navy on the pink box whose type is
   near-black and on the yellow box whose type is red. It is a constant
   of the brand, not of the flavour, so there is no per-flavour override
   here and adding one would be undoing a measurement.

   It scales off --b-w like everything else on the model, so the same
   rule serves the 268px box on products.html and the 146px box in a
   shop card. The stroke is the one thing that cannot be pure ratio: at
   card size .0054 of the face is a third of a pixel, which a browser
   rounds to nothing and the stamp loses its frame. Hence the max().
   ============================================================ */
.box3d{ --b-stamp:#1D1F46; }

.boxstamp{
  position:absolute; z-index:2;
  /* centre, as a fraction of the printed panel: .197 across, .444 down */
  left:19.7%; top:44.4%;
  width:calc(var(--b-w) * .232);
  height:calc(var(--b-w) * .1039);
  transform:translate(-50%,-50%) rotate(-12deg);
  border:max(.6px, calc(var(--b-w) * .0054)) solid var(--b-stamp);
  border-radius:999px;
  display:flex; align-items:center; justify-content:center;
  /* print on a model of a box, like the side panels: the whole face is
     a surface to grab and spin, and a selection starting mid-drag would
     fight that */
  pointer-events:none; user-select:none; -webkit-user-select:none;
}
/* the second loop, following the first — what makes it read as a drawn
   frame rather than a border */
.boxstamp::before{
  content:""; position:absolute;
  inset:calc(var(--b-w) * .0135);
  border:max(.5px, calc(var(--b-w) * .0044)) solid var(--b-stamp);
  border-radius:999px;
}
.boxstamp__t{
  font-family:var(--display); font-weight:700;
  font-size:calc(var(--b-w) * .0377);
  line-height:1; white-space:nowrap; color:var(--b-stamp);
  /* same reason as the side panels: this is a reproduction of something
     already printed, and the packaging is set in the upright Cyrillic,
     not Lora's Bulgarian localised forms */
  font-feature-settings:"locl" 0;
}
/* the back of the model is the same photograph dimmed, so its stamp is
   dimmed with it or the mark floats a shade brighter than the box */
.box3d.is-3d .box3d__face--back .boxstamp{ filter:brightness(.92); }

/* ============================================================ SIDE PRINT
   The legal panels on the two sides: ingredients, nutrition and the
   barcode on one, the cooking method and the recipe QR on the other.
   The copy and the markup are in box3d.js; this is the typography.

   THE SIDES ARE NOT STRIPED. The product photograph is square-on and
   shows neither of them, so they used to be given the front's stripe
   carried round the corner — a guess. The real panels are a solid
   field with the flavour's print ink on it, which is also the only way
   4px type is ever going to read. `is-printed` is set by box3d.js on a
   face it has copy for; a flavour with no copy keeps the stripe it has
   today. All three have copy as of 2026-08-18.

   EVERYTHING SCALES OFF --b-w like the rest of the model, so one
   panel serves the 268px box on products.html and the 146px box in a
   shop card without a media query. The base size is 4% of the face
   width, which is where the cooking steps measure on the real box —
   6px at product size, 3.3px in a card. That is meant to be too small
   to read: it is fine print, and fine print seen from across a room
   is a texture. What matters is that it degrades into TYPE rather
   than into dirt, which is why it is set rather than scanned.

   The one liberty taken is that the smallest classes — the ingredient
   paragraphs and the nutrition table — are set a shade larger than
   true scale. At their real 2.4% they stop being grey and start being
   speckle.

   Nothing in here takes the pointer: the whole side is a surface to
   grab and spin, and a text selection starting mid-drag would fight
   that. The panels are aria-hidden inside a scene that is already
   role="img" — this is print on a model of a box, not page content.
   ============================================================ */
.box3d__face--left.is-printed,
.box3d__face--right.is-printed{
  background-image:none; background-color:var(--b-field);
  background-position-x:0;
}
.boxprint{
  position:absolute; left:0; right:0; bottom:0;
  /* 22.91% is the clip-path's shoulder: all the print sits in the
     wall, clear of the gable */
  top:22.91%;
  display:flex; flex-direction:column;
  padding:calc(var(--b-d) * .075) calc(var(--b-d) * .085) calc(var(--b-d) * .055);
  font-family:var(--display); font-size:calc(var(--b-w) * .0224);
  font-weight:500; line-height:1.2; color:var(--b-print);
  text-align:center; text-wrap:pretty;
  /* Lora ships Bulgarian localised Cyrillic and the page is lang="bg",
     so `т` and `д` would arrive in their handwritten-derived forms. The
     site's own type wants exactly that; this panel is a reproduction of
     something already printed, and the packaging is set in the upright
     forms. So `locl` is off HERE and nowhere else. */
  font-feature-settings:"locl" 0;
  pointer-events:none; user-select:none; -webkit-user-select:none;
}
.boxprint p, .boxprint ol{ margin:0; padding:0; }
.boxprint b{ font-weight:700; }
.boxprint small{ font-size:.84em; font-weight:400; }

/* The fleuron that opens each panel and closes the recipe: a short
   hairline fading out at both ends with a lozenge set into the middle.
   Short is the point — the box prints an ornament about a quarter of
   the panel wide, not a rule across it. */
.boxprint__rule{ position:relative; height:.9em; flex:0 0 auto; }
/* .1em, not the .07em this started at: below about a pixel the line
   lands between device rows and renders as nothing at all on one
   panel while showing on the other, which reads as a missing rule
   rather than a fine one */
.boxprint__rule::before{
  content:""; position:absolute; left:35%; right:35%; top:50%; height:.1em;
  background:linear-gradient(90deg, transparent, currentColor 22%,
                                    currentColor 78%, transparent);
}
.boxprint__rule::after{
  content:""; position:absolute; left:50%; top:50%;
  width:.34em; height:.34em; margin:-.17em 0 0 -.17em;
  background:currentColor; transform:rotate(45deg);
}
.boxprint__lead{ font-weight:700; }

/* ---- the recipe side ---- */
.boxprint--recipe{ justify-content:space-between; }
.boxprint__steps{ list-style:none; }
/* a dot above every step, the separator the box actually prints */
.boxprint__steps li{ position:relative; padding:1.15em .2em 0; font-weight:700; }
.boxprint__steps li::before{
  content:""; position:absolute; left:50%; top:.42em;
  width:.3em; height:.3em; margin-left:-.15em;
  border-radius:50%; background:currentColor;
}
.boxprint__yield{
  display:flex; align-items:center; justify-content:center;
  gap:.5em; font-size:1.06em;
}
.boxprint__yield span{ display:inline-flex; align-items:center; gap:.3em; white-space:nowrap; }
.boxprint__ico{ width:1.15em; height:.9em; flex:0 0 auto; overflow:visible; }
.boxprint__ico:last-of-type{ width:.9em; }

.boxprint__scan{ display:flex; align-items:flex-end; gap:.4em; text-align:left; }
/* Four classes deep so it clears .shop-card:hover .shop-card__media img,
   which would otherwise nudge and scale the QR on every card hover. */
.box3d.is-3d .box3d__face .boxprint__qr{
  width:44%; height:auto; max-width:none; flex:0 0 auto;
  /* position/z-index too: .product__media img hands every image on that
     page a z-index of 2, which is the layer the face's key light sits
     on — the QR is printed on the box and is lit with the rest of it */
  position:static; z-index:auto;
  filter:none; transform:none; transition:none;
  /* navy on white with no alpha channel, so it is multiplied onto the
     field rather than masked — which is how ink on coloured stock
     behaves anyway. The face carries a transform, so it is already a
     stacking context and the blend cannot reach past it. */
  mix-blend-mode:multiply;
}
.boxprint__note{
  position:relative; align-self:flex-end;
  padding-bottom:.15em; font-weight:700; font-size:.94em; line-height:1.15;
}
.boxprint__arrow{
  position:absolute; right:100%; bottom:1.05em;
  width:2.05em; height:1.35em; margin-right:.06em; overflow:visible;
}

/* ---- the small-print side ----
   Set smaller than the recipe side, as it is on the box: this panel
   has four ingredient declarations and a nutrition table to fit into
   the same wall. The sizes below are the ones that land it inside the
   panel with the foot resting on the bottom margin — the block is
   `margin-top:auto`, so any slack opens above the table rather than
   pushing the barcode off the board. */
.boxprint--facts{ font-size:calc(var(--b-w) * .0162); line-height:1.15; }
.boxprint--facts .boxprint__lead{ font-size:1.3em; margin-bottom:.3em; }
/* Centred and ragged, which is how the current print sets it — the
   justified, first-line-indented block this replaced was the older
   packaging. It is also the block that gives way: if the webfont
   never arrives and the fallback sets wider, SOMETHING has to go,
   and losing the last line of an ingredient list nobody can read at
   this size beats pushing the barcode and the net weight off the
   bottom of the board. */
.boxprint__ing{
  flex:0 1 auto; min-height:0; overflow:hidden; hyphens:auto;
}
.boxprint__ing p{ margin-bottom:.2em; }

/* flex-end, so the two columns finish level: on the box the storage
   rule and the net weight sit on the same last band, and the column
   that runs longer eats upwards into the gap above the table rather
   than hanging below its neighbour. */
.boxprint__foot{
  display:flex; align-items:flex-end; gap:.6em;
  margin-top:auto; padding-top:.4em; text-align:left;
}
/* Two columns, as printed: the nutrition table with the storage rule
   under it on the left, the barcode with the trader block and the net
   weight on the right — so the storage line and the net weight sit on
   the same last line of the panel the way they do on the box. */
.boxprint__col{ flex:1 1 55%; }
/* the nutrition table: a grid rather than a <table>, so a row's label
   can carry a <small> sub-line without a nested cell */
.boxprint__nut{
  display:grid; grid-template-columns:1fr auto auto;
  font-size:.88em; line-height:1.08;
  border-top:.09em solid; border-left:.09em solid;
}
.boxprint__nut > span{
  padding:.14em .2em; border-bottom:.09em solid; border-right:.09em solid;
}
.boxprint__nut small{ display:block; font-size:.78em; }
.boxprint__u, .boxprint__v{ text-align:center; white-space:nowrap; }
.boxprint__store{ font-weight:700; margin-top:.3em; }

.boxprint__mark{ flex:1 1 45%; }
/* Sizes here are set per class, never on `.boxprint__mark p` — that
   selector carries a type and would quietly outrank every class below
   it, which is how the net weight ended up the same size as the
   ingredient list it is supposed to dominate. */
.boxprint__mark p{ margin-bottom:.3em; }
.boxprint__bars{ display:block; width:88%; height:2.6em; margin-bottom:.08em; }
/* the outer digits sit either side of the guard bars, as printed */
.boxprint__ean{
  font-size:1.05em; letter-spacing:.06em; white-space:nowrap;
  display:flex; justify-content:space-between; width:96%;
}
/* stepped down so its three lines break where the box breaks them
   rather than turning "13," into a fourth */
.boxprint__trader{ font-size:.85em; }
.boxprint__claim{ font-size:.95em; margin-bottom:0; }
.boxprint__net{
  display:flex; align-items:baseline; justify-content:space-between;
  gap:.3em; font-size:1.5em; font-weight:700; margin-bottom:0;
}
.boxprint__net small{ font-size:.55em; font-weight:400; }

/* ---- what it stands on ----
   Two shadows, because one never reads: a wide, soft, blue pool for the
   light the room bounces around, and a small dark one for the line of
   actual contact, where nothing gets underneath the board. Both tighten
   as the box lands (--b-scale), which is most of what sells the drop —
   a shadow that stayed the same size would make the box look like it
   was sliding on glass.

   --b-shw and --b-shy come from box3d.js and are what stop it being a
   sticker under a picture. A box turned edge-on stands on half the
   ground a square-on box does, so the pool narrows with it; and because
   the model pivots about its middle rather than its base, tilting it
   back lifts the box clear of the floor unless the shadow rises the
   same distance to meet it. */
/* The element is the box's actual footprint, worked out rather than
   nudged into place, so the two gradients inside it can be positioned
   against something real. The model's centre sits 0.83 × --b-w below
   the top of the scene (half the scene, plus the pivot drop); tilted to
   the resting −18°, the base's near edge falls a further 0.4066 and its
   far edge 0.2335 (½·--b-h·cos18 ∓ ½·--b-d·sin18). That is a footprint
   0.173 deep centred at 1.150 — and 1.124 wide, the same resting
   footprint --b-shw is measured against, which is why --b-shw arrives
   already scaled to 1 here. */
.box3d__shadow{
  position:absolute; left:50%; top:calc(var(--b-w) * 1.0635);
  width:calc(var(--b-w) * 1.124); height:calc(var(--b-w) * .173);
  margin-left:calc(var(--b-w) * -.562);
  opacity:var(--b-opacity);
  transform:
    translateY(calc(var(--b-w) * var(--b-shy)))
    scale(calc((.72 + .28 * var(--b-scale)) * var(--b-shw)), var(--b-scale));
  pointer-events:none;
}
.box3d__shadow::before,
.box3d__shadow::after{
  content:""; position:absolute; inset:0;
  transition:transform .46s var(--ease-out), opacity .46s var(--ease-out);
}
/* Both are pushed towards the viewer. A light hung straight overhead
   puts the shadow exactly under the box, where the box is standing on
   it and nothing of it can be seen — which is what the first attempt
   at this did. Every photograph of a product on a table is lit from
   behind and above, so the shadow pools out towards the camera; these
   translateY values are that, and they are what makes the box look
   like it is on the shelf rather than in front of it. */
/* Each gradient holds its darkness out to about half its radius before
   it starts to fall away. A plain centre-to-transparent ramp spends all
   of its density in the middle — which is the one part of the shadow
   the box is standing on and nobody can see — and leaves 3% of a navy
   at the edges, where the whole effect actually has to happen. */
.box3d__shadow::before{
  background:radial-gradient(closest-side,
    rgba(14,40,80,.34), rgba(14,40,80,.26) 52%, rgba(14,40,80,0) 100%);
  transform:scale(1.42,2.5) translateY(16%);
}
.box3d__shadow::after{
  background:radial-gradient(closest-side,
    rgba(10,26,52,.58), rgba(10,26,52,.44) 46%, rgba(10,26,52,0) 92%);
  transform:scale(1,1.7) translateY(26%);
}
/* lifted off the surface: the pool widens and thins, the contact line
   loosens its grip */
.box3d.is-3d:hover .box3d__shadow::before,
.box3d.is-3d:focus-within .box3d__shadow::before{ transform:scale(1.66,2.95) translateY(18%); opacity:.8; }
.box3d.is-3d:hover .box3d__shadow::after,
.box3d.is-3d:focus-within .box3d__shadow::after{ transform:scale(1.18,2) translateY(28%); opacity:.62; }

/* ---- "drag to rotate" cue, retired once they have ----
   z-index because the hint is a sibling the scene is appended after:
   without it the cue is painted underneath the very box it is pointing
   at, which is where it spent its first outing. The arrow is a mask
   rather than an <svg> so the markup stays one translatable span, and
   it turns a few degrees on hover — the gesture, demonstrated. */
.box3d.is-3d .box3d__hint{
  display:inline-flex; align-items:center; gap:.5em; z-index:5;
  position:absolute; left:50%; bottom:calc(var(--b-w) * -.15);
  transform:translateX(-50%);
  margin:0; padding:.36em .95em .36em .8em; border-radius:999px;
  background:rgba(252,247,238,.9); border:1px solid var(--line);
  box-shadow:0 6px 16px -10px rgba(14,40,80,.5);
  font-family:var(--body); font-size:var(--label-sm); font-weight:700; letter-spacing:.1em;
  text-transform:uppercase; color:var(--ink); white-space:nowrap;
  /* rides the box's own fade-in rather than sitting there during it */
  opacity:calc(.88 * var(--b-opacity)); pointer-events:none;
  transition:opacity var(--t-med) var(--ease-out);
}
.box3d.is-3d .box3d__hint::before{
  content:""; width:1.05em; height:1.05em; flex:0 0 auto; opacity:.75;
  background:var(--accent, var(--notte));
  -webkit-mask:var(--b-turn-icon) center/contain no-repeat;
          mask:var(--b-turn-icon) center/contain no-repeat;
  transition:transform var(--t-med) var(--ease-out);
}
.box3d.is-3d:hover .box3d__hint::before{ transform:rotate(38deg); }
.box3d.is-touched .box3d__hint{ opacity:0; }

/* ---- where the boxes appear ---- */
.product__media .box3d{ position:relative; z-index:2; --b-w:min(268px, 54vw); }
.shop-card__media .box3d{ position:relative; z-index:1; --b-w:min(146px, 40vw); }
@media (max-width:900px){
  .product__media .box3d{ --b-w:min(258px, 56vw); }
}
@media (max-width:680px){
  .product__media .box3d{ --b-w:min(228px, 60vw); }
  .shop-card__media .box3d{ --b-w:min(134px, 38vw); }
}

@media (prefers-reduced-motion:reduce){
  .box3d.is-3d .box3d__hint{ transition:none; }
  /* the lift and the shadow's answer to it are still worth having —
     they just arrive at once instead of over half a second */
  .box3d__scene,
  .box3d__shadow::before,
  .box3d__shadow::after,
  .box3d.is-3d .box3d__hint::before{ transition:none; }
}

/* ============================================================ WHAT IS IN THE BOX
   The contents that tip out of the 3D box on /products. contents3d.js
   builds them; this is their geometry, their thickness, the bubble
   that speaks for the one you picked, and the open state of the
   alcove they land in.

   THE OBJECT IS THE PHOTOGRAPH. Until 2026-09-07 each item sat inside
   a drawn translucent bag — six faces of gradient plastic around the
   cut-out — and at the size these render the bags read as a hand of
   rectangular cards. Now the cut-out IS the object: contents3d.js lays
   the same image down a dozen times along Z (.pouch3d__layer), so the
   silhouette has thickness and the rim shades darker toward the middle
   the way the edge of anything solid does. No rectangle, no new bytes,
   nothing drawn that the photograph does not show.

   Everything derives from ONE number, --u-base, so a breakpoint only
   ever has to say how big an object is. --u-w / --u-h are written per
   item by contents3d.js — the width as a multiple of the base, the
   height from the photograph's own proportions.
   ============================================================ */

/* ---- the list, which is the whole feature with no JavaScript ----
   A photograph, a name and a line for each thing in the box is a
   complete answer to "what is in the box", so this is not a
   placeholder — it is what the page says until contents3d.js is
   certain it can do better, and what it goes on saying if it never
   can. The line is the same sentence the bubble will say. */
.contents{ flex:0 0 100%; margin-top:clamp(20px,3vw,30px); --u-base:min(186px, 34vw); }
.contents__lead{
  font-family:var(--body); font-size:var(--label-sm); font-weight:700;
  letter-spacing:.22em; text-transform:uppercase; color:var(--accent);
  margin:0 0 1rem; transition:color .35s var(--ease-out);
}
.contents__list{
  display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));
  gap:18px 16px; text-align:center;
}
.contents__item{
  display:flex; flex-direction:column; align-items:center; gap:.5em;
  font-size:.8rem; font-weight:700; color:var(--notte);
}
.contents__item img{
  width:auto; max-width:100%; max-height:120px;
  filter:drop-shadow(0 10px 16px rgba(14,40,80,.24));
}
/* On the field, like everything else in this panel — and note the
   sibling above: .contents__item, the NAME, is already --notte. Only
   its description had been left in body grey. (The bubble's copy is a
   different element on a different ground: .contents__bubble__text sits
   on the bubble's own cream and correctly stays --ink.) */
.contents__desc{ margin:0; font-size:.78rem; font-weight:400; line-height:1.5; color:var(--notte); }

/* ---- upgraded ----
   The overlay is the alcove itself: inset:0 against .product__media,
   the same box .product__panel paints, so an object placed at "60%
   across" lands 60% across the lit niche and not 60% across some other
   rectangle. It takes no pointer — the box underneath has to stay
   tappable everywhere an object isn't — and hands it back on the
   objects and on the close button. */
.contents.is-3d{ position:absolute; inset:0; z-index:3; margin:0; pointer-events:none; }
/* Not display:none — that drops the box's contents out of the
   accessibility tree along with the picture, so a screen-reader user
   loses "what is in the box" the moment this file swaps the list for
   objects. Clipped instead (the site's .sr-only technique): gone for
   sighted users, still read in order — name and line — for assistive
   tech. The objects and the bubble are the decorative layer; this list
   stays the content, open or closed. */
.contents.is-3d .contents__list{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%);
  white-space:nowrap; border:0;
}
.contents.is-3d .contents__lead{
  position:absolute; left:3%; top:2%; margin:0;
  font-size:var(--label-sm); letter-spacing:.18em;
  opacity:0; transform:translateY(-4px);
  transition:opacity var(--t-med) var(--ease-out), transform var(--t-med) var(--ease-out);
}
.contents.is-3d.is-open .contents__lead{ opacity:1; transform:none; }

/* the cue, while nothing is chosen. It stands where the bubble will —
   the band along the floor that the slots keep clear — and steps aside
   for it rather than sharing the alcove with it: one voice at a time.
   Not under the eyebrow, where it ran into the signature. */
.contents__hint{
  position:absolute; left:6%; right:6%; bottom:5%; margin:0; text-align:center;
  font-size:.76rem; line-height:1.4; font-style:italic; color:var(--ink);
  opacity:0; transform:translateY(4px);
  transition:opacity var(--t-med) var(--ease-out) .25s, transform var(--t-med) var(--ease-out) .25s;
}
.contents.is-open:not(.has-focus) .contents__hint{ opacity:.78; transform:none; }

.contents__stage{ position:absolute; inset:0; }

/* ---- the way out ---- */
.contents__close{
  position:absolute; right:2%; top:1.5%; z-index:60;
  width:2.1em; height:2.1em; padding:0; border-radius:50%;
  display:grid; place-items:center;
  font-family:var(--body); font-size:1rem; line-height:1; color:var(--ink);
  background:rgba(252,247,238,.92); border:1px solid var(--line); cursor:pointer;
  box-shadow:0 8px 18px -12px rgba(14,40,80,.6);
  opacity:0; transform:scale(.8); pointer-events:none;
  transition:opacity var(--t-med) var(--ease-out), transform var(--t-med) var(--ease-out),
             background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.contents.is-open .contents__close{ opacity:1; transform:none; pointer-events:auto; }
.contents__close:hover{ background:var(--accent); color:#fff; }
.contents__close:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }

/* ============================================================ ONE OBJECT
   The host carries the flight (from the box to its slot), the arc
   wrapper the rise-and-fall that bends it, the scene the resting tilt
   and the pointer, the pivot the engine's rise/scale, the model the
   engine's rotate, and the layers are the object. Nothing here is a
   rectangle: every box is exactly the photograph's box, and where the
   photograph is transparent so is the object. */
.pouch3d{
  --u-d:calc(var(--u-w) * var(--u-dr));      /* thickness: the object's own */
  /* written by the engine (box3d.js spin(), prefix --u-) */
  --u-opacity:1; --u-scale:1; --u-rise:0; --u-shw:1; --u-shy:0;
  /* written by contents3d.js: the resting tilt, the tumble out of the
     box, and the flight */
  --u-tilt:0deg; --u-tumble:0deg;
  --u-fx:0px; --u-fy:0px; --u-fs:1; --u-fo:1; --u-arc:0px; --u-delay:0ms;
  --u-lift:0px; --u-grow:1;

  position:absolute; width:var(--u-w); pointer-events:auto;
  transform:
    translate(-50%,-50%)
    translate(var(--u-fx), var(--u-fy))
    scale(calc(var(--u-fs) * var(--u-grow)));
  /* two opacities multiplied on purpose: --u-opacity is the engine's,
     --u-fo is the flight out of the box and back into it. Either can
     run without cancelling the other. */
  opacity:calc(var(--u-fo) * var(--u-opacity));
  /* The flight. Its curve is its own rather than --ease-out, which
     spends four fifths of the travel in the first fifth of the time —
     on a throw across half the alcove that is a flash followed by a
     crawl. This one leaves briskly and still has somewhere to go when
     the arc below starts bringing it down, which is what makes the
     two read as one path. */
  transition:transform .78s cubic-bezier(.3,.9,.3,1) var(--u-delay),
             opacity .36s var(--ease-soft) var(--u-delay);
}

/* ---- the arc ----
   A straight line from the box to the floor is a thing being lowered
   on a string. This wrapper lifts and drops on the same clock as the
   flight, so the sum of the two is a toss: up and out, then down into
   place — and it tumbles as it goes, landing upright. The two legs
   have their own curves: the rise eases out (it is thrown), the fall
   eases in and settles (it lands). Only on the way OUT — contents3d.js
   drops .is-out for the way back, and things get put back in a box
   rather than tossed. */
.pouch3d__arc{ position:relative; width:var(--u-w); height:var(--u-h); }
.pouch3d.is-out .pouch3d__arc{ animation:pouch-arc .78s var(--u-delay) both; }
@keyframes pouch-arc{
  0%  { transform:translateY(0) rotate(var(--u-tumble));
        animation-timing-function:cubic-bezier(.2,.65,.4,1); }
  44% { transform:translateY(calc(var(--u-arc) * -1)) rotate(calc(var(--u-tumble) * .3));
        animation-timing-function:cubic-bezier(.5,0,.35,1); }
  100%{ transform:translateY(0) rotate(0deg); }
}

.pouch3d__scene{
  position:absolute; inset:0;
  perspective:calc(var(--u-w) * 3.6); perspective-origin:50% 45%;
  cursor:grab; -webkit-tap-highlight-color:transparent;
  /* a horizontal drag spins it, a vertical swipe still scrolls — the
     same bargain box3d.js strikes, and it matters more here: there are
     four of these across the middle of a phone screen */
  touch-action:pan-y;
  transform:rotate(var(--u-tilt)) translateY(var(--u-lift));
  transition:transform .46s var(--ease-out);
}
.pouch3d:hover, .pouch3d:focus-within{ --u-lift:calc(var(--u-w) * -.035); }
.pouch3d.is-dragging .pouch3d__scene{ cursor:grabbing; transition-duration:.18s; }
.pouch3d__scene:focus-visible{ outline:2px solid var(--accent); outline-offset:6px; border-radius:10px; }

.pouch3d__pivot{
  position:absolute; inset:0; transform-style:preserve-3d;
  transform:translateY(calc(var(--u-h) * var(--u-rise))) scale(var(--u-scale));
}
.pouch3d__model{ position:absolute; inset:0; transform-style:preserve-3d; }

/* ---- the layers: the object itself ----
   Deep specificity on purpose, and the same trap box3d.js documents:
   .product__media img hands every image in this column a width, a
   drop-shadow and a z-index, and inheriting those here shrinks the
   food to 82% of the alcove and hangs a shadow on all twelve slices.

   contain, not cover: the photograph's box IS the object's box, so
   nothing is cropped and nothing is padded — the silhouette is the
   whole shape. Each slice sits at --u-z of the thickness (+.5 front,
   −.5 back) and is darkened by --u-br, both written by contents3d.js.
   Filters are per slice and the slices are leaves, so preserve-3d
   above them is untouched — a filter on the model would flatten it. */
.contents.is-3d .pouch3d__layer{
  position:absolute; inset:0; display:block;
  width:100%; height:100%; max-width:none;
  object-fit:contain; object-position:center;
  transform:translateZ(calc(var(--u-d) * var(--u-z, 0)));
  filter:brightness(var(--u-br, 1)) saturate(.94);
  z-index:auto; pointer-events:none; user-select:none; -webkit-user-drag:none;
  transition:none;
}
/* The two faces. Each hides its own back so that, turned round, the
   mirrored copy is what shows and not the front seen through itself.
   The front carries the object's own shadow, in the plane of its face
   — what lifts it off the alcove instead of lying on it; a drop-shadow
   rather than a box-shadow because a box-shadow is a rectangle. */
.contents.is-3d .pouch3d__layer--front{
  --u-z:.5;
  filter:drop-shadow(0 calc(var(--u-w) * .05) calc(var(--u-w) * .055) rgba(14,40,80,.36));
  backface-visibility:hidden; -webkit-backface-visibility:hidden;
}
.contents.is-3d .pouch3d__layer--back{
  transform:rotateY(180deg) translateZ(calc(var(--u-d) * .5));
  /* the far side of anything is the far side: same thing, less light */
  filter:brightness(.88) saturate(.9);
  backface-visibility:hidden; -webkit-backface-visibility:hidden;
}

/* ---- ground shadow ----
   The same two numbers the box's shadow takes, written by the same
   engine: --u-shw is how much of its resting footprint the object
   still covers as it turns, --u-shy how far it has ridden up as it
   was tipped back. */
.pouch3d__shadow{
  position:absolute; left:50%; top:calc(var(--u-h) * .9);
  width:calc(var(--u-w) * .92); height:calc(var(--u-w) * .16);
  margin-left:calc(var(--u-w) * -.46);
  transform:
    translateY(calc(var(--u-w) * var(--u-shy)))
    scale(var(--u-shw), 1);
  background:radial-gradient(closest-side,
    rgba(14,40,80,.32), rgba(14,40,80,.18) 52%, rgba(14,40,80,0) 100%);
  pointer-events:none;
  transition:opacity var(--t-med) var(--ease-out);
}
.pouch3d:hover .pouch3d__shadow,
.pouch3d:focus-within .pouch3d__shadow{ opacity:.72; }

/* ---- the name ----
   Under the object and outside the scene, so it never turns with it:
   a label that rotates with the object it labels stops being a label.
   Quieter than it was — the bubble now does the talking, and this is
   a caption, not a card. */
.pouch3d__label{
  position:absolute; left:50%; top:calc(100% + var(--u-w) * .04);
  transform:translateX(-50%);
  padding:.3em .72em; border-radius:999px; white-space:nowrap;
  background:rgba(252,247,238,.82);
  font-family:var(--body); font-size:var(--label-sm); font-weight:700;
  letter-spacing:.08em; text-transform:uppercase; color:var(--ink);
  pointer-events:none;
  transition:opacity var(--t-med) var(--ease-out);
}

/* ---- one of them, looked at properly ----
   It grows where it stands and lifts a little, and the others step
   back — smaller and fainter, not gone, so the group is still legible
   as a group and the one you picked is the one nearest you. The
   captions go: the bubble below names the chosen one, and the others'
   were being half-covered by it as it grew — "BOLOGNESE SAU" under a
   nest of pasta is worse than no caption. They return with the group. */
/* 1.18, not more: the sauce is the widest object and sits nearest the
   wall, and at 1.22 its grown edge crossed the panel's inset */
.pouch3d.is-focused{ --u-grow:1.18; --u-lift:calc(var(--u-h) * -.08); z-index:80; }
.contents.has-focus .pouch3d__label{ opacity:0; }
.contents.has-focus .pouch3d:not(.is-focused){
  --u-grow:.94;
  opacity:calc(var(--u-fo) * var(--u-opacity) * .48);
}

/* ---- the bubble ----
   The band along the floor of the alcove, kept clear by the slots in
   contents3d.js. Its tail is aimed at the chosen object (--u-tail, in
   per cent of the bubble). Under the focused object in the stack: a
   thing standing in front of its own caption, not behind it. */
.contents__bubble{
  --u-tail:50%;
  /* inside .product__panel's own 2% inset, so it sits on the shelf
     rather than hanging over its lip */
  position:absolute; left:4%; right:4%; bottom:3.4%; z-index:50;
  padding:.7em 1em .8em; border-radius:16px;
  background:rgba(255,253,247,.95); border:1px solid var(--line);
  box-shadow:0 18px 34px -24px rgba(14,40,80,.6);
  text-align:left;
  opacity:0; transform:translateY(8px); pointer-events:none;
  transition:opacity .28s var(--ease-out), transform .36s var(--ease-out);
}
.contents__bubble.is-in{ opacity:1; transform:none; }
.contents__bubble__tail{
  position:absolute; top:-7px; left:var(--u-tail); width:13px; height:13px;
  margin-left:-7px; transform:rotate(45deg);
  background:inherit;
  border-left:1px solid var(--line); border-top:1px solid var(--line);
  border-top-left-radius:3px;
  transition:left .36s var(--ease-out);
}
.contents__bubble__name{
  margin:0 0 .15em;
  font-family:var(--display); font-size:.94rem; font-weight:700; line-height:1.25;
  color:var(--accent); transition:color .35s var(--ease-out);
}
.contents__bubble__text{ margin:0; font-size:.8rem; line-height:1.45; color:var(--ink); text-wrap:pretty; }

/* ============================================================ THE BOX, OPEN
   The box does not vanish when it opens — it steps back and up and
   becomes the thing the objects came out of, which is the only way the
   pile in front of it reads as its contents rather than as a second
   product. The offset is in --b-w, the box's own width and the one
   number its whole model derives from, so it keeps its relationship to
   the alcove at every breakpoint instead of being re-tuned at each. */
.product__media{ flex-wrap:wrap; transition:min-height .62s var(--ease-out); }
.product__media .box3d{ transition:transform .62s var(--ease-out); }

/* The box is a flex item aligned to the FLOOR of the alcove, and the
   alcove is what grows — so a retreat written as a fixed multiple of
   --b-w leaves the box wherever the new height happens to put it, which
   at 1280px was 45% down a panel it was supposed to have vacated.

   So it is solved rather than eyeballed. --alcove is the open height,
   named once and used for both the min-height and this; the box's own
   centre sits (--alcove − --alcove-pad − half a scene) from the top,
   and it wants to be at .17 of the alcove:

     T = .17·A − (A − pad − .64·w) = pad + .64·w − .83·A

   Half a scene is .64·--b-w because .box3d__scene is 1.28 × --b-w tall.
   Change where the box parks and only the .17 moves.

   The alcove grew on 2026-09-07 to make room for the bubble along its
   floor; the slots in contents3d.js sit correspondingly higher. */
.product.is-open .product__media{ --alcove:clamp(620px,64vw,780px); }
.product.is-open .product__media .box3d{
  transform:
    translateY(calc(var(--alcove-pad) + var(--b-w) * .64 - var(--alcove) * .83))
    scale(.46);
}
/* the cue has been taken, and it points at the gesture that is no
   longer the interesting one */
.product.is-open .box3d__hint{ opacity:0; }

/* ---- the signature gets out of the way ----
   The box retreats to the top of the alcove, which is exactly where the
   flavour is signed across the back wall — so the two land on each
   other and the word comes out half-eaten: "Bolog", with a box parked
   on the rest of it. Reading that as a z-order problem and fading the
   word is the wrong fix — it is still half-eaten, only fainter.

   The geometry says it cannot stay where it is at the size it is. The
   gutter left of the retreated box is about 130px of a 400px panel and
   the word at display size wants 240; there is nowhere in the open
   alcove it fits whole. Moving the BOX down instead is worse — it runs
   straight into the back row of objects, which have no room to give.

   So it shrinks into that gutter and sits under the eyebrow: fully
   visible and fully legible, which is what it could not be at any size
   that overlapped the box. Same script, same angle, same colour, just
   written smaller — a signature on a card rather than on a poster. */
.product.is-open .product__media .product__panel{ --word-o:.68; }
.product.is-open .product__media .product__panel::before{
  top:4.5%; left:3.5%;
  font-size:clamp(1.15rem, 1.9vw, 1.5rem);
}

/* Room for the objects to land in. The alcove grows rather than the
   objects shrinking into the space that was already there — a fan of
   four at the size that space allowed was four things you could not
   tell apart. */
.product.is-open .product__media{ min-height:var(--alcove); }

/* The cursor stays `grab`. The box is doing two jobs now, but dragging
   is still the one the pointer is being offered — `pointer` would take
   away the only affordance the rotation has and tell a half-truth about
   the tap in its place. The cue under the box names both. */

@media (max-width:900px){
  .contents{ --u-base:min(172px, 33vw); }
}
@media (max-width:680px){
  /* a step smaller than the phone can fit: at 37vw the nest of pasta
     sat on the sauce's caption, and there is no slot layout at this
     width where four objects at that size and their captions all
     clear each other */
  .contents{ --u-base:min(132px, 34vw); }
  /* one column here, so the alcove has the whole page width and can
     afford to be taller; --alcove drives the retreat with it. Taller
     again than the desktop panel: the bubble wraps to four lines at
     this width and needs the band under the objects to be deeper. */
  .product.is-open .product__media{ --alcove:clamp(680px,160vw,780px); }
  .pouch3d__label{ padding:.26em .6em; }
  .contents__hint{ font-size:.72rem; }
  .contents__bubble{ left:2.5%; right:2.5%; bottom:2.8%; padding:.65em .85em .75em; }
  .contents__bubble__name{ font-size:.92rem; }
  .contents__bubble__text{ font-size:.78rem; }
}

@media (prefers-reduced-motion:reduce){
  /* the objects still arrive and still turn under a finger — they just
     stop travelling to get there */
  .pouch3d,
  .pouch3d__scene,
  .contents__bubble,
  .contents__bubble__tail,
  .product__media,
  .product__media .box3d{ transition:none; }
  .pouch3d.is-out .pouch3d__arc{ animation:none; }
}

/* ============================================================ STOCKISTS (find-us.html)
   The map and the list of shops that carry the boxes. The list is the
   page's content — real markup, readable with no JS and no network —
   and the map is what makes it quick to answer "which of these is
   near me". Laid out in that order of importance: on a narrow screen
   the map is a fixed-height panel and the list runs underneath it. */
.stockists{ padding:clamp(40px,6vw,72px) 0 clamp(64px,10vw,128px); }
.stockists__inner{
  display:grid; grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);
  gap:clamp(28px,4vw,48px); align-items:start;
}

/* the postal code they arrived with, answered before anything else */
.stockists__code{
  margin:0 0 clamp(20px,3vw,32px); padding:.85em 1.1em; border-radius:14px;
  border:1px solid var(--warn-border); background:var(--warn-bg); color:var(--warn-ink);
  font-size:.95rem; font-weight:600;
}

/* ---- the map panel ----
   z-index:0 puts it BELOW body::before's paper grain (same trick as
   .product__media): a map is the one thing on this page that would
   otherwise read as pasted in from another website. Don't raise it.

   Since 2026-08-25 it carries the delivery circle as well as the shop
   pins, and a caption underneath saying which is which — two kinds of
   mark on one map is exactly when a legend stops being decoration. */
.stockists__maphelp{
  margin:.6em .2em 0; font-size:.78rem; line-height:1.5;
  color:var(--ink-soft);
}
.stockists__map{
  position:relative; z-index:0;
  height:clamp(420px,58vh,600px);
  border:1px solid var(--line); border-radius:20px; overflow:hidden;
  background:var(--panna-2);
  box-shadow:0 26px 50px -34px rgba(22,48,107,.55);
}

.smap__surface{
  position:absolute; inset:0; overflow:hidden;
  cursor:grab; touch-action:none;      /* one finger drags the map */
  -webkit-user-select:none; user-select:none;
  -webkit-tap-highlight-color:transparent;
}
.smap.is-grabbing .smap__surface{ cursor:grabbing; }

/* Tiles are laid out in world pixels inside this layer; the layer's
   own transform is the whole pan/zoom. transform-origin must stay at
   the top-left corner — the projection maths assumes it. */
.smap__layer{
  position:absolute; top:0; left:0; width:0; height:0;
  transform-origin:0 0;
}
.smap__tile{
  position:absolute; display:block; max-width:none;
  opacity:0; transition:opacity .3s var(--ease-out);
  /* Warm the standard OSM raster into the house cream — sampled to sit
     between --panna and --panna-2, not to look "vintage". Per tile and
     not on the layer: these are per-pixel colour operations, so they
     leave no seams, and a filter on a zero-sized layer is the kind of
     thing engines disagree about. */
  filter:sepia(.3) saturate(.7) hue-rotate(-8deg) brightness(1.05) contrast(.9);
}
.smap__tile.is-in{ opacity:1; }

/* the tile host is unreachable (offline, or missing from the CSP's
   img-src): say so over the paper instead of showing a blank hole */
.smap__blank{
  position:absolute; inset:auto 12% 50% 12%; margin:0; text-align:center;
  font-size:.92rem; color:var(--ink); opacity:0; transition:opacity var(--t-med) var(--ease-out);
  pointer-events:none;   /* it is a message, not a target — see .smap__note */
}
.smap--noTiles .smap__blank{ opacity:.8; }
.smap--noTiles .smap__layer{ display:none; }

/* ---- pins ----
   Positioned by store-map.js in screen pixels; everything below is
   the shape hanging off that point. The anchor's translate puts the
   TIP of the teardrop on the coordinate, which is the only anchoring
   that stays honest as you zoom. */
.smap__pins{ position:absolute; inset:0; pointer-events:none; z-index:3; }
.smap__pin{
  position:absolute; top:0; left:0; padding:0; border:0; background:none;
  pointer-events:auto; cursor:pointer; line-height:0;
}
.smap__pin.is-off{ display:none; }
.smap__pinAnchor{
  position:relative; display:block; width:30px; height:42px;
  transform:translate(-50%,-100%);
}
.smap__pinBody{
  position:absolute; inset:0;
  transform-origin:50% 100%;
  transition:transform var(--t-med) var(--ease-out), opacity var(--t-med) var(--ease-out);
  animation:smap-drop .5s var(--ease-out) backwards;
  animation-delay:calc(var(--i,0) * 90ms + 180ms);
}
/* A shop has been chosen: the map has flown to it, so the pins it is not
   about stand down. Faded, not hidden — they are still where the next
   choice gets made from, and eight of them sit within a kilometre of each
   other over the centre. */
.smap--focused .smap__pin:not(.is-active) .smap__pinBody{ opacity:.42; }
.smap--focused .smap__pin:not(.is-active):hover .smap__pinBody,
.smap--focused .smap__pin:not(.is-active):focus-visible .smap__pinBody{ opacity:1; }
.smap__pin:hover .smap__pinBody,
.smap__pin:focus-visible .smap__pinBody,
.smap__pin.is-active .smap__pinBody{ transform:translateY(-4px) scale(1.06); }

.smap__pinBody svg{
  width:100%; height:100%; display:block;
  fill:var(--pomodoro); stroke:var(--oro-soft); stroke-width:1.4;
  filter:drop-shadow(0 4px 6px rgba(22,48,107,.35));
}
.smap__pinNum{
  position:absolute; top:24%; left:0; right:0; text-align:center;
  font-family:var(--body); font-size:.76rem; font-weight:700; line-height:1;
  color:var(--panna); letter-spacing:0;
}
/* the ground the pin stands on — small, soft, and directly under the
   tip, because the light on this page comes from overhead */
.smap__pinShadow{
  position:absolute; left:50%; bottom:-2px; width:22px; height:7px;
  transform:translateX(-50%);
  background:radial-gradient(50% 50% at 50% 50%, rgba(22,48,107,.38), rgba(22,48,107,0) 70%);
}
.smap__pinLabel{
  position:absolute; left:50%; bottom:calc(100% + 4px); transform:translateX(-50%) translateY(4px);
  padding:.3em .7em; border-radius:999px; border:1px solid var(--line);
  background:var(--white-soft); color:var(--notte);
  font-family:var(--body); font-size:.74rem; font-weight:700; line-height:1.3;
  white-space:nowrap; opacity:0; pointer-events:none;
  box-shadow:0 8px 18px -12px rgba(22,48,107,.8);
  transition:opacity var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out);
}
.smap__pin:hover .smap__pinLabel,
.smap__pin:focus-visible .smap__pinLabel,
.smap__pin.is-active .smap__pinLabel{ opacity:1; transform:translateX(-50%) translateY(0); }

@keyframes smap-drop{
  from{ opacity:0; transform:translateY(-22px) scale(.86); }
  to  { opacity:1; transform:translateY(0) scale(1); }
}

/* ---- controls, credit, hint ---- */
.smap__controls{ position:absolute; top:14px; right:14px; z-index:4; display:flex; flex-direction:column; gap:8px; }
.smap__btn{
  width:38px; height:38px; display:grid; place-items:center;
  border:1px solid var(--line); border-radius:12px; background:var(--white-soft);
  color:var(--notte); cursor:pointer; padding:0;
  box-shadow:0 10px 20px -14px rgba(22,48,107,.9);
  transition:background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out);
}
.smap__btn svg{ width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; }
.smap__btn:hover{ background:var(--notte); color:var(--panna); }
.smap__btn:active{ transform:scale(.94); }
.smap__btn:disabled{ opacity:.4; cursor:default; }
.smap__btn:disabled:hover{ background:var(--white-soft); color:var(--notte); }

/* required by the OpenStreetMap tile usage policy — keep it legible */
.smap__credit{
  position:absolute; right:0; bottom:0; z-index:4; margin:0;
  padding:.25em .6em; border-top-left-radius:8px;
  background:rgba(252,247,238,.86); color:var(--ink);
  font-size:var(--label-sm); letter-spacing:.01em; line-height:1.4;
}
.smap__credit a{ color:var(--oro-ink); text-decoration:underline; text-underline-offset:2px; }

.smap__hint{
  position:absolute; left:50%; bottom:18px; z-index:4; margin:0;
  transform:translateX(-50%) translateY(6px);
  padding:.5em 1em; border-radius:999px;
  background:rgba(15,34,71,.9); color:var(--panna);
  font-size:.78rem; font-weight:600; white-space:nowrap; max-width:88%;
  opacity:0; pointer-events:none;
  transition:opacity var(--t-med) var(--ease-out), transform var(--t-med) var(--ease-out);
}
.smap__hint.is-in{ opacity:1; transform:translateX(-50%) translateY(0); }

/* ============================================================ PICKER MODE
   The same map on shop.html: the kvartals we deliver to, shaded and
   named, and the pin the customer drops on their own address. Only
   the parts that differ live here — the surface, the tiles, the
   controls and the credit are the ones above.
   ============================================================ */
.smap--picker .smap__surface{ cursor:crosshair; }
.smap--picker.is-grabbing .smap__surface{ cursor:grabbing; }

/* The coverage region. One <path> per layer holding every kvartal as
   a circle, all wound the same way, so the default nonzero fill rule
   renders their UNION — a flat region with no seam where two of them
   overlap. The edge is the same shape drawn wider underneath, which
   is how the outline has no internal lines either. Green because it
   is the same "yes" as the verdict panel's. */
.smap__zones{
  position:absolute; inset:0; z-index:2;
  width:100%; height:100%; pointer-events:none; overflow:visible;
}
.smap__zoneEdge{ fill:rgba(90,140,104,.42); }
.smap__zoneFill{ fill:rgba(126,173,138,.30); }

/* ---- the kvartal names on it ----
   Positioned in screen pixels by store-map.js like the pins, and
   likewise never in the way: the whole surface is a click target for
   dropping a pin, so nothing here takes the pointer. */
.smap__areas{ position:absolute; inset:0; z-index:2; pointer-events:none; }
.smap__area{
  position:absolute; top:0; left:0;
  display:flex; align-items:center; gap:5px;
  transform-origin:0 0; white-space:nowrap;
  /* the dot marks the centre; the name hangs to its right */
  margin:-5px 0 0 -5px;
}
.smap__area.is-off{ display:none; }
.smap__areaDot{
  width:9px; height:9px; flex:0 0 auto; border-radius:50%;
  background:var(--panna); border:2px solid rgba(72,116,86,.85);
  box-shadow:0 2px 5px -1px rgba(22,48,107,.4);
}
.smap__areaName{
  font-family:var(--body); font-size:var(--label-sm); font-weight:700; line-height:1;
  color:var(--ok-ink);
  /* set on the map itself rather than in a chip: 26 chips is a rash,
     and a halo keeps fine type legible over any tile underneath */
  text-shadow:0 0 3px var(--panna), 0 0 3px var(--panna), 0 0 6px var(--panna);
}
.smap__area.is-pick .smap__areaDot{ background:var(--oro); border-color:var(--notte); transform:scale(1.25); }
.smap__area.is-pick .smap__areaName{ color:var(--notte); font-size:.78rem; }

/* ---- the dropped pin ----
   Navy, where the stockist pins are tomato: on a page that links to
   both maps, "a shop" and "your address" must never be the same
   object in two places. */
.smap__drop{ position:absolute; top:0; left:0; pointer-events:none; line-height:0; z-index:4; }
.smap__dropAnchor{ position:relative; display:block; width:30px; height:42px; transform:translate(-50%,-100%); }
.smap__dropBody{
  position:absolute; inset:0;
  animation:smap-drop .42s var(--ease-out) backwards;
}
.smap__dropBody svg{
  width:100%; height:100%; display:block;
  fill:var(--notte); stroke:var(--oro-soft); stroke-width:1.4;
  filter:drop-shadow(0 4px 6px rgba(22,48,107,.4));
}
.smap__drop.is-ok  .smap__dropBody svg{ fill:#2F6B42; }
.smap__drop.is-no  .smap__dropBody svg{ fill:#8A6A22; }
/* "This is you" on /stockists — blue, and deliberately neither of the two
   above. Green would read as a yes on a page the customer was sent to
   because the answer was no, and amber would repeat a refusal they have
   already had. Blue is the one colour on this map that is reporting a
   fact rather than delivering a verdict. */
.smap__drop.is-you .smap__dropBody svg{ fill:#2563C9; }
/* Since 2026-09-11 this carries the customer's full address, not a
   kvartal name — "улица Александър Стамболийски 128" rather than
   "Лозенец". So it can no longer be nowrap on a pill: at .74rem that
   line is ~15rem, wider than the checkout map's own column, and a
   nowrap label centred on a pin near the edge runs out of the panel and
   is clipped. It wraps inside a capped measure instead, and the radius
   drops from the pill's 999px — a two-line 999px box is a lozenge with
   the text pinched at both ends. Clamped at two lines because the pin
   is a confirmation, not a place to read an essay, and a third line
   would start covering the map it is standing on.
   Still centred on the pin: a label that reads back an address the
   customer is checking belongs over the thing it names. */
.smap__dropLabel{
  position:absolute; left:50%; bottom:calc(100% + 4px); transform:translateX(-50%);
  padding:.34em .72em; border-radius:11px; border:1px solid var(--line);
  background:var(--white-soft); color:var(--notte);
  font-family:var(--body); font-size:.74rem; font-weight:700; line-height:1.3;
  /* width:max-content is doing real work, not tidying. This box is
     absolutely positioned inside .smap__drop, whose own width is the
     30px pin anchor — so shrink-to-fit gave it the width of its LONGEST
     WORD ("Стамболийски", measured 118px) and max-width never came into
     it. max-content asks for the whole string on one line and the cap
     below is then what actually bites. */
  width:max-content; max-width:13rem; text-align:center;
  /* NOT text-wrap:balance. Balancing narrows the box to even the lines
     out — measured 118px against the 208px it was allowed — and the
     address then needed four lines and was clipped to two. An address
     the customer is being asked to CHECK is the one string on this page
     that must never be quietly cut, so it gets the full measure and
     three lines; the longest real Sofia address measured here ("бул.
     Александър Стамболийски 20, Център") lands in two. */
  display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
  box-shadow:0 8px 18px -12px rgba(22,48,107,.8);
}
.smap__drop.is-ok .smap__dropLabel{ border-color:var(--ok-border); background:var(--ok-bg); color:var(--ok-ink); }
.smap__drop.is-no .smap__dropLabel{ border-color:var(--warn-border); background:var(--warn-bg); color:var(--warn-ink); }
.smap__drop.is-you .smap__dropLabel{
  border-color:rgba(37,99,201,.34); background:rgba(37,99,201,.10); color:#1B4894;
}

/* Where Enter drops the pin. Shown only while the map has keyboard
   focus, because for everyone else the pointer already says where. */
.smap__crosshair{
  position:absolute; left:50%; top:50%; z-index:3;
  width:22px; height:22px; margin:-11px 0 0 -11px;
  opacity:0; pointer-events:none;
  transition:opacity var(--t-fast) var(--ease-out);
  background:
    linear-gradient(var(--notte),var(--notte)) 50% 0/2px 100% no-repeat,
    linear-gradient(var(--notte),var(--notte)) 0 50%/100% 2px no-repeat;
}
.smap__surface:focus-visible .smap__crosshair{ opacity:.75; }

/* What the reverse geocode is doing, when it is worth saying. It lies
   inside the surface, and on the picker the whole surface is "put the
   pin here" — so like every other overlay it must not take the pointer,
   or a click meant for the map behind it drops a pin under the message. */
.smap__note{
  position:absolute; left:12px; bottom:12px; right:96px; z-index:4; margin:0;
  pointer-events:none;
  padding:.5em .8em; border-radius:12px;
  background:rgba(252,247,238,.94); border:1px solid var(--line);
  color:var(--ink); font-size:.74rem; line-height:1.45;
  box-shadow:0 10px 22px -18px rgba(22,48,107,.9);
}

@media (prefers-reduced-motion: reduce){
  .smap__dropBody{ animation:none; }
}

/* ---- the list beside it ----
   A scroll window the height of the map, not a column. Eight entries
   stacked beside a 600px map ran to twice its height, so the page
   itself became the scroll and the map slid out of view while you
   were still reading addresses. Now the two columns are one height
   and the list scrolls inside its own frame — with the heading kept
   out of the window as its title, and enough room under the last
   entry that the fade never sits on a button.

   The fade is on the aside rather than on the scroll box: a mask on
   the box would fade its own scrollbar with it. scroll-padding is
   what store-map.js's scrollIntoView honours, so a shop chosen on the
   map lands above the fade and not under it. */
.stockists__aside{ position:relative; }
.stockists__aside .form-eyebrow{ margin-top:0; }
.stockists__scroll{
  max-height:calc(clamp(420px,58vh,600px) - 2.4rem);
  overflow-y:auto; overscroll-behavior:contain;
  padding:6px 12px 36px 6px; margin:-6px -6px 0;
  scroll-padding-bottom:40px;
  scrollbar-width:thin; scrollbar-color:var(--oro-soft) transparent;
}
.stockists__aside::after{
  content:""; position:absolute; left:0; right:14px; bottom:0; height:38px;
  background:linear-gradient(180deg, rgba(252,247,238,0), var(--panna));
  pointer-events:none;
}
.stockists__list{ display:grid; gap:14px; }
.stockist{
  border:1px solid var(--line); border-radius:16px; background:var(--white-soft);
  padding:4px 4px 12px; overflow:hidden;
  transition:border-color var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out);
}
.stockist.is-active{ border-color:var(--carbonara); box-shadow:0 16px 32px -26px rgba(22,48,107,.9); }
.stockist.is-flash{ outline:3px solid var(--carbonara); outline-offset:3px; }

.stockist__btn{
  display:grid; grid-template-columns:auto minmax(0,1fr); gap:14px; align-items:start;
  width:100%; padding:14px 14px 10px; border:0; background:none; cursor:pointer;
  text-align:left; font-family:var(--body); color:inherit;
}
.stockist__num{
  display:grid; place-items:center; width:30px; height:30px; border-radius:50%;
  background:var(--notte); color:var(--panna); font-size:.86rem; font-weight:700; line-height:1;
}
.stockist__text{ display:block; }
.stockist__name{ display:block; font-family:var(--display); font-size:1.08rem; font-weight:700; color:var(--notte); line-height:1.2; }
.stockist__area{ display:block; margin-top:.2em; font-size:var(--label-sm); font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--oro-ink); }
.stockist__addr{ display:block; margin-top:.5em; font-size:.92rem; line-height:1.5; opacity:.86; }
.stockist__btn:hover .stockist__name{ color:var(--carbonara-text); }

/* ---- the folding panel under a chosen shop ----
   The Google Maps button ships OPEN in the markup and is folded shut by
   store-map.js, which adds .is-foldable to each entry it wired (see
   wireList()). No JS, or an entry stockists.js doesn't know, and the
   button is simply there — the addresses and the links to them are this
   page's content, the map is the convenience.

   grid-template-rows 0fr → 1fr rather than max-height, so the panel opens
   to exactly the height of its own contents and nothing has to guess a
   ceiling. A browser without fractional grid rows drops the declaration
   and leaves the panel open, which is the right way for this to fail. */
.stockist__more{ display:grid; grid-template-rows:1fr; }
.stockist.is-foldable .stockist__more{
  grid-template-rows:0fr;
  transition:grid-template-rows var(--t-med) var(--ease-out);
}
.stockist.is-foldable.is-active .stockist__more{ grid-template-rows:1fr; }
/* min-height:0 is what lets the grid row actually go to zero. It carries
   NO padding of its own: overflow clips at the padding box, so 8px of
   padding here is 8px of the button's top edge still showing under every
   shut panel. The spacing is the button's margin instead, which a
   zero-height clip box takes with it. */
.stockist__moreIn{ overflow:hidden; min-height:0; }

.stockist__map{
  display:inline-flex; align-items:center; gap:.4em;
  margin:2px 14px 6px 58px;
  padding:.62em 1.15em; border-radius:999px;
  border:1.5px solid var(--notte); background:transparent;
  font-size:.78rem; font-weight:700; letter-spacing:.03em;
  color:var(--notte); text-decoration:none;
  transition:background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out),
             transform var(--t-fast) var(--ease-out);
}
.stockist__map:hover{ background:var(--notte); color:var(--panna); transform:translateY(-1px); }
.stockist__map:active{ transform:translateY(0); }

.stockists__soon{
  margin:clamp(28px,4vw,44px) 0 0; text-align:center;
  font-size:.94rem; opacity:.88; text-wrap:pretty;
}
.stockists__soon .linkish{
  margin-left:.4em; color:var(--oro-ink); font-weight:700;
  text-decoration:underline; text-underline-offset:3px;
}
.stockists__soon .linkish:hover{ color:var(--notte); }

@media (max-width:960px){
  .stockists__inner{ grid-template-columns:minmax(0,1fr); }
  .stockists__map{ height:clamp(340px,52vh,420px); }
  /* under the map now, so its own height rather than the map's — tall
     enough for three entries, short enough to leave the page under it */
  .stockists__scroll{ max-height:min(64vh, 520px); }
}
@media (max-width:520px){
  .stockist__btn{ gap:11px; padding:12px 12px 8px; }
  .stockist__map{ margin:2px 12px 4px 53px; }

}

@media (prefers-reduced-motion:reduce){
  /* the pins are still the point — they just arrive already standing */
  .smap__pinBody{ animation:none; }
  /* and the panel is open or shut, without the travel in between */
  .stockist.is-foldable .stockist__more{ transition:none; }
}

/* ============================================================ MOTION / A11Y */
/* No border-radius here, and that is the fix rather than the omission.
    This rule is a bare pseudo-class — specificity (0,1,0) — which is exactly
    what .btn--pill, .tab, .qty__btn and .modal__close are, and it is declared
    3000 lines after all of them. On a tie the later rule wins, so its
    border-radius:4px was overriding theirs for as long as the element held
    keyboard focus: every pill button squared off to a rectangle, and both
    round buttons became squares, the moment you tabbed to them. It was there
    to soften the ring on a bare inline link, which is not worth reshaping
    every control on the site — and an outline follows the element's own
    radius anyway, which is the behaviour that was wanted in the first place. */
/* --notte, not --carbonara. The teal ring passed on the neutral grounds
   it was presumably checked against — 3.89 on cream, 4.15 on white —
   and failed on every one of the three PACKAGING FIELDS, which is
   exactly where a page head puts its buttons: 2.29:1 on the turquoise,
   2.80 on the yellow, 2.88 on the pink, against the 3:1 WCAG 2.4.11
   asks of a focus indicator. On /products that was the ring around
   "Към магазина" and "Поръчай", both of which sit on the turquoise.
   The navy is 6.94 / 8.49 / 8.71 on those three and 10.7-12.6 on cream,
   white and parchment — better on every ground the site has, not a
   trade. The per-flavour rings that already override this one
   (.box3d__scene, .lineup__next, .contents__close, .pouch3d__scene, at
   --accent) were measured on their own fields and clear it: 5.82,
   6.17, 4.72. They stay as they are. */
 :focus-visible{ outline:2px solid var(--notte); outline-offset:3px; }
/* ...except where the ground is the navy itself. No single ink can
   serve both a pastel field and a dark band — navy is 1.00:1 on the
   values band and 1.25:1 on the footer — so the two dark sections flip
   the ring to the cream they already set their text in: 11.79:1 and
   14.68:1. These are the only dark grounds on the site that hold
   anything focusable (the footer's nav and social links, the story
   page's closing links). */
.values :focus-visible,
.story :focus-visible,
.site-footer :focus-visible{ outline-color:var(--panna); }
/* Text fields sit --form-bind (4px) under their label, and 2px of ring at
   3px of offset reaches 5px past the box — into the label's descenders. One
   documented exception, on a type selector so it holds for every field on
   the site rather than per form. */
input:focus-visible, textarea:focus-visible, select:focus-visible{ outline-offset:1px; }
@media (prefers-reduced-motion:reduce){
  *{ animation:none !important; transition:none !important; scroll-behavior:auto !important; }
  .reveal{ opacity:1; transform:none; }
  .reveal-stagger > *{ opacity:1; transform:none; }
  .site-header.is-hidden{ transform:none; }
  .scroll-cue svg{ animation:none; }
  main{ animation:none; }
}
