/* ==========================================================================
   Konoba Parangal ... design system implementation
   Implements brand-system.md (palette locked 2026-07-23: champagne base,
   warm-brown ink, dark-olive action colour, bronze detail).
   Fonts are loaded separately from fonts-inline.css (base64 woff2).
   ========================================================================== */

/* ---------- 1. Tokens ---------------------------------------------------- */

:root {
  /* Champagne (base / surfaces) */
  --champagne:      #F6EEDF;
  --champagne-deep: #EFE4CE;
  --champagne-hi:   #FBF6EC;

  /* Warm brown (ink + structure) */
  --brown-ink: #2E2419;
  --brown:     #3B2F23;
  --brown-60:  #6B5D4A;
  --brown-30:  #B9A98F;

  /* Dark olive (ACTION: buttons, links, active) */
  --olive-dark: #2F3A22;
  --olive-deep: #242C19;
  --olive-mid:  #3A4526;
  --on-olive:   #FBF6EC;
  --olive-wash: #E6E7D8;

  /* Bronze (detail accent only, never a button) */
  --bronze:    #6B4A2A;
  --bronze-lt: #8A6B3F;

  /* Lines */
  --line:        #E0D4BC;
  --line-strong: #C9B896;

  /* Midnight (footer / contrast blocks) */
  --midnight:        #1C1A16;
  --on-midnight:     #EFE4CE;
  --on-midnight-80:  #DCD0B6;
  --on-midnight-60:  #B7AB92;
  /* One step quieter than --on-midnight-60, and it exists for exactly one line:
     the ZIMAJ d.o.o. / OIB / seat disclosure at the bottom of the footer. Both
     numbers are measured against --midnight, not judged by eye:
       --on-midnight-60  #B7AB92  7.7:1
       --on-midnight-40  #968C76  5.2:1
     The floor for text this size is 4.5:1, so this is the quietest the line is
     allowed to be while still being a disclosure somebody can read. Do not reach
     for it to tone down anything else, and do not darken it further. */
  --on-midnight-40:  #968C76;

  /* Functional */
  --focus-ring:    #2F3A22;
  --overlay-scrim: rgba(28, 26, 22, 0.42);

  /* Type */
  /* EB Garamond, not Cormorant Garamond: Cormorant draws the lowercase caron as a
     hairline tick that floats detached above the letter, and on a Croatian menu that
     lands on nearly every line (Pršut, Paški, ovčji, rajčice, Crni rižoto) plus the
     Czech ř ě. EB Garamond joins the caron to the letter. The face is embedded as
     base64 in fonts-inline.css; Georgia is the last-resort fallback only. */
  --font-display: "EB Garamond", Georgia, serif;
  --font-body:    "Mulish", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* Spacing (8pt) */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 24px; --s-6: 32px; --s-7: 48px; --s-8: 64px;
  --s-9: 96px; --s-10: 128px; --s-11: 160px;

  /* Sticky chrome.
     --header-h is the real height of the bar, not a guess. The logo is the
     tallest thing in it: a clamp(120px, 14vw, 168px) wide box at the logo's own
     622x277 ratio (0.4453), plus the bar's 16px padding top and bottom. That
     resolves to 85px at 390 and 107px at 1280, which is what the browser
     measures. site.js overwrites this and --jump-h with the measured values on
     load and on resize; these are the no-JS floor, and they must stay close,
     because everything that has to clear the chrome reads them:
       - .menu-jump top      (it used to hardcode 64px under a 107px header and
                              sat 43px inside it),
       - html scroll-padding-top + .menu-sec scroll-margin-top (a jumped-to
                              heading used to land completely behind the bar). */
  --header-h: calc(clamp(120px, 14vw, 168px) * 0.4453 + var(--s-4) * 2);
  --jump-h: 54px;

  /* Layout */
  --maxw-content: 1200px;
  --maxw-prose: 68ch;
  --gutter: clamp(24px, 6vw, 120px);

  /* Motion */
  --dur-fast: 180ms;
  --dur-base: 420ms;
  --dur-slow: 680ms;
  --ease-out:   cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-soft:  cubic-bezier(0.33, 0.00, 0.20, 1);
  --ease-inout: cubic-bezier(0.65, 0.00, 0.35, 1);
}

/* ---------- 2. Reset ----------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Every in-page jump lands below the fixed/sticky header instead of behind
     it. The menu page stacks its own jump bar under the header and adds that on
     top through .menu-sec's scroll-margin-top (the two offsets add up). */
  scroll-padding-top: calc(var(--header-h) + var(--s-5));
}

body {
  margin: 0;
  background: var(--champagne);
  color: var(--brown-ink);
  font-family: var(--font-body);
  font-size: 1.0625rem;   /* 17px */
  line-height: 1.65;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, picture, svg { display: block; max-width: 100%; height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }
ul, ol { list-style: none; margin: 0; padding: 0; }

:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: 2px;
}

/* Skip link: visible only on keyboard focus */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 999;
  background: var(--olive-dark);
  color: var(--on-olive);
  padding: var(--s-3) var(--s-5);
  border-radius: 0 0 2px 0;
}
.skip-link:focus { left: 0; }

/* ---------- 3. Typography ------------------------------------------------ */

/* DISPLAY SIZES ARE RE-CUT FOR EB GARAMOND. Measured against the Cormorant
   Garamond they replaced, at the weights this page actually uses:

     cap height   EB 0.658em vs Cormorant 0.625em   ... +5.3%
     x-height     EB 0.439em vs Cormorant 0.399em   ... +10.2% at w500
     stem ('l')   EB@500 0.2203em vs Cormorant@600 0.2202em ... +0.05%

   Two consequences, and every display value below follows from them.

   1. SIZE: every display font-size is multiplied by 0.95, rounded to a whole
      pixel. 0.95 is the cap-height match (0.625 / 0.658), so the section
      headings ... which are set in real capitals from the printed menu, HLADNA
      PREDJELA, BRUSCHETTE, FUZI ... keep the exact presence they were drawn at.
      It leaves the x-height about 4.7% above Cormorant's, which is the part of
      the swap worth keeping: it is what makes the diacritics readable.

   2. WEIGHT: every display 600 becomes 500. This is not a lightening. Cormorant
      600 and EB 500 have the same stem to within 0.05%, so the ink on the page
      is unchanged. Holding 600 would have used EB's 500-to-600 step, which is
      +7.5% of stem where Cormorant's was +2.6%, and turned 172 dish names bold.

   Letter-spacing is untouched: the negative tracks on h1/h2 read the same on
   EB, which fits marginally tighter than Cormorant at these sizes. */

h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--brown);
  margin: 0;
  font-weight: 500;
  text-wrap: balance;
}

h1 { font-size: clamp(2.375rem, 7.6vw, 3.8125rem);   line-height: 1.05; letter-spacing: -0.5px; }
h2 { font-size: clamp(1.875rem, 4.75vw, 2.625rem);   line-height: 1.12; letter-spacing: -0.25px; }
h3 { font-size: clamp(1.5625rem, 3.325vw, 1.875rem); line-height: 1.18; }
h4 { font-size: 1.4375rem;                           line-height: 1.25; }

p { margin: 0 0 var(--s-4); max-width: var(--maxw-prose); }
p:last-child { margin-bottom: 0; }

