/* Pekara ODRA landing: the stage and its choreography only. All shared world chrome (tokens,
   ground, header, footer, pills, links, forms) lives in vb-site.css, loaded by
   base.html for every public page. */


/* ===================== the stage and its views ===================== */

.vb-stage {
  position: relative;
  height: 100dvh;
  padding-top: var(--vb-head-h);
  overflow: hidden;
}

/* Reel law: views never fade or slide as blocks — a view is visible or it is not, and all
   motion happens on its child layers, opaque, clipped by the stage. index.js drives the
   choreography classes below. */
/* overflow-anchor: an animating strip changes the scrollable width every frame, Chrome's
   scroll anchoring nudges scrollLeft, and index.js resetting it to 0 turns that into jitter
   for the first frames of an entrance. index.js adds .is-settling around each transition. */
.vb-stage { overflow-anchor: none; }
.vb-stage.is-settling { overflow: hidden; }

.vb-view {
  position: absolute; inset: var(--vb-head-h) 0 46px;
  display: grid;
  visibility: hidden;
}
.vb-view.is-active { visibility: visible; }
/* Pre-paint hold, lifted by the inline boot script (web.py): on a /#kruh arrival every view
   stays hidden until index.js applies the real state in the same tick. Without it the
   server-rendered home painted for the ~60ms before the deferred script ran. */
body.vb-boot-hold .vb-view { visibility: hidden !important; }
/* an exiting view stays visible while its layers fly out */
.vb-view.is-exit { visibility: visible; }
/* momentary measurement state: rect of a hidden view as if it were settled */
.vb-view.is-measure { visibility: hidden !important; transform: none !important; transition: none !important; }

/* Reel easing vocabulary (--vbe-*) lives in vb-site.css: the whole world uses it. */

/* ===================== view: home ===================== */

/* No `position: relative` here: .vb-view already positions every view at
   `inset: var(--vb-head-h) 0 46px`. Relative re-applies its `top` on top of the stage's
   padding, dropping the composition 148px and pushing the CTA out of the clip at 768px tall. */
.vb-view--home { align-items: center; overflow: hidden; }
.vb-home__grid {
  width: 100%;
  /* grid item: default min-width:auto means min-content, so with nothing capping it the
     mobile loaf's `width: min(96vw, 520px)` pushed this grid to 470px inside a 390px phone */
  min-width: 0;
  /* Content edge derived from the crust, not the window: a plain clamp(22px, 16vw, 232px)
     freezes at 232px above ~1450px while the loaf, part of the board photograph, keeps
     tracking the viewport at 45.2vw — 329px of dead board at 2560x1251. The second term is
     the edge at which the clear band exactly fits the height-governed size, a fixed point:
         edge = 42.8vw - 4.8857em x size,  size = min(10.5vh, 148px)
     Below ~1450px it falls under the clamp, so max() keeps the old behaviour. .vb-h1 and
     .vb-home__lead both read this var, so edge and size cannot drift apart. */
  --vb-copy-edge: max(clamp(22px, 16vw, 232px), calc(42.8vw - 4.8857 * min(10.5vh, 148px)));
  padding-inline: var(--vb-copy-edge);
  display: block;
  /* No max-width + auto margins: centring assumed the loaf was an element in this grid, but
     it now lives in the viewport-fixed board photograph. Past ~1440px centring pushed the
     copy right faster than the bread moved, walking the headline into the crust (11.7vw over
     it at 1920). Full-bleed viewport padding is the space the size derivation assumes. */
  position: relative;
}
/* comp z-order: the loaf composites on top of the type, hiding part of the last word */
.vb-home__copy { position: relative; z-index: 1; }

.vb-h1 {
  font-family: 'Anton', var(--font-display); font-weight: 400;
  /* Sized to the clear band LEFT of the loaf, not run to its centre: the loaf is part of the
     board photograph, so nothing composites over the headline and the tail must stop short of
     the crust. Fractions measured off worktop.webp as `cover` renders it at 4:3 or wider,
     where cover scales to WIDTH and they hold at every desktop size:
         loaf core 45.2vw..81.9vw of the window, gap 2.4vw before the crust
         "DOBRO JUTRO" = 5.0857em in Anton-Regular (upm 2048, .012em tracking) governs
         size = (45.2vw - edge - 2.4vw) / (5.0857em - .2em)     [-.2em = margin-left]
     Line 1 governs, not line 3: "SVJEŽI KRUH" is 4.7331em, so it was the widest until
     "JUTRO" became "DOBRO JUTRO". Re-measure this constant whenever a line's text changes —
     it is the only thing keeping the tail out of the crust. tools/check_h1_measure.py
     ~85px at 1440, capped 148px. Three lines, not two: the two-line measure (5.9868em)
     forced ~76px. `42.8vw - edge` reproduces the old max(26.8vw, 42.8vw - 232px). */
  font-size: clamp(34px, min(calc((42.8vw - var(--vb-copy-edge)) / 4.8857), 10.5vh), 148px);
  /* Per-language measure: the EN line "GOOD MORNINGS" is wider than the HR constant covers,
     and one shared constant would shrink the Croatian headline to pay for it. The html[lang]
     overrides further down re-run the same formulas with the EN divisor (6.03 = measured
     6.2186em - .2em margin); tools/check_h1_measure.py verifies both live. */
  /* 1.08, not 1.171: three lines of Croatian caps at 1.171 waste ~40px of leading and carons
     still clear at 1.08. The 10.5vh term holds the three-line block inside the two-line
     height budget: 3.16 x 10.5vh ≈ 2.171 x 15vh. */
  line-height: 1.08; letter-spacing: .012em;
  color: var(--vb-gold); margin: 0 0 clamp(28px, 3.6vh, 46px);
  margin-left: -.2em;   /* optical: cancel Anton's left side bearing so caps align with the copy */
  white-space: nowrap;   /* the lines are sized to fit the clear band; never wrap them */
}
.vb-h1__line { display: inline-block; }

.vb-home__lead {
  /* comp measure is 584px, but the copy has to stop where the bread starts: at 1440 the full
     584px reached 52.8vw, 8vw into the crust */
  font-size: var(--t-lead); line-height: 28px; color: var(--vb-flour);
  max-width: 584px; margin: 0 0 36px;
}
/* The clear band is desktop geometry: it assumes `cover` scales the board to width, true only
   at 4:3 or wider. Below 881px cover scales to height and crops the sides, the loaf is nowhere
   near 45.2vw, and unscoped this squeezed the lead to 119px on a 390px phone. */
@media (min-width: 881px) {
  .vb-home__lead { max-width: min(584px, calc(42.8vw - var(--vb-copy-edge))); }
}

/* the landing's CTA is .vb-link .vb-link--display (modifier in vb-site.css) */

/* The phone keeps the desktop CTA — the gold display link, not a berry pill.
   It was a pill for two measured reasons, and the board has moved out from under both. The
   contrast one (3.4:1 with flour beneath it, unfixable by shifting the crop 8-28%) was taken
   when the copy sat mid-frame over the flour ring; the plate now sits at its right edge and the
   composition is top-anchored, so the link lands on dark grain — re-measured, and the numbers
   are in CHANGELOG 2026-08-13.
   The target one is still real, and it is answered for every .vb-link at once in vb-site.css's
   touch block — padding plus an equal negative margin, both orientations — rather than here.
   It used to be solved here and portrait-only, which left the same link 28px wide in landscape.
   What stays in this file is where the CTA SITS, which is composition and belongs to the stage. */