/* Headings carry margin: 0, so an h2 and the paragraph under it were separated
   by nothing but the leading of the two lines. On a 42px display heading over
   19px body that reads as one block, and the owner called it out on the story
   section ("spacing ovaj tu moze biti malo veci"). 24px is the same step the
   eyebrow already uses above the heading, so the eyebrow / heading / paragraph
   stack now breathes evenly instead of clamping shut at the bottom. Every h2 on
   the site is followed either by a paragraph or by a list, and the section
   titles on the menu page carry their own margin-bottom, so this touches only
   the places that were actually clamped. */
h2 + p { margin-top: var(--s-5); }

.overline {
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--bronze);
  margin: 0 0 var(--s-3);
}

.lead   { font-size: 1.1875rem; line-height: 1.7; color: var(--brown-60); }
.small  { font-size: 0.875rem;  line-height: 1.5; letter-spacing: 0.2px; color: var(--brown-60); font-weight: 500; }
.fine   { font-size: 0.75rem;   line-height: 1.6; letter-spacing: 0.2px; color: var(--brown-60); }

/* Bronze hairline mark above an eyebrow */
.rule-mark { width: 32px; height: 2px; background: var(--bronze); border: 0; margin: 0 0 var(--s-5); }
.section--centered .rule-mark { margin-inline: auto; }

a { color: var(--olive-dark); text-decoration: none; }
a:not(.btn) {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  transition: background-size var(--dur-fast) var(--ease-out);
}
a:not(.btn):hover { background-size: 100% 1px; }

/* ---------- 4. Layout ---------------------------------------------------- */

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

/* 64px top and bottom at EVERY width. This has come down twice: 128px desktop
   (256px between sections) read as a series of separate pages, 96px (192px) was
   still called out as too airy through the whole page. At 64px the gap between
   two sections is 128px, which still separates them clearly because the section
   backgrounds alternate. The desktop scale comes from the wider columns and the
   bigger display sizes, which is where it belongs. Deliberately NOT a clamp, so
   nothing can shrink it further on a narrow screen. */
.section { padding-block: var(--s-8); }

.section--alt      { background: var(--champagne-deep); }
/* Two bands in a row would fuse into one tall block and the edge the band is
   there to draw would vanish. It cannot happen with today's order, but the FAQ
   between the reviews and the visit section is generated only when there are
   FAQ items to show, and if that ever empties the two bands become neighbours.
   The hairline is the page's existing separator (menu section titles use the
   same token), not a new device. */
.section--alt + .section--alt { border-top: 1px solid var(--line-strong); }
.section--centered { text-align: center; }
.section--centered p { margin-inline: auto; }
.section__head { margin-bottom: var(--s-7); }

/* ---------- 5. Buttons --------------------------------------------------- */
/* No reservations anywhere: the primary action is calling or contacting. */

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--font-body);
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: 0.4px;
  padding: 14px 28px;
  border-radius: 2px;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}

.btn--primary { background: var(--olive-dark); color: var(--on-olive); }
.btn--primary:hover { background: var(--olive-deep); }

.btn--ghost { background: transparent; border-color: var(--line-strong); color: var(--brown-ink); }
.btn--ghost:hover { border-color: var(--olive-dark); color: var(--olive-dark); }

/* On dark surfaces (hero, footer) */
.on-dark .btn--ghost { border-color: rgba(239, 228, 206, 0.45); color: var(--on-midnight); }
.on-dark .btn--ghost:hover { border-color: var(--on-midnight); }

/* ---------- 6. Header ---------------------------------------------------- */

.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  /* The header runs edge to edge and does not need the full content gutter.
     At 1024-1280 the content gutter (6vw) plus five German nav labels plus the
     language switcher left the nav crowding the switcher, so the bar gets its
     own tighter gutter. */
  padding: var(--s-4) clamp(20px, 3.5vw, 120px);
  transition: background-color var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out);
}
/* Scrim under the transparent bar. The hero's own gradient is deliberately
   light at the top, which left the nav and the language code sitting on bare
   sky: at 1280 the "DE" was effectively invisible. This carries the bar's own
   contrast and fades out once the bar solidifies. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 220%;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(28, 26, 22, 0.55) 0%,
    rgba(28, 26, 22, 0.30) 48%,
    rgba(28, 26, 22, 0) 100%
  );
  transition: opacity var(--dur-base) var(--ease-out);
}
.site-header.is-solid::before { opacity: 0; }

.site-header__logo,
.site-header__proof,
.site-header nav,
.site-header .lang { position: relative; z-index: 1; }

/* ---- The rating, stuck to the bar -------------------------------------- */
/* Google 4,5 from over 1.300 reviews, on every page, at every width, in all
   seven languages. VISIBLE TEXT ONLY: it is deliberately absent from the JSON-LD
   (Google's review-snippet policy makes self-serving rating markup ineligible),
   and that is permanent, not pending.

   The bar was already tight in German between 900 and 1280, so the mark carries
   two forms of the same fact and the CSS decides which one is affordable:

     compact (default)  ... star + number + the word "Google". Every part of
       that is language-independent, so it cannot grow when the page is Czech
       or Hungarian and it cannot push the nav into the language switcher.
     full (>= 1200px)   ... the localised phrase from the copy, "preko 1.300
       Google recenzija" and its six translations. The literal "Google" chip is
       switched OFF here so the word never prints twice.

   Either way the platform is named, so the number is never an unattributed
   rating. The mark takes the free space with margin-right:auto, which groups it
   with the logo on the left and keeps nav and switcher together on the right
   instead of letting four flex children drift apart. */
.site-header__proof {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  margin: 0 auto 0 0;
  padding-left: clamp(var(--s-3), 1.4vw, var(--s-5));
  border-left: 1px solid rgba(239, 228, 206, 0.28);
  white-space: nowrap;
  line-height: 1;
  font-size: 0.75rem;
  letter-spacing: 0.3px;
  /* Same ink as the nav links beside it, not the quieter --on-midnight-80.
     Measured over the brightest hero frame with the header's own scrim applied,
     -80 put the phrase at 3.5:1 at its worst pixel while the nav next to it sat
     at 4.4:1, and this is 12px text. Its size and the hairline that separates
     it already make it the quieter element; being dimmer as well was buying
     nothing and costing legibility. */
  color: var(--on-midnight);
}
.proof__score {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 500;
  line-height: 1;
  color: var(--on-midnight);
  font-variant-numeric: lining-nums tabular-nums;
}
/* The star is the one character on the page with no embedded font behind it
   (see ALLOWED_FALLBACK in the build's font-coverage test), so it is sized and
   coloured on its own instead of inheriting a size drawn for EB Garamond. */
.proof__star { font-size: 0.75rem; line-height: 1; opacity: 0.85; }
.proof__src {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 1.1px;
  text-transform: uppercase;
}
.proof__full { display: none; }
@media (min-width: 1200px) {
  .proof__full { display: inline; }
  .proof__src { display: none; }
}
.site-header.is-solid .site-header__proof {
  color: var(--brown-60);
  border-left-color: var(--line);
}
.site-header.is-solid .proof__score { color: var(--brown); }
.site-header.is-solid .proof__star { color: var(--bronze); opacity: 1; }

.site-header__logo { width: clamp(120px, 14vw, 168px); color: var(--on-midnight); }
/* Two stacked logo files, white over the hero and ink once the bar solidifies,
   cross-faded rather than swapped so the flip is not a blink. The dark one is
   taken out of flow, otherwise both print and the mark renders twice. */
.site-header__logo { position: relative; display: block; }
.site-header__logo img { width: 100%; height: auto; }
.site-header__logo .logo-light,
.site-header__logo .logo-dark { transition: opacity var(--dur-base) var(--ease-out); }
.site-header__logo .logo-dark { position: absolute; inset: 0; opacity: 0; }
.site-header.is-solid .logo-light { opacity: 0; }
.site-header.is-solid .logo-dark  { opacity: 1; }
.site-header nav a { color: var(--on-midnight); font-size: 0.875rem; letter-spacing: 0.4px; }
/* The toggle takes `color: inherit`, and the header itself carries no colour, so
   over the transparent bar the "HR" was rendering in body ink on the dark hero
   and read as invisible. It flips with the logo and the nav, not with the page. */
.site-header .lang__toggle { color: var(--on-midnight); }

/* Once scrolled past the hero the bar solidifies and flips to ink-on-champagne */
.site-header.is-solid {
  background: rgba(246, 238, 223, 0.94);
  backdrop-filter: saturate(1.1) blur(8px);
  box-shadow: 0 1px 0 var(--line);
}
.site-header.is-solid .site-header__logo,
.site-header.is-solid nav a,
.site-header.is-solid .lang__toggle { color: var(--brown-ink); }

/* Nav gap breathes with the viewport instead of sitting at a fixed 32px: at
   900px German ("Speisekarte", "Geschichte", "Anfahrt") ran into the language
   switcher, at 1440px+ it opens back up to the intended rhythm. */
.nav-list { display: none; gap: clamp(var(--s-4), 2.1vw, var(--s-6)); }
@media (min-width: 900px) { .nav-list { display: flex; } }

/* A hairline keeps the switcher reading as a separate control rather than a
   sixth nav item, whatever the label lengths do. */
.site-header .lang {
  padding-left: clamp(var(--s-4), 2vw, var(--s-6));
  margin-left: var(--s-1);
  border-left: 1px solid currentColor;
  line-height: 1;
  opacity: 1;
}
.site-header .lang { border-left-color: rgba(239, 228, 206, 0.32); }
.site-header.is-solid .lang { border-left-color: var(--line-strong); }

/* ---------- 7. Hero ------------------------------------------------------ */
/* Deviation from element-blueprint.md, decided by Fran 2026-07-23: the hero
   holds THREE images that slowly cross-fade instead of one still image.
   Rules that keep it from reading as a carousel: exactly 3 frames, 8s each,
   1.5s cross-fade, no arrows, no dots, text never moves, frame 1 is the LCP
   image and is the only one loaded eagerly. Honours prefers-reduced-motion. */

.hero {
  position: relative;
  /* Opaque base under the frames. Without it the page background composites
     through the cross-fading frames and the hero renders as a champagne haze
     with no photo at all (caught 2026-07-23 in the browser). It also doubles as
     the LCP placeholder colour while the first frame is still decoding. */
  background: var(--midnight);
  min-height: 100svh;
  display: grid;
  align-items: end;
  padding-block: var(--s-11) var(--s-9);
  color: var(--on-midnight);
  isolation: isolate;
  overflow: hidden;
}

/* Stacking is deliberately all non-negative. A negative z-index inside the
   hero pushed the frames behind the page background and the photo never
   painted at all (caught 2026-07-23 in the browser). frames 0, scrim 1,
   content 2. */
.hero__frames {
  position: absolute;
  inset: 0;
  z-index: 0;
  /* One explicit compositing layer for the whole frame stack. Without it the
     three cross-fading frames get promoted and demoted individually mid-
     transition, which is where the paint order goes wrong. */
  transform: translateZ(0);
}

/* The change has to READ as a change. At 8s a frame with a 1.5s dissolve, three
   near-identical views of the same terrace did not register as three pictures
   at all. Two things fix that and neither of them depends on the frames being
   different from each other, which matters because frame 3 is being replaced:
     - the fade is 700ms, quick enough to be an event rather than a drift,
     - the arriving frame settles out of a 4.5% zoom over the 5s it is up, so
       the eye is given motion to notice even when the composition repeats.
   The zoom is a transform, so it is composited and costs no layout or paint.
   site.js holds the matching 5s interval. */
.hero__frame {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 700ms var(--ease-out);
  will-change: opacity;
}
.hero__frame.is-active {
  opacity: 1;
  animation: hero-settle 5700ms var(--ease-out) both;
}
@keyframes hero-settle {
  from { transform: scale(1.045); }
  to   { transform: scale(1); }
}
/* <picture> is inline by default, so an img with height:100% inside it resolves
   against auto and the frame only fills the top of the hero. Both need to be
   block-level and full-height for object-fit: cover to do its job. */
.hero__frame picture { display: block; width: 100%; height: 100%; }
.hero__frame img { width: 100%; height: 100%; object-fit: cover; }

/* Scrim.
   The old single wash was balanced to the brightest frame and reported as
   "5.5:1 body, 4.6:1 eyebrow". Those were AVERAGES, and an average is the wrong
   statistic for a marina at midday: the bright pixels are not outliers here,
   they are masts, white hulls and water running straight through the line of
   type. Measured properly ... per frame, per rendered line box, against the
   real composited pixel under the glyphs, at the 95th percentile ... the old
   scrim was carrying:

     at 1280   eyebrow 2.1:1   h1 4.7:1   sub 7.2:1   (worst pixel 1.8 / 2.9 / 4.7)
     at  390   eyebrow 1.7:1   h1 2.4:1   sub 2.8:1   (worst pixel 1.2 / 1.8 / 1.7)

   The floor for the 13px eyebrow is 4.5:1. It was not close.

   The two widths need different shapes, because the text does. On a phone the
   copy runs nearly edge to edge, so there is no "beside the text" left to
   protect and the honest tool is a strong bottom-weighted band. On desktop the
   text is a 760px column against a 1280px frame, so the weight is pooled off
   the bottom-left corner where the words actually are, and the right half of
   the picture ... the boats, the olive tree, the lamps ... stays a photograph
   rather than mud. Measured after (worst of the three frames):

     at 1280   eyebrow 6.0:1   h1 9.0:1   sub 10.8:1  (worst pixel 5.4 / 6.5 / 6.7)
     at  390   eyebrow 4.9:1   h1 7.0:1   sub  8.7:1  (worst pixel 3.9 / 5.1 / 6.5)

   The mean luminance of the TOP 45% of the frame, which is the part that is
   pure photograph, moves from 0.19 to 0.15: the picture is a fifth deeper, not
   washed out. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    to bottom,
    rgba(28, 26, 22, 0.48) 0%,
    rgba(28, 26, 22, 0.20) 20%,
    rgba(28, 26, 22, 0.58) 44%,
    rgba(28, 26, 22, 0.86) 62%,
    /* The last stop stays under the 0.86 plateau rather than climbing past it:
       nothing below 70% of the frame is type, so more ink there would only
       delete the table and the boards the hero is standing on. */
    rgba(28, 26, 22, 0.88) 100%
  );
}
@media (min-width: 900px) {
  .hero::after {
    /* The pool reaches higher and holds its middle longer than it used to. With
       the three new hero frames the two SMALL strings (the 13px overline above
       the h1, and the ghost button's label) were the only things failing: median
       contrast around 10:1 but the worst 5% of pixels behind them down at 3,7
       against a 4,5 requirement, because a white hull or a bright wall lands
       there on one frame or another. The h1 was never at risk. Growing the pool
       up the text column fixes exactly those two without touching the right half
       of the frame, which is the part a guest is actually looking at. */
    background:
      radial-gradient(120% 84% at 8% 100%,
        rgba(20, 18, 15, 0.92) 0%,
        rgba(20, 18, 15, 0.82) 34%,
        rgba(20, 18, 15, 0.58) 64%,
        rgba(20, 18, 15, 0.00) 94%),
      linear-gradient(
        to bottom,
        rgba(28, 26, 22, 0.44) 0%,
        rgba(28, 26, 22, 0.16) 26%,
        rgba(28, 26, 22, 0.32) 52%,
        rgba(28, 26, 22, 0.60) 74%,
        rgba(28, 26, 22, 0.78) 100%
      );
  }
}