@media (max-width: 880px) and (orientation: portrait) {
  /* the CTA sits lower than the copy it follows, by request: the frame is top-anchored and has
     the room below, so this is spent from slack rather than taken from anything. 72, not 59,
     because the -13px press-box margin above eats into it — the gap that actually shows is
     72 - 13 = 59, against the 36 it was. Even 320x568, which has the least slack of any size,
     still clears the frame footer by 38px and does not scroll. */
  .vb-home__lead { margin-bottom: 72px; }
}

/* ---- and lower again, where the board makes it necessary ----
   At 390x844 — the commonest phone there is — the link's tail lands on a flour clump and reads
   1.01:1 over its own ink. Not an artifact of the press-box padding: scored over the content box
   alone it is worse than over the padding box (1.01 vs 1.24), so the flour is on the glyphs.
   Every other size is clear at the same place (5.56–7.25), which is what a photographic ground
   does — the plate scales to viewport height, so one height puts one clump under one line.
   Swept the CTA's own offset against the board, floor 4.5, worst across all six sizes:
       +0   1.24   +20  3.21   +40  5.15   +60  5.26   +80  5.25
   The cliff is between +20 and +40, so +60 is taken rather than +40: 20px of margin on the edge
   of a flour drift is not margin. Height-banded because 320x568 has 38px of slack and cannot
   afford it — and does not need it, measuring 4.78 where it already sits. 620px is the same
   floor the board shift uses. */
@media (max-width: 880px) and (orientation: portrait) and (min-height: 620px) {
  .vb-home__lead { margin-bottom: 132px; }
}

/* A little further down again, on the phones that have the room for it. Banded at 760 rather
   than added to the rule above because the two short bands are already close to their floor:
   375x667 clears the frame footer by 62px and 360x640 by 40, so another 28 would have 640
   almost touching it. 844/932/1024 clear by 203/271/343 and give it up without noticing. */
@media (max-width: 880px) and (orientation: portrait) and (min-height: 760px) {
  .vb-home__lead { margin-bottom: 160px; }
}


/* No loaf element in the home view: the real loaf is in the board photograph
   (.page-vb::before, body level, never swapped by the router), so it cross-fades to the
   flour-ring frame instead of flying off and home's entrance/exit animate copy only. */

/* ===================== view: catalog ===================== */

.vb-view--catalog { align-items: center; }
.vb-track {
  display: flex; align-items: stretch;
  gap: clamp(46px, 5.4vw, 84px);
  padding-inline: clamp(26px, 6vw, 96px) 33vw; /* right pad leaves the last card peeking */
  cursor: grab; user-select: none; -webkit-user-select: none;
  will-change: transform;
}
.vb-track.is-dragging { cursor: grabbing; }

.vb-card { flex: none; width: clamp(292px, 23vw, 350px); display: flex; flex-direction: column; }
/* One track, two card sets: the category rail and the active category's products. index.js
   toggles .is-off; everything is server-rendered so no-JS and crawlers see all cards. */
.vb-card.is-off { display: none; }

/* A set that FITS the stage composes centred: the asymmetric padding below exists for the
   drag world (left anchor + right peek), and with 3-5 cards on a wide screen it left-piled
   the strip into a lopsided frame. index.js measures the visible set (centerIfFits) and sets
   the class; an overflowing set keeps the drag world exactly as it was. The hint goes with
   it — there is nothing to drag when everything is already on screen. */