.hero__inner { position: relative; z-index: 2; max-width: 760px; }
/* The eyebrow is 13px, uppercase, tracked out: the smallest and thinnest thing
   on the hero, so it is the line the scrim has to serve first, and 4.5:1 is a
   floor rather than a target. --on-midnight-60 measured 3.1:1 and
   --on-midnight-80 measured 2.4:1 against the real worst-case pixels. It is set
   in --on-midnight now, the same ink as the body copy: at this size and weight
   the eyebrow still reads as the quieter line because of its size and its
   tracking, and "quieter" was never worth being unreadable for. */
.hero .overline { color: var(--on-midnight); }
.hero h1 { color: var(--champagne-hi); }
.hero__sub { color: var(--on-midnight); font-size: 1.1875rem; line-height: 1.7; margin-top: var(--s-4); }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-6); }

/* ---------- 8. Story ----------------------------------------------------- */

.story { display: grid; gap: var(--s-8); }
@media (min-width: 900px) {
  /* Centred, not stretched. Stretching the media to the text height composes
     the two columns more tightly, but the story photo is the konoba's own
     painted sign and filling the column crops it mid-wordmark. The client's
     signage stays whole; the ~100px height difference is ordinary editorial
     pairing, not a hole. */
  .story { grid-template-columns: 1.05fr 1fr; align-items: center; gap: var(--s-10); }
}
.story__media img { border-radius: 2px; }

/* A short named question with its answer under it, sitting full width below the
   story columns. Headings carry margin: 0 site-wide and there is no `h3 + p`
   rule the way there is for h2, so the pair would clamp shut without these two
   lines. Same 24px step the eyebrow/heading/paragraph stack already uses. */
.answer-block { margin-top: var(--s-7); }
.answer-block h3 { margin-bottom: var(--s-5); }

/* ---------- 9. Menu teaser ----------------------------------------------- */
/* The named dishes, with the food. 72 cut-outs were shot and shipped for the
   menu page and the home page was showing none of them, so the one thing a
   guest who reads none of the seven languages can actually use was missing from
   the page he lands on.
   A horizontal strip rather than a grid: it works with a thumb, it works with a
   trackpad, and it says "there is more of this" without claiming to be the
   whole menu. The plates sit on the champagne with no card, no border and no
   background of their own, which is the same treatment they get on the menu
   page and the reason they were cut out in the first place.
   The old two-column list of names with dotted leaders is gone with it: it
   carried no prices, so the leaders led nowhere. */

.teaser { position: relative; }

.teaser__track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: clamp(200px, 46vw, 260px);
  gap: var(--s-5);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--gutter);
  padding-inline: var(--gutter);
  padding-block: var(--s-2) var(--s-4);
  scrollbar-width: none;
  /* Five cards do not fill a 1280px page, so left-aligned they sat against the
     gutter with a third of the row empty on the right while the intro above them
     was centred. justify-content centres the row WHEN it fits and is ignored the
     moment the track overflows, so the scrolling case (phones, and any future
     sixth dish) still starts at the left edge where a reader expects it. */
  justify-content: safe center;
}
.teaser__track::-webkit-scrollbar { display: none; }
.teaser__track:focus-visible { outline-offset: -2px; }

.teaser-card { scroll-snap-align: start; }
/* One fixed media box for every card, with the cut-out contained inside it.
   The plates run from 1.05 to 0.57 wide-over-tall, so without this a portrait
   fish platter would stand twice as tall as a round plate and the names would
   stagger down the strip instead of sitting on one line. */
/* Block, NOT grid. As a grid the implicit row is auto-sized, so the picture's
   `height: 100%` had no definite grid area to resolve against, fell back to
   auto, and a portrait fish platter rendered 358px tall inside a 180px box and
   printed straight over the dish name under it. A plain block box gives the
   percentage the definite height it needs. */
.teaser-card__media { height: 180px; overflow: hidden; }
.teaser-card__media picture { display: block; width: 100%; height: 100%; }
/* contain, never cover: these are cut-outs on transparent ground, so cropping
   one is cropping the food itself. object-position defaults to centre. */
.teaser-card__img { width: 100%; height: 100%; object-fit: contain; }
.teaser-card__name {
  margin: var(--s-4) 0 0;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.25rem;
  line-height: 1.2;
  color: var(--brown-ink);
}
.teaser-card__desc {
  margin: var(--s-2) 0 0;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--brown-60);
}

/* The affordance, lifted wholesale from the menu page's jump bar: the strip
   hides its scrollbar, so without a hint the only clue that there is more to
   the right is a card sliced in half at the edge. The champagne-deep in the
   fades is the section's own background (.section--alt), and site.js switches
   each side off at the end it belongs to so the hint never points at nothing.
   Defaults are the no-JS truth: at rest the strip is at its left end. */
.teaser::before,
.teaser::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: var(--s-4);
  width: 64px;
  z-index: 2;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.teaser::before {
  left: 0;
  opacity: 0;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 12'%3E%3Cpath d='M6.5 1L1.5 6l5 5' fill='none' stroke='%236B5D4A' stroke-width='1.6'/%3E%3C/svg%3E")
      left 6px center / 8px 12px no-repeat,
    linear-gradient(to right, var(--champagne-deep) 40%, rgba(239, 228, 206, 0));
}
.teaser::after {
  right: 0;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 12'%3E%3Cpath d='M1.5 1L6.5 6l-5 5' fill='none' stroke='%236B5D4A' stroke-width='1.6'/%3E%3C/svg%3E")
      right 6px center / 8px 12px no-repeat,
    linear-gradient(to left, var(--champagne-deep) 40%, rgba(239, 228, 206, 0));
}
.teaser.is-scrolled::before { opacity: 1; }
.teaser.is-at-end::after { opacity: 0; }

/* Honest fresh-fish block: price by weight, ask the staff. Never a fake price. */
.catch-block {
  margin-top: var(--s-7);
  padding: var(--s-5) var(--s-6);
  background: var(--olive-wash);
  border-left: 2px solid var(--olive-dark);
  border-radius: 2px;
}
.catch-block .overline { color: var(--olive-dark); }

/* ---------- 10. Gallery (contact sheet) ----------------------------------- */
/* 32 photographs at the bottom of the page: 17 of the konoba, 15 plated dishes.
   A GRID, at every width, where this used to be a one-at-a-time swipe track of
   17. The owner asked for a gallery with more pictures, of the room and of the
   food, below everything. A track answers that request by hiding 31 of the 32
   behind a gesture; the thing he wants a guest to feel is that there is a lot,
   and only a grid says that without being asked. It costs about 3000px of
   scrolling on a phone at two columns, which is affordable precisely because he
   asked for it "ispod svega": nothing is behind it, so no one has to scroll
   past it to reach anything.
   Two columns on a phone, three from 640px, four from 1100px. */

.gallery { position: relative; }

.gallery__track {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s-2);
  margin: 0; padding: 0; list-style: none;
  max-width: var(--maxw-content);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
@media (min-width: 640px) {
  .gallery__track { grid-template-columns: repeat(3, 1fr); gap: var(--s-3); }
}
@media (min-width: 1100px) {
  .gallery__track { grid-template-columns: repeat(4, 1fr); }
}

.gallery__item {
  overflow: hidden;
  border-radius: 2px;
  background: var(--champagne-deep);
}
/* One square cell for every frame, venue and dish alike. The set is mixed:
   the room was shot landscape and every plate was shot portrait, and a grid
   that honours both orientations either staggers into ragged rows or needs
   row-spans that reflow differently at every breakpoint. A square crops both by
   the same amount, it is the shape a contact sheet has always been, and it is
   the same argument as the uniform plate box on the menu page: uniformity is
   what makes a mixed set read as one thing.
   `cover`, not `contain`: these are photographs on their own ground, not
   cut-outs, so a crop costs composition and never leaves a hole. */
.gallery__item img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  transition: transform var(--dur-base) var(--ease-soft);
}
.gallery__item:hover img { transform: scale(1.03); }

/* The square crop above takes the middle of the frame, which is correct for a
   room and for a plate. It is wrong for a tall portrait of a standing person:
   galerija-19 is the waiter in the doorway with a platter of ćevapi, his face
   is in the top fifth of the frame, and the centre square begins under his
   chin. 12% down puts the crop window over the face AND the platter, with the
   wine rack still behind him. Set from GALLERY_TOP_CROP in build_site.py, and
   it applies to nothing else. */
.gallery__item--top img { object-position: 50% 12%; }

/* The prev/next buttons scrolled a track sideways. There is no track any more,
   at any width, so there is nothing for them to scroll. The markup and the JS
   stay in place (the JS is inert against a grid, see site_js) rather than being
   torn out on deadline; this is what makes them not render. */
.gallery__nav { display: none; }

/* ---------- 11. Guest reviews -------------------------------------------- */
/* This block was three grey quotes in a row under a small number, in a section
   sized for three when the copy carried two, so a third of it was a hole. It is
   the most persuasive content on the page and it looked like a footnote.
   What it is now:
     - the score leads, as a real display figure with the review count beside it
       and a rule under the whole band, so the first thing read is the number a
       guest came looking for,
     - the reviews are cards on champagne-hi over the section's champagne-deep,
       which is the only place on the site that uses that lift, and it is used
       here because this is the part worth stopping at,
     - the layout is COLUMNS, not a fixed grid, so 3, 4 or 5 quotes of 1 to 3
       sentences all fill it. The generator writes the count into data-count and
       the column count is capped by it, because a 3-column grid holding 2
       quotes is exactly the hole this is replacing.
   There is no photograph here on purpose. See the note in build_site.py. */

.rating {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-6);
  margin-top: var(--s-6);
  padding-bottom: var(--s-6);
  border-bottom: 1px solid var(--line-strong);
}
.rating__score { display: flex; align-items: baseline; gap: var(--s-2); margin: 0; }
/* EB Garamond's Google release ships LINING figures only, and they stand
   0.644em, so this prints a 44px digit at the top of the clamp, not the 28px a
   naive reading of the size would suggest. That is deliberate here: the trust
   block was drawn around a quiet 19px number back when the section was a
   footnote, and it is the headline of the section now. */
.rating__num {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(3rem, 6.5vw, 4.25rem);
  line-height: 0.85;
  color: var(--brown);
  font-variant-numeric: lining-nums;
}
/* Sized and coloured on its own: it is the one glyph with no embedded font
   behind it, so inheriting a size drawn for EB Garamond made it clumsy. */
.rating__star { font-size: clamp(1.25rem, 2.4vw, 1.625rem); line-height: 1; color: var(--bronze); }
.rating__meta { margin: 0; display: grid; gap: var(--s-1); }
.rating__primary { font-size: 1.0625rem; font-weight: 600; color: var(--brown); }
.rating__second  { font-size: 0.875rem; color: var(--brown-60); letter-spacing: 0.2px; }

.quotes { margin-top: var(--s-7); column-gap: var(--s-5); column-count: 1; }
@media (min-width: 700px) {
  .quotes { column-count: 2; }
  .quotes[data-count="1"] { column-count: 1; }
}
@media (min-width: 1100px) {
  .quotes { column-count: 3; column-gap: var(--s-6); }
  .quotes[data-count="1"] { column-count: 1; }
  .quotes[data-count="2"] { column-count: 2; }
}

.quote {
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  margin: 0 0 var(--s-5);
  padding: var(--s-6);
  background: var(--champagne-hi);
  border: 1px solid var(--line);
  border-top: 2px solid var(--bronze);
  border-radius: 2px;
  box-shadow: 0 1px 2px rgba(28, 26, 22, 0.04),
              0 14px 30px -20px rgba(28, 26, 22, 0.40);
}
.quote__text {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.25rem;
  line-height: 1.45;
  color: var(--brown);
  /* Two of the quotes carry a real line break the guest typed (Mirna opens with
     a line of her own, Jakub closes with one). HTML would collapse it to a
     space and quietly re-set someone else's words. pre-line keeps the break and
     still collapses runs of spaces, which is what we want: one quote has four
     stray spaces mid-sentence that are stored verbatim but must not be drawn. */
  white-space: pre-line;
}
/* The guest wrote in his own language and that is what is quoted, verbatim, on
   all seven pages. The translation sits under it in body type, quieter, so it
   reads as a service to the visitor and not as a second review. */
.quote__translation {
  margin: var(--s-3) 0 0;
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--brown-60);
}
.quote__by {
  margin-top: var(--s-5);
  padding-top: var(--s-3);
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-1) var(--s-3);
}
.quote__author { font-size: 0.875rem; font-weight: 600; color: var(--brown); }
/* The platform stays on every single quote. That is the attribution the section
   is built on, and it is why the block needs no paragraph of disclaimer. */
.quote__src {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--bronze);
}

/* One quiet line, not a legal notice: the honesty stays, the paragraph goes.
   The hairline is what marks it as an aside instead of body copy. */
.quotes__note {
  margin: var(--s-4) 0 0;
  padding-left: var(--s-4);
  border-left: 1px solid var(--line-strong);
  font-size: 0.75rem;
  line-height: 1.5;
  letter-spacing: 0.2px;
  color: var(--brown-60);
  max-width: 62ch;
}