.vb-view--catalog.is-centered .vb-track { justify-content: center; padding-inline: clamp(26px, 6vw, 96px); }
.vb-view--catalog.is-centered .vb-catalog__hint { display: none; }
/* the rail card has no price, so its single pill keeps the product cards' right-edge rhythm */
.vb-card--cat .vb-card__buy { justify-content: flex-end; }
.vb-card__name {
  font-family: 'Anton', var(--font-display); font-weight: 400;
  font-size: clamp(30px, 2.6vw, 38px); line-height: 1.04; letter-spacing: .014em;
  color: var(--vb-gold); margin: 0 0 18px; min-height: 2.08em; text-wrap: balance;
}
/* the name is a button, dressed as the heading it sits in */
.vb-card__open {
  font: inherit; letter-spacing: inherit; color: inherit; text-align: left;
  border: 0; padding: 0; background: transparent; cursor: pointer;
}
.vb-card__open:hover { color: var(--vb-flour); }
.vb-card__photo { height: 176px; display: grid; place-items: center; margin-bottom: 18px; }
.vb-card__photo img {
  /* the cutout's body is ~60% of the image, so an oversized frame still renders ~168px of
     bread; the transparent halo just overflows the photo box */
  height: 276px; width: auto; max-width: none; object-fit: contain;
  /* -48px: a real photo's ink measured 50px below the sketches' ellipse line (the sketch's
     ink rides high in its box because the "foto stiže" label hangs under it, and the pilot
     photo read as sagging — owner call). The lift puts every photo's body on the same
     optical line the sketches drew; transparent halo is what crosses into the name band. */
  margin-top: -48px;
  filter: drop-shadow(0 16px 20px rgba(0, 0, 0, .5));
  transition: transform .35s var(--vb-ease);
}
.vb-card:hover .vb-card__photo img { transform: scale(1.04) rotate(-1.5deg); }
.vb-card__photo--sketch { position: relative; color: var(--vb-chalk); opacity: .75; }
.vb-card__photo--sketch svg { width: 200px; height: 128px; filter: url(#vb-chalk); }
.vb-card__soon {
  position: absolute; left: 50%; bottom: -4px; transform: translateX(-50%);
  font-size: var(--t-micro); font-weight: 600; letter-spacing: .22em; color: var(--vb-muted);
  text-transform: uppercase; white-space: nowrap;
}

.vb-card__desc {
  font-size: var(--t-small); line-height: 1.65; color: var(--vb-flour-dim); margin: 0 0 14px;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.vb-card__tag { font-size: var(--t-micro); font-weight: 700; letter-spacing: .24em; color: var(--vb-gold-link); margin: 0 0 16px; }

/* price left, pill right (owner, 2026-09-18: prices are back on the cards) */
.vb-card__buy { margin-top: auto; display: flex; align-items: center; justify-content: space-between; gap: 16px; }

/* .vb-price, .vb-pill and .vb-link are world grammar and live in vb-site.css; duplicating
   them here let the two copies of .vb-link diverge, with stylesheet order picking the
   winner. The landing's louder version is .vb-link--display. */

/* The foot band: the drag hint and the price-list link, bottom-LEFT, aligned with the track's
   own padding. Left because the board's bottom-right is its brightest region and the hint is
   the smallest, dimmest text on the page. The link (2026-09-03: /ponuda is this view's
   crawlable twin and nothing in the reel said so) shares the band so it sits on the same dark
   grain the hint was placed for, not on the flour. 44px band, like the crumb's press box. */
.vb-catalog__foot {
  /* bottom 16px, not 0: the view's box ends 46px above the stage (.vb-view inset) but the
     fixed footer is up to ~60px tall, so at 0 the link's 44px press box ran 14px under the
     footer (tools/check_keyboard.py: "under fixed chrome" at 1440x900). */
  position: absolute; left: clamp(26px, 6vw, 96px); bottom: 16px; z-index: 2;
  display: flex; align-items: center; gap: clamp(22px, 2.6vw, 44px);
  min-height: 44px;
}
.vb-catalog__hint {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--t-micro); font-weight: 600; letter-spacing: .2em; color: var(--vb-muted); margin: 0;
}
/* Dressed like the crumb (caps micro-link, 44px press box) but in gold-link: it leaves the
   reel, where the crumb and the hint only move within it. Stays through .is-centered — the
   hint hides there because there is nothing to drag; the price list is still a place to go. */
.vb-catalog__cjenik {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 0 4px;
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .2em; text-transform: uppercase;
  color: var(--vb-gold-link); transition: color .15s ease;
}
.vb-catalog__cjenik:hover { color: var(--vb-gold); }
/* Back to the category rail. Mirrors .vb-close's band on the left, dressed like the hint —
   caps micro-link — but with a 44px press box. Visible only while a category is open. */
.vb-catalog__crumb {
  position: absolute; top: 8px; left: clamp(26px, 6vw, 96px); z-index: 2;
  display: none; align-items: center; gap: 8px;
  min-height: 44px; border: 0; padding: 0 4px; background: transparent; cursor: pointer;
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .2em; text-transform: uppercase;
  color: var(--vb-flour-dim); transition: color .15s ease;
}
.vb-catalog__crumb:hover { color: var(--vb-gold); }
.vb-view--catalog.has-cat .vb-catalog__crumb { display: inline-flex; }

/* Foot band, crumb and close button are positioned against the view, not inside any flying
   layer, so while `is-exit` holds the catalog visible they stay painted over the incoming view
   as the only things that have not moved. */
.vb-view--catalog.is-exit .vb-catalog__foot,
.vb-view--catalog.is-exit .vb-catalog__crumb,
.vb-view--catalog.is-exit .vb-close { visibility: hidden; }

/* ---- catalog choreography (reel-exact) ----
   The text enters as one strip (shell from +72vw, long-tail ease-out, ~1s); the per-column
   stagger falls out of geometry, not scripted delays. Breads are a parallax layer: +130px
   extra, released 150ms later over ~1.05s. Nothing fades; the stage clips. */
.vb-shell { display: grid; }
.vb-view--catalog.tx-pre .vb-shell { transform: translateX(72vw); transition: none; }
.vb-view--catalog.tx-pre .vb-shell .vb-card .vb-card__img { transform: translateX(130px); transition: none; }
.vb-view--catalog.tx-enter .vb-shell { transform: none; transition: transform 1s var(--vbe-cascade); }
.vb-view--catalog.tx-enter .vb-shell .vb-card .vb-card__img { transform: none; transition: transform 1.05s var(--vbe-cascade) .15s; }
/* per-column cascade layered on the strip: each card carries a small extra offset released on
   an nth-child delay, so columns arrive in sequence while the shell travels as one surface */
.vb-view--catalog.tx-pre .vb-card { transform: translateX(52px); transition: none; }
.vb-view--catalog.tx-enter .vb-card { transform: none; transition: transform .9s var(--vbe-cascade); }
/* nth-child(N of ...): plain nth-child counts HIDDEN siblings too, and with the category rail
   and the other categories' cards display:none'd in the same track, the visible set could
   start at DOM position 6 and the whole cascade collapsed onto the flat .3s delay. */
.vb-view--catalog.tx-enter .vb-card:nth-child(2 of .vb-card:not(.is-off)) { transition-delay: .06s; }
.vb-view--catalog.tx-enter .vb-card:nth-child(3 of .vb-card:not(.is-off)) { transition-delay: .12s; }
.vb-view--catalog.tx-enter .vb-card:nth-child(4 of .vb-card:not(.is-off)) { transition-delay: .18s; }
.vb-view--catalog.tx-enter .vb-card:nth-child(5 of .vb-card:not(.is-off)) { transition-delay: .24s; }
.vb-view--catalog.tx-enter .vb-card:nth-child(n+6 of .vb-card:not(.is-off)) { transition-delay: .3s; }
/* the hover zoom shares the img's transform channel; it must not hijack the parallax
   transition mid-entrance */
.vb-view--catalog.tx-pre .vb-card:hover .vb-card__photo img,
.vb-view--catalog.tx-enter .vb-card:hover .vb-card__photo img { transform: none; }
/* exit toward the right (back to home) — accelerating, opaque */
.vb-view--catalog.tx-exit .vb-shell { transform: translateX(72vw); transition: transform .55s var(--vbe-in); }
/* ---- the category switch: SINK, swap unseen, RISE — both form factors ----
   The switch happens with the eye already ON the strip, and content changing in place must
   not send the world flying through the stage (the owner's verdict, phone first and then
   desktop: "that animation that scrolls fast — not exactly needed"). The strip sinks in
   240ms, index.js swaps the card set and resets scroll/drag while nothing is visible, and
   the new set rises on --vbe-inout — real velocity, then a STOP; no --vbe-cascade tail, so
   the shimmer crawl this entrance was once rebuilt to kill cannot return. The reel's
   horizontal language remains for ARRIVALS (home <-> catalog), where the strip genuinely
   enters or leaves the stage. The 110vw fly-through, the drag ride-back and the clearance
   poll are gone with it. */
.vb-view--catalog.tx-sink .vb-shell {
  transform: translateY(14px); opacity: 0;
  transition: transform .24s var(--vbe-in), opacity .22s ease;
}
.vb-view--catalog.tx-pre.tx-swap-in .vb-shell { transform: none; transition: none; }
.vb-view--catalog.tx-pre.tx-swap-in .vb-shell .vb-card .vb-card__img { transform: none; transition: none; }
.vb-view--catalog.tx-pre.tx-swap-in .vb-card { transform: translateY(26px); opacity: 0; transition: none; }
.vb-view--catalog.tx-enter.tx-swap-in .vb-shell { transform: none; transition: none; }
.vb-view--catalog.tx-enter.tx-swap-in .vb-shell .vb-card .vb-card__img { transform: none; transition: none; }
.vb-view--catalog.tx-enter.tx-swap-in .vb-card {
  transform: none; opacity: 1;
  transition: transform .45s var(--vbe-inout), opacity .38s ease;
}

/* ---- the same choreography, spoken phone (<=880) ----
   Below 881 the track scrolls natively, and the horizontal cascade moves the x-snap targets
   every frame (snap areas are TRANSFORMED border boxes) — which is why the scroller had to
   be clipped for 1.5s while it ran, and why an early swipe on a real iPhone landed dead.
   On the phone the cards RISE instead — the reveal voice the flow pages already speak:
   x-geometry never animates, index.js skips the clip below 881, and the strip is scrollable
   from its first painted frame. The nth-child stagger above outranks these shorthands, so
   the columns still arrive in sequence. Exits stay horizontal (they leave the stage; the
   110vw clearance rules below 880 already cover them). */
@media (max-width: 880px) {
  .vb-view--catalog.tx-pre .vb-shell,
  .vb-view--catalog.tx-enter .vb-shell { transform: none; transition: none; }
  .vb-view--catalog.tx-pre .vb-shell .vb-card .vb-card__img,
  .vb-view--catalog.tx-enter .vb-shell .vb-card .vb-card__img { transform: none; transition: none; }
  .vb-view--catalog.tx-pre .vb-card { transform: translateY(26px); opacity: 0; transition: none; }
  .vb-view--catalog.tx-enter .vb-card {
    transform: none; opacity: 1;
    transition: transform .55s var(--vbe-inout), opacity .45s ease;
  }
}

/* ---- the open-morph: neighbours are shoved off by the expanding column, rigid, no fade ----
   The distance is per side, set by index.js (setPushDistance) from the CHOSEN card's rect, so
   the shove always clears the viewport: a fixed 70vw did not when the chosen card was the one
   peeking at the far right (owner's captures, 2026-09-14: opening Lepinja parked Štapovi at
   the left edge for the whole morph, then the handoff cut it; the close popped it in from the
   same spot). Never shorter than the old 70vw; the fallbacks keep it when nothing set it. */
.vb-card.push-r { transform: translateX(var(--push-r, 70vw)); transition: transform .58s var(--vbe-inout); }
.vb-card.push-l { transform: translateX(var(--push-l, -70vw)); transition: transform .58s var(--vbe-inout); }
.vb-card.push-hold-r { transform: translateX(var(--push-r, 70vw)); transition: none; }
.vb-card.push-hold-l { transform: translateX(var(--push-l, -70vw)); transition: none; }
.vb-card.push-return { transform: none; transition: transform .58s var(--vbe-inout); }
/* Every piece of the chosen card must leave while its travelers fly: the chosen card is the
   one card not pushed aside, so anything left standing sits fully opaque at its original
   position for the whole 630ms morph. Everything that has a twin in the detail FLIES (photo,
   name, desc, price, anchor, €/kg) and is hidden the instant its flyer launches — the flyer
   is it. The pill has no twin and fades in the first 200ms rather than cutting.
   (2026-09-18: the desc was first hidden and re-shown, then held on the card for 380ms as a
   cross-fade — which read as text left behind once its neighbours had gone. It flies now.) */
.vb-card.vb-hide-parts .vb-card__photo,
.vb-card.vb-hide-parts .vb-card__name,
.vb-card.vb-hide-parts .vb-card__desc,
.vb-card.vb-hide-parts .vb-price,
.vb-card.vb-hide-parts .vb-sidrena { visibility: hidden; }
.vb-card.vb-hide-parts .vb-pill { opacity: 0; transition: opacity .2s ease; }
/* The RETURN morph's first act (detail -> catalog): the card is the visual TARGET under
   landing travelers, so here everything IS masked instantly — a separate class, because the
   timed fades above would show the desc for 380ms before hiding it. */
.vb-card.vb-hold-parts .vb-card__photo,
.vb-card.vb-hold-parts .vb-card__name,
.vb-card.vb-hold-parts .vb-card__desc,
.vb-card.vb-hold-parts .vb-card__buy { visibility: hidden; }
/* (.vb-card__tag is a rail-card piece only since 2026-09-17 — product cards lost their
   "KATEGORIJA · IZ NOĆNE PEKE" strap — and rail cards never morph, so it needs no mask.) */
/* The RETURN morph's second act (detail -> catalog only). vb-hold-parts is right while the
   card is covered by the detail view, but on the way back the card is the visual TARGET:
   holding everything hidden for the full 720ms meant the travelers landed into a half-empty
   card and the rest popped in visibility-style, with no transition. Mid-flight the card
   swaps to this narrower mask: the flying counterparts (photo, name, desc, price, anchor)
   stay hidden — their travelers are still in the air, unhiding them would double the ink —
   while the one piece that never flies, the pill, fades in under the landing travelers. */
.vb-card.vb-fly-holes .vb-card__photo,
.vb-card.vb-fly-holes .vb-card__name,
.vb-card.vb-fly-holes .vb-card__desc,
.vb-card.vb-fly-holes .vb-price,
.vb-card.vb-fly-holes .vb-sidrena { visibility: hidden; }
.vb-card.vb-fly-holes .vb-pill { animation: vb-parts-in .32s ease both; }
@keyframes vb-parts-in { from { opacity: 0; } to { opacity: 1; } }

/* ===================== view: detail ===================== */

.vb-view--detail { align-items: center; }
.vb-detail__grid {
  width: 100%; max-width: 1240px; margin-inline: auto;
  padding-inline: clamp(22px, 5vw, 72px);
  display: grid; grid-template-columns: .9fr 1.1fr; align-items: center;
  gap: clamp(28px, 4vw, 72px);
}
.vb-detail__photo { position: relative; display: grid; place-items: center; }
.vb-detail__photo img {
  width: min(50vw, 700px); height: auto; object-fit: contain;
  filter: drop-shadow(0 34px 42px rgba(0, 0, 0, .55));
}
/* travelers: fixed clones that fly between card and detail on the shared morph timeline */
.vb-tr--sketch {
  position: fixed; z-index: 60; pointer-events: none;
  transform-origin: top left;
  transition: transform .63s var(--vbe-inout);
  will-change: transform;
}
.vb-tr--sketch svg { width: 100%; height: 100%; }

.vb-traveler {
  position: fixed; z-index: 60; pointer-events: none;
  object-fit: contain;
  filter: drop-shadow(0 24px 30px rgba(0, 0, 0, .5));
  transform-origin: top left;
  transition: transform .63s var(--vbe-inout);
  will-change: transform;
}
.vb-tr-text {
  position: fixed; z-index: 60; pointer-events: none; margin: 0;
  transform-origin: top left;
  /* font-size animates INSTEAD of scale: Firefox rasterizes a transform-scaled flyer once at
     its starting scale and stretches the bitmap, so the glyphs and their text-shadow halo
     blurred through the whole flight and "settled" crisp only when the real element landed
     (Dorijan's third capture). A font-size transition re-rasterizes crisp every frame, and
     the width is set in EM so the wrap cannot change mid-flight. */
  transition: transform .63s var(--vbe-inout), font-size .63s var(--vbe-inout);
  will-change: transform;
  white-space: nowrap;
}
/* white-space: normal, overriding the traveler default: the name flyer is dst-sized and
   MUST wrap like its destination — with nowrap, "SAVIJAČA RAVNA (10 KOMADA)" flew as one
   long line overflowing the column while both endpoints render it wrapped (Dorijan's second
   capture). Short bread names hid this since v3 shipped. */
.vb-tr-text.vb-tr--name { font-family: 'Anton', var(--font-display); font-weight: 400; line-height: 1.04; letter-spacing: .014em; color: var(--vb-gold); white-space: normal; text-wrap: balance; }
/* the description flyer: body face, flour-dim, wrapping like both of its endpoints (card
   ~49ch, detail 52ch); line-height between the two so neither end shows a jump */
.vb-tr-text.vb-tr--desc { font-family: var(--font-body); font-weight: 400; line-height: 1.7; color: var(--vb-flour-dim); white-space: normal; }
.vb-tr-text.vb-tr--price { display: inline-flex; align-items: baseline; gap: 5px; color: var(--vb-gold); font-family: var(--font-display); font-weight: 700; letter-spacing: -.01em; }
/* font-size too: without it the € inherits the traveler's 36px price size while both
   endpoints render it at --t-small (13.5px), so 95px of content flew inside a 78.5px box.
   .vb-tr--price is a separate class from .vb-price and restates everything. */
.vb-tr-text.vb-tr--price i { font-style: normal; font-weight: 600; font-size: var(--t-small); }
/* the anchor / €/kg flyer: the .vb-sidrena grammar restated (a flyer inherits nothing from
   its source), body face, flour-dim label, the amount in flour. Its font-size rides the
   12.5 -> 13.5px transition like the price's; nowrap so the wrap cannot change mid-flight. */
.vb-tr-text.vb-tr--sidrena {
  font-family: var(--font-body); font-weight: 400; line-height: 1.25; letter-spacing: .01em;
  color: var(--vb-flour-dim); white-space: nowrap;
}
.vb-tr-text.vb-tr--sidrena b { font-weight: 700; color: var(--vb-flour); }
.vb-detail__sketch svg { width: min(30vw, 380px); height: auto; }
.vb-detail__sketch { color: var(--vb-chalk); }
.vb-detail__sketch .vb-card__soon { bottom: -8px; }   /* -34% floated it ~65px off the sketch */

.vb-detail__name {
  font-family: 'Anton', var(--font-display); font-weight: 400;
  font-size: clamp(44px, 4.8vw, 74px); line-height: 1.02; letter-spacing: .012em;
  /* 9.4em: the SAME character measure the card name wraps at (its box runs 8.9-9.7em across
     the clamp range). Em-relative on purpose — line breaks depend on measure IN EMS, so
     matching it makes the card and the detail break long names at the same words and the
     name morph never re-wraps mid-air. Both ends use text-wrap: balance, same split. */
  max-width: 9.4em;
  color: var(--vb-gold); margin: 0 0 20px; text-wrap: balance;
}
.vb-detail__desc { font-size: var(--t-body); line-height: 1.75; color: var(--vb-flour-dim); max-width: 52ch; margin: 0 0 30px; }
/* (.vb-facts and .vb-detail__buy retired 2026-09-17 with the facts row and the "Naruči za
   sutra" pill — the detail is name, copy and the price lockup, which is world grammar from
   vb-site.css.) */

.vb-close {
  position: absolute; top: 8px; right: clamp(22px, 5vw, 72px); z-index: 2;
  display: grid; place-items: center; width: 44px; height: 44px;
  border: 0; border-radius: 50%; background: transparent; cursor: pointer;
  color: var(--vb-flour); transition: color .15s ease, background .15s ease;
}
.vb-close:hover { color: var(--vb-gold); background: var(--vb-hair-soft); }

/* ---- detail choreography (reel-exact) ----
   Open is a shared-element morph driven from JS: the card's bread, title and price travel to
   their detail rects while the real pieces stay hidden until the travelers land.
   The copy fades in drifting right at ~420ms, the close button rotates in from "+" at
   ~600ms. (The chalk "slices" accent and its tx-go beat retired with the first real photo,
   2026-09-10 — chalk next to photography read as scaffolding. The facts row and the pill
   that shared the +420ms beat retired 2026-09-17; the beat now carries the copy alone.) */
.vb-view--detail.tx-morph #vb-detail-img,
.vb-view--detail.tx-morph .vb-detail__name,
.vb-view--detail.tx-morph .vb-detail__sketch,
.vb-view--detail.tx-morph .vb-detail__desc,
.vb-view--detail.tx-morph .vb-price--lg,
.vb-view--detail.tx-morph .vb-sidrena--lg { visibility: hidden; }   /* every piece with a flyer in the air */

/* (The desc used to fade in here at +420ms — tx-late — because it did not fly. It flies since
   2026-09-18; the tx-late beat now carries nothing and stays only as a timeline marker.) */

.vb-view--detail.tx-morph .vb-close { opacity: 0; transform: rotate(-45deg) scale(.8); transition: none; }
.vb-view--detail.tx-morph.tx-close-in .vb-close {
  opacity: 1; transform: none;
  transition: opacity .15s ease-out, transform .15s ease-out;
}

/* split exit toward home: info flies right, photo group flies left — fast, accelerating */
.vb-view--detail.tx-exit-split .vb-detail__info { transform: translateX(38vw); transition: transform .18s var(--vbe-in); }
.vb-view--detail.tx-exit-split .vb-detail__photo { transform: translateX(-36vw); transition: transform .18s var(--vbe-in); }

/* plain entrance for direct loads (#kruh-N boot): both sides glide in, opaque */
html.js .vb-view--detail.tx-pre .vb-detail__photo { transform: translateX(-36vw); transition: none; }
html.js .vb-view--detail.tx-pre .vb-detail__info { transform: translateX(38vw); transition: none; }
html.js .vb-view--detail.tx-enter .vb-detail__photo,
html.js .vb-view--detail.tx-enter .vb-detail__info { transform: none; transition: transform .6s var(--vbe-cascade); }

/* ===================== home choreography (reel-exact) =====================
   Enter (load, or from another view): headline from -72vw (ease-head, .54s, +90ms), lead+CTA
   from -46vw (ease-inout, .6s), loaf from +34vw (ease-loaf, .48s, +210ms). Exit: copy left
   -72vw (.63s), loaf right +36vw scaling to 1.22 (.57s). Opaque; the stage clips. */

html.js .vb-view--home.tx-pre .vb-h1 { transform: translateX(-72vw); transition: none; }
html.js .vb-view--home.tx-pre .vb-home__lead,
html.js .vb-view--home.tx-pre .vb-link { transform: translateX(-46vw); transition: none; }

html.js .vb-view--home.tx-enter .vb-h1 { transform: none; transition: transform .54s var(--vbe-head) .09s; }
html.js .vb-view--home.tx-enter .vb-home__lead,
html.js .vb-view--home.tx-enter .vb-link { transform: none; transition: transform .6s var(--vbe-inout); }

/* the exit offset must clear the viewport at any width, or the copy is still partly on-screen
   when the transition ends and the visibility cut pops it out. -72vw clears a DESKTOP: the
   copy column is narrower than the window there. */
.vb-view--home.tx-exit .vb-h1,
.vb-view--home.tx-exit .vb-home__lead,
.vb-view--home.tx-exit .vb-link { transform: translateX(-72vw); transition: transform .63s var(--vbe-in); }
/* On a phone the content IS the window, so every 72vw-class exit parks a clipped fringe of
   ink on-screen at its endpoint (seen on a real iPhone as "leftover words"). The HOME copy
   keeps its horizontal signature at 110vw clearance — it animates over a static
   composition. The catalog world's exits do NOT go sideways on a phone at all: a strip
   over a live snap scroller and a detail over a dynamic viewport SINK instead, the same
   vertical voice as every phone entrance ("spoken phone", above). */
@media (max-width: 880px) {
  .vb-view--home.tx-exit .vb-h1,
  .vb-view--home.tx-exit .vb-home__lead,
  .vb-view--home.tx-exit .vb-link { transform: translateX(-110vw); }
  .vb-view--catalog.tx-exit .vb-shell {
    transform: translateY(14px); opacity: 0;
    transition: transform .3s var(--vbe-in), opacity .26s ease;
  }
  .vb-view--detail.tx-exit-split .vb-detail__info,
  .vb-view--detail.tx-exit-split .vb-detail__photo {
    transform: translateY(12px); opacity: 0;
    transition: transform .22s var(--vbe-in), opacity .2s ease;
  }
}

/* ---- the phone opens a product WITHOUT the morph ----
   Three device rounds in one day — travelers overshooting, edge landings, the × hopping —
   were the platform's verdict on flying a shared element over a live snap scroller and a
   dynamic viewport. Below 881 index.js routes catalog <-> detail around the morph and the
   incoming view simply RISES, the voice the cards already speak: nothing flies, nothing is
   shoved, nothing hides mid-air. Desktop keeps the full choreography. */
@media (max-width: 880px) {
  html.js .vb-view--detail.tx-pre .vb-detail__photo,
  html.js .vb-view--detail.tx-pre .vb-detail__info { transform: none; transition: none; }
  html.js .vb-view--detail.tx-enter .vb-detail__photo,
  html.js .vb-view--detail.tx-enter .vb-detail__info { transform: none; transition: none; }
  .vb-view--detail.tx-pre .vb-detail__grid { transform: translateY(26px); opacity: 0; transition: none; }
  .vb-view--detail.tx-enter .vb-detail__grid {
    transform: none; opacity: 1;
    transition: transform .45s var(--vbe-inout), opacity .38s ease;
  }
}

/* ---- the composition sits above the stage's centre ----
   The view centres copy between the header and the 46px footer reserve, not in the viewport:
   at 2560x1251 the copy's optical centre sat at 54.8% against the loaf's 49%. Bottom padding
   lifts it by half of itself — pure layout, so it cannot collide with the choreography, which
   owns `transform` here. Guarded at 900px tall; below that 1366x768 clears by only ~12px. */
@media (min-width: 881px) and (min-height: 900px) {
  .vb-view--home { padding-bottom: 16vh; }   /* lift = 8vh: 100px at 1251, 115px at 1440 */
}

/* ===================== podnožje kadra =====================
   Off above 880: there the chrome still carries the since line itself — .vb-lockup__since sits
   under the wordmark — so a second copy at the foot of the stage would be a duplicate, and the
   stage is a clipped 100dvh box up there anyway.
   The mobile rules are in the ≤880 block below, with the rest of the frame. */
.vb-frame-foot { display: none; }

/* ===================== responsive =====================
   Chrome adaptations (--vb-head-h, utility row, lockup, nav) live in vb-site.css for every
   page; this file only adapts the stage. */

@media (max-width: 880px) {
  /* The stage scrolls on small screens and becomes the frame: a column that fills the screen,
     with .vb-frame-foot as its last item. Without this the views were block boxes of their own
     content height stacked under the header, so `align-items: center` had no slack to centre
     in and the composition sat at the top with the bottom ~40% left as bare board (measured:
     copy ended at y=509 of 844 on the landing, y=430 in the catalog). `flex: 1 0 auto` hands
     the active view that slack; shrink stays 0, so content taller than the frame still grows
     the page rather than being squeezed. */
  .vb-stage { height: auto; min-height: 100dvh; overflow: visible; display: flex; flex-direction: column;
              position: relative; }
  .vb-view { position: relative; inset: auto; display: none; }
  .vb-view.is-active { display: grid; flex: 1 0 auto; }
  /* The view that is leaving has to stay on screen long enough to fly out — but it must not be a
     second item in the frame's column, or the two views stack and the composition jumps a whole
     screen mid-transition. Absolute lifts it out of the flow and over the incoming one, which is
     what the desktop stage gets for free from positioning every view.
     `top: var(--vb-head-h)`: absolute insets resolve against the padding box, so 0 would be the
     stage's top edge and this puts it on the content edge instead, where the view actually sits.
     The stage carries .is-settling for the length of a transition (index.js), and that clips —
     without it the incoming shell at +72vw would widen the document while it flies. */
  .vb-view.is-exit {
    display: grid;
    position: absolute; left: 0; right: 0; top: var(--vb-head-h); bottom: 0;
  }

  /* The landing is ONE screen on a phone, and .vb-foot was the only reason it was not: a
     100dvh stage with a static footer after it made the document 962px against an 844px
     screen, so the page scrolled 118px to reveal a second footer under the one the frame
     already has. The podnožje kadra IS the landing's footer here — it carries the brand line —
     and the credit strip keeps its place on every inner route.
     Scoped by body class, NOT written bare: vb-router's ensureCSS only ever ADDS stylesheets,
     so index.css is still loaded after a PJAX hop to /lokacije, and a bare `.vb-foot` rule
     would follow it there and delete that page's footer. The landing is the one public body
     without .page-flow. */
  .page-vb:not(.page-flow) .vb-foot { display: none; }

  /* ---- podnožje kadra ---- */
  .vb-frame-foot {
    flex: none;
    /* Pinned to the bottom by `auto` margin, NOT by being the last child of a full column.
       The column is only full while a view is in flow, and TX["home>catalog"] leaves a gap:
       .is-active comes off the outgoing view at t=0 and does not land on the incoming one until
       t=510, while .is-exit is out of flow (absolute). For those ~510ms the stage had no
       `flex: 1 0 auto` child at all and this row rode up under the header — measured jumping
       from 1075 to 64 and back at 845x1129, which reads as the footer breaking and settling.
       `margin-top: auto` eats the free space itself, so the row sits on the bottom edge whether
       or not anything else is in the column. */
    margin-top: auto;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    /* 22px is the phone gutter --vb-copy-edge uses, so the rule below starts on the same
       vertical as the headline's left edge */
    padding: 4px 22px calc(6px + env(safe-area-inset-bottom));
  }
  .vb-frame-foot__since {
    font-size: var(--t-micro); letter-spacing: .2em; text-transform: uppercase;
    white-space: nowrap; margin: 0;
    display: inline-flex; align-items: center; gap: 10px;
    font-weight: 600; color: var(--vb-muted);
  }
  /* The comp's short rule. A pseudo-element and not a border: a border on the text box would
     run the width of the whole line.
     It is the one shrinkable thing in the row, and that is what keeps the narrowest phones off
     a second breakpoint. Both texts are `nowrap`, so their `min-width: auto` floor is their own
     text width and flexbox can take nothing from them; `min-width: 0` here lets the rule give
     back whatever the row is short. Measured at 320: the two texts plus gaps need 257.2px of
     the 276px available, so the rule draws at ~19px instead of 22 and nothing overflows. */
  .vb-frame-foot__since::before {
    content: ""; flex: 0 1 22px; min-width: 0; height: 1px;
    background: currentColor; opacity: .55;
  }
  /* Below ~344px the line cannot hold .2em however far the rule shrinks. Tracking is the
     cheapest thing to spend: .14em leaves the caps still reading as caps, where dropping to
     --t-fine would break the micro-caps step. */
  @media (max-width: 379.98px) {
    .vb-frame-foot__since { letter-spacing: .14em; }
  }

  .vb-home__grid {
    /* Symmetric, where it used to be 12/40. The asymmetry was right while the view was a block
       at the top of the stage and the bottom padding was the only thing between the copy and
       the footer; now the view centres in the frame, so uneven padding just moves the optical
       centre — it put the ink 14px (half of 40-12) above true centre. Same 52px total, so a
       screen too short to centre anything behaves exactly as before. */
    grid-template-columns: 1fr; gap: 30px; padding-block: 26px;
    /* Phone gutter, the same 22px the catalog track uses. The desktop edge is derived from the
       loaf's place in a 4:3 crop; below 881px `cover` scales to height instead and that
       derivation spends 16vw — 62px of a 390px phone — on empty board to the left. */
    --vb-copy-edge: 22px;
  }
  /* The three authored lines stay, sized to the board rather than the window. Below 881px
     `cover` scales the photograph by max(100vw/3840, 100vh/2880), so a size fixed in BOARD
     pixels holds its distance from the loaf at every phone crop; 167 of them is where the tail
     of "SVJEŽI KRUH" stops short of the flour crest ringing it — the real obstacle here, and
     brighter than the crust. 12vw over the old 16vw gutter put that tail on the flour: 1.05:1
     at 320x568 and 1.82:1 at 390x844 against a 3:1 floor, now 4.15 and 5.34. The first term is
     the 22px gutter as a hard stop, since these lines never wrap. */
  .vb-h1 { font-size: clamp(34px, min(calc((100vw - 44px) / 4.8857), max(4.35vw, 5.8vh)), 68px); }
  /* The gutter moved 40px left, so cap the measure: a full-width lead would spend those 40px
     running further across the crust instead of away from it. */
  .vb-home__lead { max-width: 28ch; }
  .vb-track {
    overflow-x: auto; padding-inline: 22px 18vw; gap: 40px;
    scroll-snap-type: x proximity; scroll-padding-inline: 22px;
    -webkit-overflow-scrolling: touch;
    cursor: default;
    /* The scroll indicator draws at the element's bottom EDGE, and with a content-tall track
       that edge is the Pogledaj button's baseline — the bar sat right on the pill (owner
       report). Padding moves the edge, not the cards: the indicator now rides 22px (the
       phone gutter) below the buttons. */
    padding-bottom: 22px;
  }
  /* (The TRACK-freeze rule lived here — .is-settling .vb-track { overflow-x: hidden } —
     because the snap targets chased the horizontal cascade, and an early swipe on a real
     iPhone landed dead. The phone entrance is now VERTICAL — see "spoken phone" in the
     choreography section — x-snap positions never move, so the freeze is gone and the strip
     is live from its first frame. The stage-level .is-settling clip at the top of this file
     STAYS at every width: exits fly to 110vw and would widen the document without it.) */
  /* stops iOS treating a pan that starts at the strip's left edge as the back-swipe */
  .vb-track { overscroll-behavior-x: contain; }
  /* 561-880 (portrait tablets): the desktop-style left-aligned strip, two cards and a peek. */
  .vb-card { scroll-snap-align: start; width: min(68vw, 320px); }
  /* The product name is a button and it is the obvious thing to tap, but a single-line title
     measured 31px against the 44 floor — every touch-target sweep in this file's history has
     reported it and none had fixed it. Padding plus an equal negative margin, the same trade the
     rest of the world uses: the press box grows to 45px and the heading's own rhythm, including
     the 2.08em two-line reservation that keeps card tops aligned, does not move.
     `display: block` so the padding applies on both sides of an inline-level button. */
  .vb-card__open { display: block; padding-block: 7px; margin-block: -7px; }
  /* Give the close button its own band. It is absolutely positioned against the view, so it
     shared the top row with the card titles; at 68vw the peeking card's title now starts left
     of it and ran under the glyph. Padding moves the strip, not the button. */
  .vb-view--catalog { padding-top: 52px; }
  /* the whole foot band: no drag hint on a native scroller, and the overlay menu already
     carries "Cjenik" */
  .vb-catalog__foot { display: none; }

  .vb-detail__grid { grid-template-columns: 1fr; gap: 26px; padding-block: 8px 46px; }
  .vb-detail__photo img { width: min(64vw, 320px); }
  .vb-detail__sketch svg { width: min(58vw, 300px); }
  /* (The detail × was a sticky grid item at top:100px here — reachable while a short phone
     scrolls, but 92px BELOW the catalog's ×, so the close button visibly hopped on every
     open and close (owner report). Both ×s now share the base rule: absolute, top 8, same
     spot in both views — the swap is invisible. Short-phone scroll reachability is paid
     with the back gesture, which closes the detail anyway.) */
}

/* ---- true phones: ONE product, centered (owner's call from device testing) ----
   The left-aligned strip reads as a cut-off list on a phone; the native pattern is a card
   held in the centre with slivers of its neighbours as the affordance. Same vw unit for the
   card and the side padding, so the centring is symmetric at every width: 78vw card + 11vw
   sides; the 16px gap leaves a ~27px sliver of each neighbour at 390. `mandatory`, not
   `proximity`: a release must always land ON a product, never between two. */
@media (max-width: 560px) {
  .vb-card { scroll-snap-align: center; width: 78vw; }
  .vb-track {
    scroll-snap-type: x mandatory;
    padding-inline: 11vw;
    gap: 16px;
  }
  /* The buy row on a 78vw card (250px at 320): "Sidrena cijena 1,50 €" nowrap (~150px) beside
     the pill (~100px) overflowed the card. The label may wrap; the amount never splits (its
     <b> stays nowrap), and the stack yields width before the pill does. */
  .vb-price-stack { min-width: 0; }
  .vb-sidrena { white-space: normal; }
  .vb-sidrena b { white-space: nowrap; }
}

/* SHORT phones, not narrow ones — the distinction this project has paid for before. The catalog
   card is a fixed ~412px (name 62.4 + photo 176 + desc 44.5 + buy ~48 + margins; the 19px tag
   strap left 2026-09-17 and the anchor line under the price, ~16px, took part of its room
   2026-09-18), so the catalog view needs 64 chrome + 52 close-button band + ~412 + 54 frame
   footer ≈ 582px.
   Any viewport shorter than 582 scrolls, and the landing must not: at 320x568 it was over by 14.
   The 52px band is not the lever — .vb-close is 44px tall at top: 8, so the band is exactly its
   bottom edge and shaving it puts the peeking card's title back under the glyph. The photo box
   is: it only has to hold the 128px chalk sketch (a real photo is 276px and overflows the box by
   design, at 176 as much as at 150), so 26px comes out of empty space and nothing drawn changes
   size. 555.9px total at 568, with 12px to spare. */
@media (max-width: 880px) and (max-height: 620px) {
  .vb-card__photo { height: 150px; }
}

/* ---- the landing composition sits LOW in the frame (portrait) ----
   Not a taste change: it is what pays for the bread. Sliding the portrait plate to its right
   edge (vb-site.css) brings a brighter band under the copy, and centred the lead measured 3.61
   at 390x844 and 3.70 at 320x568 against a 4.5 floor — it is 17px text, so WCAG scores it as
   normal, not large. Dropping the composition moves it off that band. Swept with the project's
   own tool at 10px steps, worst run inside .vb-home__copy, text-shadow off:

       drop      +55   +75   +85   +95  +105  +110  +115  +120  +160
       430x932  3.56  4.56  4.88  4.90  5.20  5.37  4.88  4.20  2.59
       390x844  3.42  4.21  4.74  4.67  4.91  5.05  4.67  3.81  2.73

   +110 is the peak and the cliff is between 115 and 120, so this is a measured optimum, not a
   round number — and it lands within 5px of where the handoff's own mockup draws the headline
   (263px of air above it against the 158 centring gives).
   Expressed as margin on the grid item because `align-items: center` centres the MARGIN box:
   220px of top margin moves the border box down exactly half of it. The margin must never
   exceed the frame's slack or the view grows and the landing starts scrolling again — measured
   slack is 325 / 257 / 116 / 94 / 32 at 932 / 844 / 667 / 640 / 568, hence the two bands and
   nothing at all below 620. 768x1024 takes the full drop too, and that alone fixes a failure
   that predates this work: 'JUTRO' measured 2.89 there against a 3.0 floor, now 6.36. */
@media (max-width: 880px) and (orientation: portrait) {
  .vb-view--home { align-items: start; }
}

/* ---- wide portrait is not a phone ----
   The mobile band is everything under 880 WIDE, and that is not all phones: 641x932 and
   849x1051 are portrait, inside the band, and nearly three times the frame area of a 390x844
   phone. Every value in this file was tuned against that phone, and applied unchanged they left
   the composition stranded at the top of a much bigger frame — measured, the ink filled 72% of
   the frame at 390x844 but only 61% at 849x1051 and 59% at 880x1100, with ALL of the slack
   piling up underneath it (175px of void at 390, 374px at 880) and the lead pinned at the same
   267px measure the whole way, a third of the width at 849.
   Above 560 the three phone decisions are unwound, each for its own reason:
     - centred, not top-anchored: the top anchor is what the handoff drew for a frame the
       composition nearly fills; when it fills 60% the same rule reads as content that fell to
       the top of the page.
     - a wider measure: 28ch is a phone line length. The cap exists so copy does not run into the
       crust, and at this width 38ch still stops well short of it.
     - the CTA's 160px offset goes back to 72: it was bought to clear a flour drift at 390x844,
       and at these sizes the plate frames differently — the CTA measures 7.7:1 here against the
       5.0 it manages on a phone.
   LAST in the file on purpose: `align-items: start` and `max-width: 28ch` are both set by blocks
   above at the same specificity, so anything that means to override them has to sit below them.
   Placed higher first, and only the margin took — the other two silently lost. */
@media (max-width: 880px) and (orientation: portrait) and (min-width: 561px) {
  /* Centred, and it is the headline size below that earns the right to be.
     At the phone's type size this was impossible: the plate has a bright stripe where a centred
     composition puts the first line, and 'JUTRO' measured 2.79 at 641x932 and 2.82 at 768x1024
     against a 3.0 floor. Worse, no size between 54px and 79px escaped it — swept, they all read
     2.35-2.86, because a smaller headline does not clear the stripe, it just sits inside it.
     The fix for the void and the fix for the contrast turned out to be the same move. Once the
     headline is large enough to SPAN the stripe rather than sit in it, every offset from +80 to
     +240 clears comfortably (worst 4.63-5.99 across five widths), which means position stops
     being load-bearing and can go back to being a composition decision.
     So: no magic offset. `center` balances the frame exactly at every size in the band, where a
     fixed margin could only ever be right for one of them — the centred offset wants 118px at
     641x932 and 184px at 845x1129. */
  .vb-view--home { align-items: center; }
  .vb-home__lead { max-width: 38ch; margin-bottom: 72px; }
  /* And the headline gets its own bound, because the phone one shrinks it as the frame grows.
     The mobile size is governed by `5.8vh` — a term that exists because height is the scarce
     axis on a phone — so at 641x932 it lands on 54px (8.4% of the width) and at 849x1051 on 61px
     (7.2%): the bigger the frame, the smaller the type reads. 7.2vh with a 6.4vw floor and a 92
     cap gives 67 / 74 / 76 / 79 across 641 / 768 / 849 / 880, which holds ~9-10% of the width
     instead of falling away. The `(100vw - 44px) / 4.8857` term is untouched and still governs:
     it is the one that keeps the widest line off the crust, and it has room to spare here — 177px
     of allowance at 849 against the 76 actually used. */
  /* 9vh, not 7.2. The void at this band is not where the composition sits — at 845x1129 it is
     already near-balanced, 266 above and 229 below — it is how little of the frame the ink
     covers, 516px of 1011. Only size fills that, and here size also BUYS contrast rather than
     costing it: bigger lines reach past the plate's bright stripe instead of sitting in it.
     Swept from the shipped position, worst headline run / scroll:
         845x1129   81px 2.83   94px 3.08   102px 3.27   110px 3.31
         768x1024   74px 3.75   94px 3.91   102px 4.23
         641x932    67px 4.49   86px 5.73   94px 6.01 but the page starts to SCROLL
     641 is the binding constraint — it is the shortest frame in the band, so it runs out of room
     before the others run out of clearance. 9vh gives 84 / 92 / 95 / 99 / 102 across
     641 / 768 / 849 / 880 / 845, which is under 641's scroll threshold and over everyone's floor.
     The `(100vw - 44px) / 4.8857` term is untouched and still the one that keeps the widest
     line off the crust; at 641 it allows 122px against the 84 actually taken. */
  .vb-h1 {
    font-size: clamp(48px, min(calc((100vw - 44px) / 4.8857), max(6.4vw, 9vh)), 108px);
  }
}

@media (max-height: 660px) and (min-width: 881px) {
  .vb-stage { height: auto; min-height: 100dvh; overflow: visible; }
  .vb-view { position: relative; inset: auto; padding-bottom: 60px; }
  .vb-view:not(.is-active) { display: none; }
}


/* ===================== per-language headline measure (EN) =====================
   Same formulas as the three .vb-h1 rules above, divisor swapped for the English lines'
   width ("GOOD MORNINGS" = 6.2186em in Anton; K = 6.03 after the .2em margin, with jitter
   room). Scoped to html[lang] so the Croatian headline keeps its own, larger size.
   The desktop pair is ALSO media-scoped: below 881px the phone-gutter rule
   (--vb-copy-edge: 22px) must win for BOTH languages, and this selector outranks it —
   unscoped, it pushed the whole EN copy block 40px right into the flour spill on phones
   (lead measured 3.17:1 at 390x844). */
@media (min-width: 881px) {
  html[lang="en"] .vb-home__grid { --vb-copy-edge: max(clamp(22px, 16vw, 232px), calc(42.8vw - 6.03 * min(10.5vh, 148px))); }
  html[lang="en"] .vb-h1 { font-size: clamp(30px, min(calc((42.8vw - var(--vb-copy-edge)) / 6.03), 10.5vh), 148px); }
}
@media (max-width: 880px) {
  html[lang="en"] .vb-h1 { font-size: clamp(30px, min(calc((100vw - 44px) / 6.03), max(4.35vw, 5.8vh)), 68px); }
}
@media (max-width: 880px) and (orientation: portrait) and (min-width: 561px) {
  html[lang="en"] .vb-h1 { font-size: clamp(40px, min(calc((100vw - 44px) / 6.03), max(6.4vw, 9vh)), 108px); }
}