/* ---------- 12. Visit ---------------------------------------------------- */

.visit { display: grid; gap: var(--s-7); }
/* Row gap and column gap are deliberately different now. 128px BETWEEN the two
   columns is the editorial breathing room this was drawn with; 128px ABOVE the
   map, which spans both columns, was a hole with the address stranded at the
   top of it. */
@media (min-width: 900px) {
  .visit { grid-template-columns: 1fr 1fr; column-gap: var(--s-10); row-gap: var(--s-7); }
}
.visit dl { margin: 0; display: grid; gap: var(--s-5); }
.visit dt { font-size: 0.8125rem; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; color: var(--bronze); }
.visit dd { margin: var(--s-2) 0 0; font-size: 1.0625rem; }

/* ---- The map ------------------------------------------------------------ */
/* OpenStreetMap tiles with a self-hosted Leaflet, NOT a Google embed: a Maps
   iframe sets cookies the moment it loads, and this page is otherwise
   cookie-free, so embedding one would have put a consent banner on the site to
   buy a map. The library is fetched only when this block nears the viewport.

   Until then, and forever if the JS never runs, the library is blocked or the
   tile server is unreachable, the container's own children ARE the content: the
   address and the link that opens it in a maps app. Nothing here is a spinner
   or a placeholder. `is-live` is added by site.js only once Leaflet is actually
   drawing, and removed again if the tiles fail, so the fallback comes back
   rather than leaving a grey rectangle where the konoba should be. */
.map {
  /* Spans both columns of .visit, and takes its spacing from that grid's row
     gap rather than a margin of its own, so the two cannot add up. */
  grid-column: 1 / -1;
  height: clamp(280px, 34vw, 420px);
  background: var(--champagne-deep);
  border: 1px solid var(--line);
  border-radius: 2px;
  overflow: hidden;
  position: relative;
  display: grid;
  place-items: center;
}
.map__fallback { text-align: center; padding: var(--s-6); }
.map__fallback p { margin: 0 0 var(--s-2); max-width: none; }
.map__fallback p:last-child { margin-bottom: 0; margin-top: var(--s-4); }
.map.is-live { display: block; }
.map.is-live .map__fallback { display: none; }
/* Once the tiles are up the container is only ever seen THROUGH them, in the
   hairline gaps Chrome leaves where a 256px tile lands on a fractional device
   pixel. Against Leaflet's own #ddd those gaps drew a visible grid across the
   harbour. Against OSM's water tone they vanish. It is two selectors deep on
   purpose: it has to beat `.leaflet-container`, whose stylesheet is injected
   after this one, and it must NOT apply to the fallback state, which is
   champagne like the rest of the page. */
.map.is-live { background: #AAD3DF; }

/* Leaflet's own chrome, brought into the palette. Nothing structural: the
   vendored stylesheet does the layout, this only stops the controls looking
   like a different website bolted onto the page. */
.map .leaflet-control-attribution {
  background: rgba(251, 246, 236, 0.90);
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--brown-60);
  padding: 2px 8px;
}
.map .leaflet-control-attribution a { color: var(--olive-dark); }
.map .leaflet-bar a {
  background: var(--champagne-hi);
  color: var(--brown-ink);
  border-bottom-color: var(--line);
}
.map .leaflet-bar a:hover { background: var(--champagne-deep); color: var(--olive-dark); }

/* A CSS pin, so the map ships no bitmap of its own and nothing can 404. */
.map-pin { background: none; border: 0; }
.map-pin span {
  display: block;
  width: 18px;
  height: 18px;
  margin: 4px;
  border-radius: 50%;
  background: var(--olive-dark);
  border: 3px solid var(--champagne-hi);
  box-shadow: 0 2px 8px rgba(28, 26, 22, 0.45);
}

/* ---------- 12b. FAQ ----------------------------------------------------- */
/* Plain question/answer pairs, two columns on desktop like the menu. Only the
   truthful, answerable questions are ever rendered here (unknowns stay in
   copy faq.pending and out of the page), so this block never fabricates. It
   exists because answer engines reward a clear, extractable Q&A. */

.faq { display: grid; gap: var(--s-6); }
@media (min-width: 900px) {
  .faq { grid-template-columns: 1fr 1fr; column-gap: var(--s-8); row-gap: var(--s-7); }
}
.faq__item { border-top: 1px solid var(--line); padding-top: var(--s-5); }
.faq__q {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.3125rem;
  line-height: 1.3;
  color: var(--brown-ink);
}
.faq__a {
  margin: var(--s-3) 0 0;
  font-size: 1rem;
  line-height: 1.65;
  color: var(--brown-60);
  max-width: 56ch;
}

/* ---------- 13. Language switcher ---------------------------------------- */
/* Two-letter code plus chevron. Never flags: a flag names a country, not a
   language, and SI vs HR flags confuse exactly the guests we are serving. */

.lang { position: relative; }
.lang__toggle {
  display: inline-flex; align-items: center; gap: var(--s-2);
  background: transparent; border: 0; cursor: pointer;
  font-size: 0.875rem; font-weight: 600; letter-spacing: 0.4px;
  padding: var(--s-2) var(--s-1);
  color: inherit;
}
.lang__menu {
  position: absolute; right: 0; top: calc(100% + var(--s-2));
  min-width: 200px;
  background: var(--champagne-hi);
  border: 1px solid var(--line);
  border-radius: 2px;
  box-shadow: 0 8px 24px rgba(28, 26, 22, 0.10);
  padding: var(--s-2);
  display: none;
}
.lang__menu[data-open="true"] { display: block; }
.lang__menu a {
  display: block;
  padding: var(--s-3) var(--s-4);
  font-size: 0.9375rem;
  color: var(--brown-ink);
  border-radius: 2px;
}
.lang__menu a:hover { background: var(--champagne-deep); }
.lang__menu a[aria-current="true"] { color: var(--olive-dark); font-weight: 600; }
.lang__menu a[aria-current="true"]::before { content: "\2713\00a0"; }

/* ---------- 14. Footer --------------------------------------------------- */

.site-footer { background: var(--midnight); color: var(--on-midnight); padding-block: var(--s-8); }
.site-footer a { color: var(--on-midnight); }
.site-footer .fine { color: var(--on-midnight-60); }
.site-footer__grid { display: grid; gap: var(--s-7); }
@media (min-width: 900px) { .site-footer__grid { grid-template-columns: 1.4fr 1fr 1fr; gap: var(--s-8); } }

/* The legal disclosure: ZIMAJ d.o.o., its OIB and its seat. Croatian
   e-commerce law requires a service provider to publish all three, so this line
   is not optional and cannot be removed. The owner did not want it reading as
   loudly as the rest of the footer on a restaurant's homepage, which is fair:
   nobody arrives looking for an OIB. So it is set the way a disclosure is set
   in print ... last, small, quiet ... and no quieter.
     - Last: its own row under the three columns, so it is the final line of the
       page at every width, not the bottom of the third column (which on a wide
       screen is the bottom RIGHT of the footer, not the bottom of it).
     - 0.6875rem / 11px, one step under the 0.75rem `.fine` beside it.
     - --on-midnight-40, measured at 5.2:1 on --midnight against a 4.5:1 floor.
       See the token. Legibility is the constraint the quietness works inside.
   A hairline rule separates it rather than more space, because it is a
   different KIND of statement from the two lines above it and the page is
   already long. `text-wrap: balance` keeps it from breaking one word onto a
   line of its own on a phone, where it wraps to two or three lines. */
.site-footer__legal {
  /* Overrides the global 62ch measure on `p`. That measure is right for prose
     and wrong here for two reasons: the hairline above the line is drawn by
     this element's own border, so a 62ch box drew a rule that stopped dead in
     the middle of a 1200px footer and read as a rendering fault, and this line
     is an address and a tax number rather than something anyone reads a
     paragraph of. Full width also keeps it to a single line wherever there is
     room for one. */
  max-width: none;
  margin: var(--s-6) 0 0;
  padding-top: var(--s-4);
  border-top: 1px solid rgba(239, 228, 206, 0.12);
  font-size: 0.6875rem;
  line-height: 1.6;
  letter-spacing: 0.2px;
  color: var(--on-midnight-40);
  text-wrap: balance;
}

/* ---------- 15. Mobile action bar ---------------------------------------- */
/* Jelovnik | Karta | Nazovite ... always reachable with a thumb. */

.action-bar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 60;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  background: var(--champagne-hi);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
}
.action-bar a {
  padding: var(--s-4) var(--s-2);
  text-align: center;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.4px;
  color: var(--brown-ink);
}
.action-bar a + a { border-left: 1px solid var(--line); }
.action-bar a.is-primary { background: var(--olive-dark); color: var(--on-olive); }
@media (min-width: 900px) { .action-bar { display: none; } }
@media (max-width: 899px) { body { padding-bottom: 64px; } }

/* ---------- 16. Scroll reveal -------------------------------------------- */

/* Gated on the `js` class the head script sets. Without the gate, every
   section starts at opacity 0 and stays there for anyone whose JS did not run
   (blocked scripts, an IntersectionObserver that never fires, a text-mode
   crawler that executes CSS but not JS). For a build whose whole point is
   being readable by machines, a blank page is the worst possible default. */
.js .reveal { opacity: 0; transform: translateY(16px); }
.js .reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}

/* ---------- 17. Reduced motion ------------------------------------------- */
/* Everything still works, nothing moves. The hero settles on frame 1. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .js .reveal { opacity: 1; transform: none; }
  .hero__frame { transition: none; }
  .hero__frame:not(:first-child) { display: none; }
}

/* ---------- 18. Print ---------------------------------------------------- */

@media print {
  .site-header, .action-bar, .gallery__nav, .hero__frames { display: none !important; }
  /* The map is a live control with nothing to say on paper, and its edge hints
     are a scrolling affordance that cannot scroll in print. The address they
     sit beside prints either way. */
  .map { display: none !important; }
  .teaser::before, .teaser::after { display: none !important; }
  .teaser__track { overflow: visible; }
  body { background: #fff; color: #000; }
}

/* ------------------------------------------------------------------ */
/* Menu page                                                          */
/*                                                                    */
/* The printed menu sets the pattern this follows: the Croatian name   */
/* in bold, the guest's own language quietly underneath, the price on  */
/* the right, and the cut-out plate floating free on the paper. The    */
/* list has 172 rows, so the work here is rhythm and scanning, not     */
/* decoration.                                                        */
/* ------------------------------------------------------------------ */

.page-menu .site-header { position: sticky; top: 0; }

.menu-head { padding-block: var(--s-9) var(--s-6); }
.menu-head h1 { margin-bottom: var(--s-4); }

/* Jump links. Sticky UNDER the header, at the header's real height, so the
   section you want is always one tap away instead of a long scroll back to the
   top. The offset used to be a hardcoded 64px against a bar that measures 85px
   at 390 and 107px at 1280, so the header (z-index 50) sat on top of the labels
   (z-index 20) and its scrim darkened them. */
.menu-jump {
  position: sticky; top: var(--header-h); z-index: 20;
  background: var(--champagne-deep);
  border-block: 1px solid var(--line);
  padding-block: var(--s-3);
}
.menu-jump ul {
  list-style: none; margin: 0; padding: 0;
  display: flex; gap: var(--s-5);
  overflow-x: auto; scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.menu-jump ul::-webkit-scrollbar { display: none; }

/* 23 sections make a ~2400px track inside a scroller whose scrollbar is
   hidden, so two thirds of it was invisible and the label at the right edge was
   chopped mid-word with nothing to say more existed. These are the affordance:
   the labels fade out into the bar's own colour at whichever end still has more
   to see, and a chevron points the way. site.js switches each fade off once
   that end is reached (.is-scrolled / .is-at-end), so the hint never lies.
   Positioned at the gutter, which is where the scroller actually clips. */
.menu-jump .wrap { position: relative; }
.menu-jump .wrap::before,
.menu-jump .wrap::after {
  content: "";
  position: absolute; top: 0; bottom: 0; width: 56px;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.menu-jump .wrap::before {
  left: var(--gutter);
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 12'%3E%3Cpath d='M6.5 1L1.5 6l5 5' fill='none' stroke='%236B5D4A' stroke-width='1.6'/%3E%3C/svg%3E")
      left 2px center / 7px 11px no-repeat,
    linear-gradient(to right, var(--champagne-deep) 45%, rgba(239, 228, 206, 0));
}
.menu-jump .wrap::after {
  right: var(--gutter);
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 12'%3E%3Cpath d='M1.5 1L6.5 6l-5 5' fill='none' stroke='%236B5D4A' stroke-width='1.6'/%3E%3C/svg%3E")
      right 2px center / 7px 11px no-repeat,
    linear-gradient(to left, var(--champagne-deep) 45%, rgba(239, 228, 206, 0));
}
/* Defaults are the no-JS truth: at rest the strip is at its left end, so only
   the right hint shows. The classes are positive rather than negative for the
   same reason ... a missing class must never turn a hint ON. */
.menu-jump .wrap::before { opacity: 0; }
.menu-jump.is-scrolled .wrap::before { opacity: 1; }
.menu-jump.is-at-end .wrap::after { opacity: 0; }

.menu-jump a {
  white-space: nowrap;
  font-size: 0.75rem; letter-spacing: 1.4px; text-transform: uppercase;
  font-weight: 700; color: var(--brown-60); text-decoration: none;
}
.menu-jump a:hover, .menu-jump a:focus-visible { color: var(--olive-dark); }

.menu-body { padding-block: var(--s-8) var(--s-9); }

.menu-sec + .menu-sec { margin-top: var(--s-9); }
/* Adds to html's scroll-padding-top (header + 24px), so a jumped-to heading
   clears the header AND the sticky jump bar under it. Without this the whole
   navigation of a 172-row page silently failed: all 23 links in all 7 languages
   parked their heading completely behind the chrome. */
.menu-sec { scroll-margin-top: var(--jump-h); }
/* The reveal keeps its fade but drops its 16px rise HERE only. Scroll-into-view
   measures the transformed box, so a jump link aimed at a section that had not
   revealed yet landed it 16px high and the heading then slid up under the bar
   the moment the reveal fired. Measured: 8px of clearance instead of 24, and a
   visible twitch on arrival. The rise is imperceptible on a block this tall
   anyway, so nothing is lost. */
.js .menu-sec.reveal { transform: none; }
.menu-sec__title {
  font-size: clamp(1.6875rem, 3.325vw, 2.375rem);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--line-strong);
  margin-bottom: var(--s-5);
}

.dish-list { list-style: none; margin: 0; padding: 0; }

/* The row follows the PRINTED menu, which the owner photographed for us and
   then checked us against: reading left to right it is the bold Croatian name,
   the translations stacked under it, the middle column, and then the plate hard
   against the right margin. We had the photo on the left, which is the one
   thing he called out. Text, note, photo.
   The middle column WAS the price column. The owner took the amounts off the
   site on 2026-08-01; it still carries the variant ("s pomfritom" / "s
   đuvečom") and the volume ("0,75 l"), which are what the guest is choosing
   between, so the track stays and simply runs empty on most food rows. */
.dish {
  /* The plate box. Square, and the SAME square on every row, because the
     printed page runs one uniform oval down the right margin and that
     uniformity is most of what makes it read as a menu rather than a gallery.
     Square rather than the print's landscape oval because the cut-outs are not
     all ovals: they run from a 0.56 wide-over-tall fish platter to a 1.13 round
     plate, and they are being re-cut tighter right now, so the box has to be
     the one shape that suits both a wide plate and a tall platter without
     cropping either. With object-fit: contain inside it, a wide plate fills the
     width, a tall platter fills the height, and both occupy the same footprint
     down the page. Never `cover`: these are cut-outs on transparent ground, so
     cropping one crops the food. */
  --dish-media: 104px;
  display: grid;
  grid-template-columns: 1fr auto var(--dish-media);
  align-items: center;
  gap: var(--s-5);
  padding-block: var(--s-5);
  border-bottom: 1px solid var(--line);
}
.dish:last-child { border-bottom: 0; }

/* The name is always in column 1, so all 172 names sit on one vertical line
   down the page whatever the row's photo state, at every width, with no gutter
   reserved to buy it. That used to cost a 140px empty column on the left of the
   100 rows that carry no photo; now it costs nothing.
   The media column stays reserved on photo-less rows rather than collapsing:
   collapsing it would slide their notes 128px right of every note above and
   below them, and a middle column that zig-zags is the ragged edge, not the
   quiet blank margin. The printed page reserves the same margin the same way. */
.dish__body { grid-column: 1; min-width: 0; }
.dish__name {
  font-family: var(--font-display);
  font-size: 1.3125rem; font-weight: 500; line-height: 1.25;
  color: var(--brown-ink); margin: 0;
}
/* Dotted leader. (The home page teaser used to carry the same device; it is a
   strip of photographed dishes now and never showed a price, so its leaders led
   nowhere and went with it. This is the only leader left on the site.)
   With up to 650px of empty champagne between a name and the middle column, the
   eye could not track one to the other. Only drawn where there IS something to
   land on: a leader running into blank space reads as content that failed to
   render, and since the prices came off that is now most rows. Suppressed at
   phone widths, where the note sits directly under the name and has nothing to
   lead across. */
@media (min-width: 641px) {
  .dish.has-note .dish__name { display: flex; align-items: baseline; }
  .dish.has-note .dish__name::after {
    content: "";
    /* Grow into the slack, never demand any: basis 0, no min-width, and no flex
       gap. The breathing room at each end is masked out of the rule rather than
       reserved as layout, because 12px of gap is enough to make two of the long
       German names wrap a word earlier than they do today. This way the wrap
       point is byte-for-byte what it was and the leader just disappears on a
       row that has no slack to fill. */
    flex: 1 1 0;
    border-bottom: 1px dotted var(--brown-30);
    transform: translateY(-5px);
    -webkit-mask-image: linear-gradient(to right, transparent 0 var(--s-3), #000 var(--s-3),
                                        #000 calc(100% - var(--s-3)), transparent calc(100% - var(--s-3)));
    mask-image: linear-gradient(to right, transparent 0 var(--s-3), #000 var(--s-3),
                                #000 calc(100% - var(--s-3)), transparent calc(100% - var(--s-3)));
  }
}
.dish__trans {
  margin: var(--s-1) 0 0;
  font-size: 0.9375rem; line-height: 1.45; color: var(--brown-60);
}

.dish__notes {
  grid-column: 2; list-style: none; margin: 0; padding: 0;
  text-align: right; display: grid; gap: var(--s-1);
}

/* Column 3. Empty on the 100 rows with no photo, and an empty div is 0 tall, so
   a text-only row keeps its text height and only the column width is held. */
.dish__media { grid-column: 3; }
.dish__media picture {
  display: block;
  width: var(--dish-media); height: var(--dish-media);
}
/* One row (15-1, the by-the-kilo fish) carries two plates. They stack rather
   than sit side by side: halving the box would render each at 120px on a
   desktop and 44px on a phone, which is below the size at which a plate reads
   as food at all. Stacking costs that single row its own height and changes
   nothing on the other 171. */
.dish__media picture + picture { margin-top: var(--s-2); }
.dish__img { display: block; width: 100%; height: 100%; object-fit: contain; }
.dish__note { display: flex; align-items: baseline; justify-content: flex-end; gap: var(--s-2); }
.dish__variant, .dish__unit {
  font-size: 0.8125rem; color: var(--brown-60); white-space: nowrap;
}
.dish__variant { font-style: italic; }

@media (max-width: 640px) {
  /* 64px was thumbnail scale: too small to read as food, and the photo is the
     one thing a guest who cannot read Croatian can actually use. 88px keeps
     ~270px of a 390px screen for the name and the note, which is where the
     German page (the longest names) measures clean.
     Two rows, not three columns: the name and the note stack in column 1 the
     way they already did, and the plate spans both of them on the right, so the
     print's reading order survives the phone instead of being reverted. */
  .dish {
    --dish-media: 88px;
    grid-template-columns: 1fr var(--dish-media);
    column-gap: var(--s-4); row-gap: var(--s-2);
  }
  .dish__body  { grid-column: 1; grid-row: 1; }
  .dish__notes { grid-column: 1; grid-row: 2; text-align: left; justify-items: start; }
  .dish__media { grid-column: 2; grid-row: 1 / span 2; }
  .dish__note  { justify-content: flex-start; }

  /* On a phone a reserved plate column costs 104px of a 390px screen, and 100
     of the 172 rows have no plate to put in it. Those rows drop the column.
     The middle column stepping right on the photo rows is invisible here
     anyway, because at this width the note is under the name and left-aligned,
     not in a column of its own: the alignment the reservation protects on
     desktop does not exist to protect. The names stay on one vertical line
     either way. */
  .dish:not(.has-photo) { grid-template-columns: 1fr; }
  .dish:not(.has-photo) .dish__media { display: none; }
}

@media (min-width: 1100px) {
  .dish { --dish-media: 132px; gap: var(--s-6); }
}

.catch {
  margin-top: var(--s-9);
  padding: var(--s-6);
  background: var(--champagne-deep);
  border-left: 2px solid var(--bronze);
}
.catch p:last-child { margin-bottom: 0; }
.menu-note { margin-top: var(--s-6); }

@media print {
  .menu-jump, .dish__media { display: none !important; }
  .dish { grid-template-columns: 1fr auto; }
  .menu-sec { break-inside: avoid; }
}
