/* ── Site nav ──────────────────────────────────────────────────────────
   Dark bar that stays flat at rest and lifts on scroll: a permanent
   hairline keeps the edge crisp against light page content, and the
   scrolled state adds depth so the bar reads as a layer above the page
   rather than a slab painted on it. */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--ink-900);
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  transition: border-color var(--duration-base) var(--ease-in), box-shadow var(--duration-base) var(--ease-in);
}
.site-nav.is-scrolled {
  border-bottom-color: rgba(255, 255, 255, 0.12);
  box-shadow: 0 14px 32px -18px rgba(0, 0, 0, 0.85);
}
.site-nav__inner { position: relative; max-width: 1250px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; padding: var(--space-16) var(--space-24); gap: var(--space-16); }
/* Never shrink: as a plain flex item the logo <img> was being compressed
   horizontally while its height stayed pinned, so the mountain shipped
   31% narrower than the artwork on a 360px phone and 13% on a 390px one
   — a silently distorted brand mark. Holding the lockup at its natural
   width turns that into an honest overflow instead, which the --logo-h
   steps below then size away. */
.site-nav__wordmark { display: flex; align-items: center; flex: 0 0 auto; color: var(--on-dark); text-decoration: none; border-radius: var(--radius-input); }
.site-nav__wordmark:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 6px; }
/* Three-row brand lockup — mountain / wordmark / tagline — matching the
   footer's `-full-` artwork, but assembled instead of baked. The flat
   3-row raster locks the tagline at 8.4% of its own height, so a legible
   tagline there would need a ~95px image and a ~127px sticky bar. The
   2-row `-on-dark-` art already ends in the rule the footer uses to
   divide wordmark from tagline, so setting the tagline as live text
   directly under it rebuilds the same lockup with the tagline sized
   independently of the mark.

   --logo-h drives the whole lockup: the gap and the tagline's font-size
   are both derived from it, so rescaling the brand for a breakpoint means
   changing one number.

   Every number below is measured off the 3618x1394 logo source art rather
   than guessed, because the footer's baked tagline is the only ground
   truth for what this row is supposed to look like. Expressed against the
   art's own wordmark cap-height (235px) and tagline cap-height (88px):

     tagline cap   = 0.3745 x wordmark cap
     tracking      = 0.351  x tagline cap of added letter-space
     word/`|` gap  = 1.74   x tagline cap of clear space
     baseline drop = 2.00   x tagline cap from the divider rule to cap top

   The nav's `-on-dark-` art is that same lockup cropped just under the
   divider rule (wordmark cap 69 of its 320px height), so the multipliers
   are those ratios re-expressed against --logo-h, allowing for Outfit's
   own cap-height of 0.688em at weight 300:

     font-size = 0.3745 x (69/320) / 0.688 = 0.1174
     gap       = 2.00 cap below the rule, less the distance the cap top
                 already sits below a line-height:1 box  = 0.1411

   The tagline reads as *narrow* type set very wide, not as a second line
   of body copy: at 0.1174 its natural ink width lands at ~93% of the
   logo's, so the stretch-aligned column stays sized by the mark. Raise the
   font-size or drop the tracking and it overruns the wordmark instead —
   which is exactly what 0.172/0.07em did. */
:root {
  --logo-h: 52px;
  /* Derived, never typed twice: the lockup is 1 + 0.1411 (gap) + 0.1174
     (tagline) = 1.2585 units of --logo-h, and the bar adds its 16px
     padding pair plus a 1px border. Everything that has to sit clear of
     the sticky nav — the booking widget, the transport section heads,
     anchored-section scroll margins — measures from this, so stepping
     --logo-h at a breakpoint moves them all with it. */
  --nav-h: calc(var(--logo-h) * 1.2585 + 33px);
}
.site-nav__lockup {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: calc(var(--logo-h) * 0.1411);
}
.site-nav__logo { height: var(--logo-h); width: auto; display: block; }
.site-nav__tagline {
  --tagline-track: 0.241em;
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 0.813em;
  font-size: calc(var(--logo-h) * 0.1174);
  font-weight: 300;
  line-height: 1;
  letter-spacing: var(--tagline-track);
  text-transform: uppercase;
  color: var(--on-dark);
}
/* letter-spacing hangs one trailing space off the last letter, which
   `center` then counts as real width — the whole line would sit half a
   track left of the mark's centre. Take it back off. */
.site-nav__tagline > :last-child { margin-right: calc(var(--tagline-track) * -1); }
/* The artwork's separators are the same solid white as the letters, at the
   same weight — not a dimmed divider. */
.site-nav__tagline-sep { opacity: 1; }

.site-nav__links { display: none; gap: var(--space-4); }
.site-nav__links a {
  position: relative;
  color: var(--on-dark-soft);
  text-decoration: none;
  font-size: 0.9375rem;
  letter-spacing: 0.005em;
  padding: var(--space-8) var(--space-12);
  border-radius: var(--radius-input);
  transition: color var(--duration-fast) var(--ease-in);
}
/* Underline grows from the centre instead of snapping on — the same
   teal used by the CTA, so hover and primary action speak one language. */
.site-nav__links a::after {
  content: "";
  position: absolute;
  left: var(--space-12);
  right: var(--space-12);
  bottom: var(--space-4);
  height: 1.5px;
  border-radius: 1px;
  background: var(--accent-bright);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform var(--duration-base) var(--ease-in);
}
.site-nav__links a:hover,
.site-nav__links a:focus-visible,
.site-nav__links a[aria-current="page"] { color: var(--on-dark); }
.site-nav__links a:hover::after,
.site-nav__links a:focus-visible::after,
.site-nav__links a[aria-current="page"]::after { transform: scaleX(1); }
.site-nav__links a:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 0; }

.site-nav__actions { display: flex; align-items: center; gap: var(--space-12); }
/* A two-segment pill (PL | EN, active one filled) so the switch reads as
   a mode toggle, never a nav destination — issue #64. Rendered twice by
   the nav partial from the same {{langSwitchHtml}} token (the render.js
   substitution is global, so one value fills both spots) — once here in
   the desktop actions bar, once inside .site-nav__links for the mobile
   dropdown sheet — since the sticky top bar (hamburger + logo + CTA) is
   already tuned to barely fit at 320-360px and has no room for a fourth
   persistent element. Each spot's own rule below controls which
   breakpoint actually shows it, so only one is ever visible at a time. */
.site-nav__lang-switch {
  display: none;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.08);
}
.site-nav__lang-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  padding: var(--space-4) var(--space-8);
  border-radius: var(--radius-pill);
  color: var(--on-dark-muted);
  text-decoration: none;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1;
  transition: color var(--duration-fast) var(--ease-in), background-color var(--duration-fast) var(--ease-in);
}
.site-nav__lang-option.is-active { color: var(--ink-900); background: var(--accent-bright); }
.site-nav__lang-option.is-disabled { opacity: 0.4; }
a.site-nav__lang-option:hover { color: var(--on-dark); background: rgba(255, 255, 255, 0.14); }
a.site-nav__lang-option:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 2px; }
/* Phone is the secondary action to the CTA's primary — outline pill vs
   solid pill — so the pair reads as one designed control group. */
.site-nav__phone {
  display: none;
  align-items: center;
  color: var(--on-dark);
  text-decoration: none;
  font-family: "Outfit", "Outfit Fallback", sans-serif;
  font-size: 0.9375rem;
  font-weight: 500;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  padding: var(--space-8) var(--space-16);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: var(--radius-pill);
  transition: border-color var(--duration-fast) var(--ease-in), background-color var(--duration-fast) var(--ease-in);
}
.site-nav__phone:hover { border-color: var(--accent-bright); background: rgba(31, 196, 196, 0.1); }
.site-nav__phone:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 2px; }
.site-nav__cta {
  background: var(--accent-bright);
  color: var(--ink-900);
  font-family: "Outfit", "Outfit Fallback", sans-serif;
  font-weight: 600;
  font-size: 0.9375rem;
  line-height: 1.2;
  letter-spacing: 0.01em;
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
  padding: var(--space-8) var(--space-20);
  border-radius: var(--radius-pill);
  box-shadow: 0 8px 20px -10px rgba(31, 196, 196, 0.8);
  transition: filter var(--duration-fast) var(--ease-in), transform var(--duration-fast) var(--ease-in), box-shadow var(--duration-fast) var(--ease-in);
}
.site-nav__cta:hover { filter: brightness(1.08); transform: translateY(-1px); box-shadow: 0 12px 26px -10px rgba(31, 196, 196, 0.9); }
.site-nav__cta:active { transform: translateY(0); }
.site-nav__cta:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 3px; }

.site-nav__hamburger {
  background: none;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius-input);
  padding: var(--space-8);
  display: grid;
  place-content: center;
  min-width: 44px;
  min-height: 44px;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-in);
}
.site-nav__hamburger:hover { border-color: rgba(255, 255, 255, 0.3); }
.site-nav__hamburger span {
  display: block;
  width: 18px;
  height: 1.5px;
  border-radius: 1px;
  background: var(--on-dark);
  transition: transform var(--duration-base) var(--ease-in), opacity var(--duration-fast) var(--ease-in);
}
.site-nav__hamburger span + span { margin-top: 4px; }
.site-nav__hamburger[aria-expanded="true"] span:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
.site-nav__hamburger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.site-nav__hamburger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }
.site-nav__hamburger:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 2px; }

/* Mirrors the min-width: 1250px switchover below — these two boundaries
   are one breakpoint, not two independent ones. Missing this half once
   already shipped a real bug: the hamburger toggled its aria-expanded
   state and the is-open class correctly between 1024-1249px, but this
   block's old max-width: 1023px meant the CSS that actually reveals the
   dropdown never matched, so the button visibly toggled while showing
   nothing. */
@media (max-width: 1249px) {
  /* Open menu is a sheet, not a dropped list: hairline-separated rows at
     full tap height, with the teal marker anchored to the left edge. */
  .site-nav__links.is-open {
    display: flex;
    flex-direction: column;
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    background: var(--ink-900);
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    box-shadow: 0 20px 40px -20px rgba(0, 0, 0, 0.85);
    padding: var(--space-4) var(--space-24) var(--space-16);
    gap: 0;
    z-index: 31;
  }
  .site-nav__links.is-open a {
    padding: var(--space-16) 0;
    font-size: 1.0625rem;
    border-radius: 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  }
  .site-nav__links.is-open a:last-child { border-bottom: none; }
  .site-nav__links.is-open a::after { left: 0; right: auto; width: 20px; bottom: var(--space-12); transform-origin: left; }
  /* The switch is deliberately NOT one of the hairline-separated rows
     above (issue #64) — its own pill, set apart with a top rule and
     margin so it reads as a site-wide control, not a seventh destination.
     The actions-bar copy stays display:none by inheriting the base rule
     at this width; only the copy inside the open mobile sheet turns on. */
  .site-nav__links.is-open .site-nav__lang-switch {
    display: inline-flex;
    align-self: flex-start;
    margin-top: var(--space-16);
    padding-top: var(--space-16);
    border-top: 1px solid rgba(255, 255, 255, 0.07);
  }
}

/* The nav row's natural width is 373px (hamburger + lockup + CTA +
   padding + gaps), so anything under that — 360px Androids, the old
   iPhone SE — needs the brand stepped down rather than squeezed. One
   number does it; 46px puts the row at ~353px with room to spare, and
   390/414px phones keep the full-size lockup. */
@media (max-width: 380px) {
  :root { --logo-h: 46px; }
}
/* 320px can't hold the row on the brand step alone, so the gaps and the
   CTA's own padding give up their share too. Below this the bar is out of
   room honestly rather than by deforming the mark — the old code "fit"
   320px only by squashing the mountain to 55% of its width. */
@media (max-width: 360px) {
  :root { --logo-h: 44px; }
  .site-nav__inner { gap: var(--space-8); }
  .site-nav__cta { padding-left: var(--space-12); padding-right: var(--space-12); }
}

/* 1024px used to be where the mobile hamburger nav switched to the full
   desktop row, but the row itself doesn't actually fit until ~1200px
   (EN, the longer-label case — "Group & Corporate"/"Where to Stay" wrap
   to two lines, the phone pill wraps, "Book now" wraps; PL's own longer
   phone pill wraps as early as ~1150px). 1250px matches .site-nav__inner's
   own max-width above and clears both languages' measured fit points
   with margin, instead of a ~200px-wide broken zone. Paired with the
   @media (max-width: 1249px) block above the mobile-sheet .is-open
   rules live in — the two must move together. */
@media (min-width: 1250px) {
  .site-nav__hamburger { display: none; }
  .site-nav__links { display: flex; }
  .site-nav__actions .site-nav__lang-switch, .site-nav__phone { display: inline-flex; }
}

/* ── Site footer ───────────────────────────────────────────────────────
   Three-column grid so the brand lockup, the offer list and the contact
   block share the width evenly instead of leaving a void in the middle. */
.site-footer {
  background: var(--ink-900);
  color: var(--on-dark-soft);
  margin-top: var(--space-96);
  padding: var(--space-64) var(--space-24) var(--space-24);
}
.site-footer__top {
  max-width: 1250px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-32);
}
/* align-self: flex-start is what makes the lockup work, and it belongs on
   the lockup — not on its children. Below 1024px .site-footer__top is a
   column whose align-items defaults to stretch, so the brand's own box was
   being forced to the full footer width (437px on a 390px phone) with
   nothing content-driven about it. Everything inside then inherited that
   wrong width: the logo's "width: auto" got stretched to 437px while its
   height stayed pinned, and object-fit's default (fill) squashed the
   artwork non-uniformly; the tagline centred itself in 437px instead of
   under the mark. Opting the brand out here makes its box shrink-to-fit
   around its widest child — the logo — which is exactly what the nav gets
   from `flex: 0 0 auto` on .site-nav__wordmark in its row parent. The
   children can then keep the plain stretch pattern nav uses: both size to
   the logo's natural width, so the logo is undistorted and the tagline's
   own justify-content: center centres it under the mark. At >=1024px
   .site-footer__top sets align-items: flex-start itself, so this is a
   no-op there and desktop is unchanged. */
.site-footer__brand { --footer-logo-h: 76px; display: flex; flex-direction: column; align-items: stretch; align-self: flex-start; gap: var(--space-16); min-width: 0; }
.site-footer__logo { height: var(--footer-logo-h); width: auto; max-width: 100%; display: block; }
/* Tagline lives in HTML (not baked into the logo PNG like the old
   single-image lockup) so it can translate per language — same recipe
   already tuned in `.site-nav__tagline` to match this artwork's
   typography, re-expressed against the footer's own logo height. */
.site-footer__tagline {
  --tagline-track: 0.241em;
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 0.813em;
  font-size: calc(var(--footer-logo-h) * 0.1174);
  font-weight: 300;
  line-height: 1;
  letter-spacing: var(--tagline-track);
  text-transform: uppercase;
  color: var(--on-dark);
}
.site-footer__tagline > :last-child { margin-right: calc(var(--tagline-track) * -1); }
.site-footer__tagline-sep { opacity: 1; }
/* Prawne + Kontakt grouped as one unit so they can sit together at the
   footer's right edge on desktop, instead of each stretching to an
   equal-width grid column with empty space trailing its short link
   list. */
.site-footer__links { display: flex; flex-direction: column; gap: var(--space-32); }
.site-footer__col { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-12); min-width: 0; }
/* Headings are eyebrows, not headlines — they label the column and then
   get out of the way, so the links below become the visible hierarchy. */
.site-footer__heading {
  font-family: "Outfit", "Outfit Fallback", sans-serif;
  font-weight: 600;
  font-size: 0.75rem;
  line-height: 1.2;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--on-dark-muted);
  margin: 0 0 var(--space-4);
}
.site-footer__col a {
  position: relative;
  color: var(--on-dark-soft);
  text-decoration: none;
  font-size: 0.9375rem;
  line-height: 1.45;
  transition: color var(--duration-fast) var(--ease-in);
}
.site-footer__col a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1px;
  background: var(--accent-bright);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--duration-base) var(--ease-in);
}
.site-footer__col a:hover,
.site-footer__col a:focus-visible { color: var(--on-dark); }
.site-footer__col a:hover::after,
.site-footer__col a:focus-visible::after { transform: scaleX(1); }
.site-footer__col a:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 4px; border-radius: 2px; }
/* The phone number is the one thing in the footer worth finding fast. */
.site-footer__col a.site-footer__phone {
  font-size: 1.25rem;
  font-weight: 500;
  line-height: 1.2;
  color: var(--on-dark);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.site-footer__address { color: var(--on-dark-muted); font-size: 0.875rem; line-height: 1.5; margin: 0; }
.site-footer__bottom {
  max-width: 1250px;
  margin: var(--space-48) auto 0;
  padding-top: var(--space-20);
  border-top: 1px solid rgba(255, 255, 255, 0.09);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-8) var(--space-24);
}
.site-footer__legal { color: var(--on-dark-muted); margin: 0; }
.site-footer__made-with { color: var(--on-dark-muted); margin: 0; }
.site-footer__credit { color: var(--on-dark-muted); text-decoration: underline; text-underline-offset: 2px; }
.site-footer__credit:hover { color: var(--on-dark); }

@media (min-width: 640px) {
  .site-footer__links { flex-direction: row; gap: var(--space-48); }
}

@media (min-width: 1024px) {
  .site-footer__top { flex-direction: row; align-items: flex-start; justify-content: space-between; gap: var(--space-64); }
  .site-footer__links { gap: var(--space-64); }
  .site-footer__brand { --footer-logo-h: 92px; }
}

main.offer-page { max-width: 1250px; margin: 0 auto; padding: 0 var(--space-24); }
.offer-title-block { padding: var(--space-32) 0 var(--space-20); }
.offer-meta-pills { display: flex; flex-wrap: wrap; gap: var(--space-8); margin: var(--space-16) 0; }
.pill { background: var(--bg-sunken); color: var(--text-secondary); font-size: 0.875rem; padding: var(--space-4) var(--space-12); border-radius: var(--radius-pill); }

.offer-gallery {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}
.offer-gallery picture {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--radius-card);
}
.offer-gallery img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
@media (min-width: 768px) {
  .offer-gallery {
    display: grid;
    grid-template-columns: 2fr 1fr 1fr;
    grid-template-rows: minmax(160px, 1fr) minmax(160px, 1fr);
    gap: var(--space-8);
    overflow: visible;
    scroll-snap-type: none;
    margin-inline: 0;
    padding-inline: 0;
    min-height: 420px;
  }
  .offer-gallery picture {
    flex: none;
    aspect-ratio: auto;
    min-height: 0;
    height: 100%;
  }
  .offer-gallery picture:nth-child(1) { grid-column: 1; grid-row: 1 / 3; }
  .offer-gallery picture:nth-child(2) { grid-column: 2; grid-row: 1; }
  .offer-gallery picture:nth-child(3) { grid-column: 3; grid-row: 1; }
  .offer-gallery picture:nth-child(4) { grid-column: 2; grid-row: 2; }
  .offer-gallery picture:nth-child(5) { grid-column: 3; grid-row: 2; }
}

/* Zoom trigger — an invisible button filling the picture's own
   position:relative box, so clicking anywhere on a gallery photo opens
   it full-size. Doesn't touch the picture/grid rules above at all. */
.offer-gallery__zoom { position: absolute; inset: 0; width: 100%; height: 100%; background: transparent; border: 0; padding: 0; margin: 0; cursor: zoom-in; }
.offer-gallery__zoom:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: -2px; }

.gallery-lightbox-overlay { position: fixed; inset: 0; background: rgba(21, 26, 46, 0.9); z-index: 310; display: flex; align-items: center; justify-content: center; padding: var(--space-20); }
.gallery-lightbox-overlay[hidden] { display: none; }
.gallery-lightbox { position: relative; max-width: 1200px; width: 100%; display: flex; flex-direction: column; align-items: center; gap: var(--space-12); }
.gallery-lightbox__image { display: block; max-width: 100%; max-height: 80vh; width: auto; height: auto; object-fit: contain; border-radius: var(--radius-card); }
.gallery-lightbox__caption { color: #ffffff; font-size: 0.875rem; text-align: center; max-width: 60ch; }
.gallery-lightbox__close { position: absolute; top: calc(-1 * var(--space-40)); right: 0; width: var(--space-40); height: var(--space-40); border-radius: var(--radius-pill); border: 0; background: rgba(255, 255, 255, 0.15); color: #ffffff; font-size: 1.125rem; cursor: pointer; }
.gallery-lightbox__nav { position: absolute; top: 50%; transform: translateY(-50%); width: var(--space-40); height: var(--space-40); border-radius: var(--radius-pill); border: 0; background: rgba(255, 255, 255, 0.15); color: #ffffff; font-size: 1.5rem; line-height: 1; cursor: pointer; }
.gallery-lightbox__nav--prev { left: var(--space-12); }
.gallery-lightbox__nav--next { right: var(--space-12); }
.gallery-lightbox__close:hover, .gallery-lightbox__nav:hover { background: rgba(255, 255, 255, 0.3); }
.gallery-lightbox__close:focus-visible, .gallery-lightbox__nav:focus-visible { outline: 2px solid #ffffff; outline-offset: 2px; }
body.gallery-lightbox-open { overflow: hidden; }
@media (max-width: 640px) {
  .gallery-lightbox__close { top: var(--space-12); right: var(--space-12); }
}

/* Buttons — one system, two weights. Solid pill = the action we want taken,
   hairline pill = the alternatives. Same pill geometry, lift and teal
   interaction language as the nav CTA/phone pair, so the whole page speaks
   with one button voice instead of two. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-8);
  text-decoration: none;
  font-family: "Outfit", "Outfit Fallback", sans-serif;
  font-weight: 500;
  font-size: 1rem;
  line-height: 1.2;
  letter-spacing: 0.01em;
  border-radius: var(--radius-pill);
  padding: var(--space-16) var(--space-24);
  text-align: center;
  border: 1px solid transparent;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-in),
              background-color var(--duration-fast) var(--ease-in),
              border-color var(--duration-fast) var(--ease-in),
              box-shadow var(--duration-fast) var(--ease-in),
              transform var(--duration-fast) var(--ease-in);
}
.btn:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }

/* Author .btn { display: inline-flex } beats the UA [hidden] rule, so
   enterFallbackMode's hidden=true would leave booking CTAs painted.
   Same for .offer-mobile-cta { display: flex } — and for
   .booking-widget__fallback-contact { display: flex }, whose *wrapper*
   (not its .btn children) is what showConfirmation() toggles hidden
   between the confirmed and mailto-fallback confirmation states. */
.btn[hidden],
[data-booking-open][hidden],
.booking-widget__fallback-contact[hidden] {
  display: none !important;
}

.btn--primary {
  background: var(--accent-bright);
  border-color: var(--accent-bright);
  color: var(--ink-900);
  font-weight: 600;
  box-shadow: 0 8px 20px -12px rgba(31, 196, 196, 0.9);
}
.btn--primary:hover { filter: brightness(1.06); transform: translateY(-1px); box-shadow: 0 14px 28px -12px rgba(31, 196, 196, 0.95); }
.btn--primary:active { filter: none; transform: translateY(0); background: var(--accent-ink); border-color: var(--accent-ink); color: var(--on-dark); box-shadow: none; }

/* Secondary is a surface, not an outline: a card-white pill on a hairline
   border reads as a considered object, where the old heavy black rule read as
   a wireframe. Depth arrives on hover, not at rest. */
.btn--secondary {
  background: var(--bg-card);
  border-color: var(--line);
  color: var(--ink-900);
  box-shadow: 0 1px 2px rgba(21, 26, 46, 0.05);
}
.btn--secondary:hover { border-color: var(--accent-bright); color: var(--accent-ink); transform: translateY(-1px); box-shadow: 0 12px 24px -14px rgba(21, 26, 46, 0.55); }
.btn--secondary:active { transform: translateY(0); background: var(--accent-wash); border-color: var(--accent-ink); color: var(--accent-ink); box-shadow: none; }

/* Phone is the only channel that reaches a person immediately, so it carries a
   quiet accent wash — real hierarchy in the row without a second solid CTA. */
.btn--secondary[href^="tel:"] {
  background: var(--accent-wash);
  border-color: rgba(11, 117, 117, 0.3);
  color: var(--accent-ink);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.btn--secondary[href^="tel:"]:hover { background: var(--accent-wash); border-color: var(--accent-ink); }

@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
  .btn:hover, .btn:active { transform: none; }
}

.offer-mobile-cta { display: flex; width: 100%; margin: var(--space-16) 0; }
@media (min-width: 1024px) { .offer-mobile-cta { display: none; } }

.offer-intro, .offer-timeline, .offer-included, .offer-bring, .offer-pickup, .offer-reviews, .offer-faq, .offer-related, .transport-service, .group-service, .group-inquiry, .narty-calculator, .legal-section { padding: var(--space-32) 0; border-top: 1px solid var(--line); }
.offer-deck { margin-top: var(--space-8); color: var(--text-secondary); max-width: none; }
/* Centered/elevated card, matching .home-contact's treatment on the
   homepage — not part of the plain content-flow sections above (no
   border-top; the card background already separates it), bigger
   padding, everything centered instead of left-aligned. */
.offer-contact {
  background: var(--bg-sunken);
  border-radius: var(--radius-card);
  padding: var(--space-64) var(--space-32);
  margin: var(--space-32) 0 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.offer-contact .body-text { margin: var(--space-16) auto 0; }
/* Content-width, centered: three buttons sized to their own labels
   read as editorial contact detail, not stretched to equal thirds.
   Below 480px they stack full-width for thumbs. */
.offer-contact__row { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-12); margin-top: var(--space-20); }
.offer-contact__row .btn { flex: 1 1 100%; }
@media (min-width: 480px) {
  .offer-contact__row .btn { flex: 0 1 auto; }
}
.offer-included { display: grid; gap: var(--space-24); grid-template-columns: 1fr; }
@media (min-width: 768px) { .offer-included { grid-template-columns: 1fr 1fr; } }
.included-list, .excluded-list { list-style: none; padding: 0; margin: var(--space-12) 0 0; display: flex; flex-direction: column; gap: var(--space-8); }
.included-list li::before { content: "\2022"; color: var(--accent-ink); margin-right: var(--space-8); }
.excluded-list li::before { content: "\2013"; color: var(--text-muted); margin-right: var(--space-8); }

.timeline { list-style: none; display: flex; flex-direction: column; padding: 0; margin: var(--space-16) 0 0; gap: var(--space-16); }
.timeline li { position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-4); padding: var(--space-16) 0 0 var(--space-12); border-top: none; border-left: 3px solid var(--accent-ink); }
@media (min-width: 768px) {
  .timeline { flex-direction: row; flex-wrap: nowrap; overflow-x: auto; padding: 0 0 var(--space-8); gap: var(--space-24); scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch; }
  .timeline li { flex: 0 0 200px; scroll-snap-align: start; padding: var(--space-16) var(--space-12) 0; border-top: 3px solid var(--accent-ink); border-left: none; }
}
.timeline__icon { font-size: 1.375rem; line-height: 1; }
.timeline__time { font-family: "Outfit"; font-weight: 600; }
.timeline__title { line-height: 1.35; }

.chip-row { display: flex; flex-wrap: wrap; gap: var(--space-8); margin-top: var(--space-12); }
.chip { background: var(--bg-sunken); color: var(--text-secondary); font-size: 0.875rem; padding: var(--space-4) var(--space-12); border-radius: var(--radius-pill); }

.offer-pickup img, .offer-pickup iframe { border-radius: var(--radius-card); margin: var(--space-12) 0; }
.offer-pickup iframe { width: 100%; max-width: 100%; aspect-ratio: 640 / 360; border: 0; display: block; }

.offer-direct-booking {
  background: var(--ink-900);
  color: var(--on-dark-soft);
  padding: var(--space-48) var(--space-32);
  margin: var(--space-32) 0 0;
  border-radius: var(--radius-card);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.offer-direct-booking .body-text { margin: var(--space-16) auto 0; }

.faq-item { border-top: 1px solid var(--line); padding: var(--space-16) 0; }
.faq-item summary { cursor: pointer; }
.faq-item p { margin-top: var(--space-12); }

.booking-widget { background: var(--bg-card); border: 1px solid var(--line); border-top: 3px solid var(--ink-900); border-radius: var(--radius-card); padding: var(--space-20); margin: var(--space-32) 0; }
@media (max-width: 399px) {
  .booking-widget {
    margin-inline: calc(-1 * var(--space-20));
    padding: var(--space-16) var(--space-4);
    border-left: none;
    border-right: none;
    border-radius: 0;
  }
}
.booking-widget__from { margin: 0; color: var(--text-secondary); }
/* .slot-toggle-row's own padding is sized for its modal context (like the
   home-offers__header override above) — the widget rail already has its
   own padding via .booking-widget, so this drops the duplicate. */
.booking-widget__vehicle-choice.slot-toggle-row { padding: 0; margin-top: var(--space-12); }
.booking-widget__participants { margin-top: var(--space-8); }
/* The live total (#57) is the number the visitor is actually about to pay —
   a teal-tinted card gives it its own visual weight, echoing the accent
   already used by the CTA and the top price-hero rather than reading as
   another plain status line. */
.booking-widget__price-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-12);
  margin: var(--space-8) 0 0;
  padding: var(--space-12) var(--space-16);
  background: rgba(31, 196, 196, 0.08);
  border: 1px solid rgba(31, 196, 196, 0.3);
  border-radius: var(--radius-input);
}
.booking-widget__price-total-label {
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.booking-widget__price-total-amount { font-size: 1.375rem; color: var(--text-primary); }
.booking-widget__open-cta { width: 100%; margin-top: var(--space-16); }
.booking-widget__calendar-header { display: flex; align-items: center; justify-content: space-between; }
.calendar-nav-btn { min-width: 44px; min-height: 44px; border: none; background: none; font-size: 1.25rem; cursor: pointer; color: var(--text-primary); }
.calendar-nav-btn:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
/* Kept in the DOM for its aria-live announcement on arrow-key navigation
   (screen readers announce text changes there, not <select> value
   changes the same way) but visually replaced by the month/year
   selects below — standard sr-only clip pattern, not display:none
   (which would also silence the live region). */
.calendar-month-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.calendar-month-year-picker { display: flex; align-items: center; gap: var(--space-4); }
.calendar-month-select, .calendar-year-select { font: inherit; font-size: 1rem; font-weight: 500; color: var(--text-primary); cursor: pointer; border-radius: var(--radius-input); }
/* :hover is fully owned by the depth-pass rule below (#162) — this
   legacy shorthand rule stayed behind after that pass and silently won
   the background-repeat/position/size longhands anyway (a `background:`
   shorthand sets every sub-property, including ones a later, more
   specific-looking rule never mentions), tiling the chevron across the
   whole control instead of pinning one copy to the right edge. */
.calendar-month-select:focus-visible, .calendar-year-select:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.calendar-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; color: var(--text-muted); font-size: 0.8125rem; line-height: 1.3; margin-top: var(--space-4); }
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-top: var(--space-4); }
.booking-modal__step:has(.booking-widget__option-card:not([hidden])) .calendar-legend { display: none; }
.calendar-cell { min-width: 44px; min-height: 44px; border: 1px solid transparent; border-radius: var(--radius-input); background: var(--bg-card); color: var(--text-primary); font-family: "Outfit"; font-weight: 500; font-variant-numeric: tabular-nums; cursor: pointer; position: relative; }
.calendar-cell:hover:not([aria-disabled="true"]):not([aria-selected="true"]) { background: var(--accent-wash); }
.calendar-cell:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.calendar-cell[aria-disabled="true"] { background: var(--bg-sunken); color: var(--text-secondary); cursor: not-allowed; }
.calendar-cell[data-today="true"] { box-shadow: inset 0 0 0 1px var(--ink-900); }
.calendar-cell[aria-selected="true"] { background: var(--ink-900); color: var(--on-dark); }
.calendar-cell[aria-selected="true"]:focus-visible { outline-color: var(--accent-bright); }
.calendar-cell[data-empty="true"] { visibility: hidden; }
.calendar-cell .offer-dot { position: absolute; bottom: 4px; left: 50%; transform: translateX(-50%); width: 4px; height: 4px; border-radius: 50%; background: var(--ember); pointer-events: none; }
.calendar-empty { margin-top: var(--space-12); color: var(--text-secondary); }
.calendar-legend { display: flex; gap: var(--space-16); margin-top: var(--space-4); color: var(--text-secondary); }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; }
.dot--available { background: var(--bg-card); border: 1px solid var(--line); }
.dot--offer { background: var(--ember); }
.dot--unavailable { background: var(--bg-sunken); border: 1px solid var(--line); }

.booking-widget__option-card { margin-top: var(--space-4); border: 1px solid var(--ink-900); border-radius: var(--radius-card); overflow: hidden; }
.booking-widget__option-card[hidden] { display: none; }
.slot-toggle-row { display: flex; flex-wrap: wrap; gap: var(--space-8); padding: var(--space-4) var(--space-12); }
.slot-toggle { min-width: 44px; min-height: 44px; padding: var(--space-8) var(--space-16); border: 1px solid var(--line-input); border-radius: var(--radius-input); background: var(--bg-card); color: var(--text-primary); font-family: "Outfit"; font-weight: 500; cursor: pointer; }
.slot-toggle:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.slot-toggle[aria-pressed="true"] { background: var(--ink-900); color: var(--on-dark); border-color: var(--ink-900); }
.slot-toggle[aria-pressed="true"]:focus-visible { outline-color: var(--accent-bright); }
.slot-toggle[aria-disabled="true"] { background: var(--bg-sunken); color: var(--text-secondary); cursor: not-allowed; }
.slot-toggle__sub { display: block; font-family: "Outfit"; font-weight: 400; font-size: 0.75rem; }

.stepper-row { display: flex; align-items: center; justify-content: space-between; padding: var(--space-12) var(--space-16); border-top: 1px solid var(--line); }
.stepper { display: flex; align-items: center; gap: var(--space-8); }
.stepper__btn { min-width: 44px; min-height: 44px; border: 1px solid var(--line-input); border-radius: 50%; background: var(--bg-card); color: var(--text-primary); font-size: 1.25rem; cursor: pointer; }
.stepper__btn:disabled { color: var(--disabled-text); background: var(--disabled-bg); border-color: var(--disabled-bg); cursor: not-allowed; }
.stepper__btn:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.stepper__count { min-width: 2ch; text-align: center; font-family: "Outfit"; font-weight: 500; font-variant-numeric: tabular-nums; color: var(--text-primary); }
/* Airport shuttle calculator: same field rhythm as the standard
   booking widget's .stepper-row, but the toggle groups need their
   label stacked above the row (a toggle group is wider than a
   stepper), not side-by-side. */
.lotnisko-calculator__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  padding: var(--space-12) var(--space-16);
  border-top: 1px solid var(--line);
}
.lotnisko-calculator__field .text-input { width: 100%; }

/* The two calculator dropdowns are the only <select>s that carry the page's
   headline decision (which airport, one-way or return), so they get a
   drawn control instead of the OS widget: native chrome stripped, one
   hairline chevron of our own, and the same rest → hover → press language
   as .btn--secondary (wireframe at rest, depth and teal on hover). Scoped
   with the .lotnisko-calculator ancestor on purpose — it outranks
   .text-input's own `background` shorthand regardless of source order, and
   it keeps every other .text-input on the site (pickup, name, phone, email,
   note, the integracja category select) exactly as it was. */
.lotnisko-calculator .lotnisko-select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  cursor: pointer;
  font-weight: 500;
  line-height: 1.2;
  /* Right padding clears the 12px chevron plus its 16px gutter, so the
     longest label ("Poprad Airport (Slovakia)") never runs under it. */
  padding: var(--space-8) var(--space-40) var(--space-8) var(--space-12);
  background-color: var(--bg-card);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.75 6 6.25 11 1.75' stroke='%234a5268' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-16) center;
  background-size: 12px 8px;
  box-shadow: 0 1px 2px rgba(21, 26, 46, 0.05);
  transition: border-color var(--duration-fast) var(--ease-in),
              box-shadow var(--duration-fast) var(--ease-in),
              background-position var(--duration-fast) var(--ease-in),
              transform var(--duration-fast) var(--ease-in);
}
/* Edge/IE legacy caret. */
.lotnisko-calculator .lotnisko-select::-ms-expand { display: none; }
/* Hover: teal hairline, the shadow bloom off .btn--secondary, and the
   chevron picks up --accent-ink so the affordance reads before the click. */
.lotnisko-calculator .lotnisko-select:hover {
  border-color: var(--accent-bright);
  transform: translateY(-1px);
  box-shadow: 0 12px 24px -14px rgba(21, 26, 46, 0.55);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.75 6 6.25 11 1.75' stroke='%230b7575' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
/* Focus doubles as the open state — the border commits to --accent-ink and
   the chevron drops 1px, the small "it's open" tell the OS caret gave for
   free. The focus ring itself stays the system's .text-input:focus-visible. */
.lotnisko-calculator .lotnisko-select:focus {
  border-color: var(--accent-ink);
  background-position: right var(--space-16) top calc(50% + 1px);
  box-shadow: 0 10px 22px -14px rgba(11, 117, 117, 0.65);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.75 6 6.25 11 1.75' stroke='%230b7575' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.lotnisko-calculator .lotnisko-select:active {
  transform: translateY(0);
  background-color: var(--accent-wash);
  border-color: var(--accent-ink);
  box-shadow: none;
}
@media (prefers-reduced-motion: reduce) {
  .lotnisko-calculator .lotnisko-select { transition: none; }
  .lotnisko-calculator .lotnisko-select:hover,
  .lotnisko-calculator .lotnisko-select:active { transform: none; }
  .lotnisko-calculator .lotnisko-select:focus { background-position: right var(--space-16) center; }
}
.pax-limit-note { padding: var(--space-8) var(--space-16); color: var(--accent-ink); }
.booking-widget .price-inline, .booking-widget .price-hero, .summary-lines .tnum { color: var(--text-primary); }

.booking-widget__pickup, .booking-widget__contact { padding: var(--space-4); border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: var(--space-4); }
.booking-widget__pickup label, .booking-widget__contact label { font-weight: 500; }
.text-input { min-height: 44px; font-size: 1rem; padding: var(--space-8) var(--space-12); border: 1px solid var(--line-input); border-radius: var(--radius-input); background: var(--bg-card); color: var(--text-primary); font-family: "Outfit"; }
.text-input:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.text-input[aria-invalid="true"] { border-color: var(--danger); }
.field-error { color: var(--danger); margin: 0; }
.field-error[hidden] { display: none; }
.pickup-suggestions { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: var(--radius-input); max-height: 200px; overflow-y: auto; position: relative; z-index: 100; background: var(--bg-card); }
.pickup-suggestions li { padding: var(--space-8) var(--space-12); cursor: pointer; }
.pickup-suggestions li:hover, .pickup-suggestions li[aria-selected="true"] { background: var(--accent-wash); }

.booking-widget__terms { padding: var(--space-4); border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: var(--space-4); }
.checkbox-row { display: flex; align-items: flex-start; gap: var(--space-8); cursor: pointer; }
.checkbox-row input[type="checkbox"] { margin-top: 2px; flex-shrink: 0; }
.checkbox-row a { color: inherit; text-decoration: underline; }

.booking-widget__summary { padding: var(--space-4); border-top: 1px solid var(--line); }
.summary-lines div { display: flex; justify-content: space-between; padding: var(--space-4) 0; color: var(--text-secondary); }
.summary-total { display: flex; justify-content: space-between; align-items: baseline; margin-top: var(--space-4); padding-top: var(--space-8); border-top: 1px solid var(--line); }
.booking-modal .summary-total-amount { font-size: 1.25rem; }

.booking-widget__cta, .booking-modal__next { width: 100%; margin-top: var(--space-16); }
.booking-widget__cta:disabled, .booking-widget__cta[aria-disabled="true"],
.booking-modal__next:disabled, .booking-modal__next[aria-disabled="true"] { background: var(--disabled-bg); border-color: var(--disabled-bg); color: var(--disabled-text); cursor: not-allowed; box-shadow: none; }
.booking-widget__cta:disabled:hover, .booking-widget__cta[aria-disabled="true"]:hover,
.booking-modal__next:disabled:hover, .booking-modal__next[aria-disabled="true"]:hover { filter: none; transform: none; box-shadow: none; }
.booking-widget__cta:focus-visible, .booking-modal__next:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
/* .btn--secondary is defined with the rest of the button system above. */
.booking-widget__fallback-contact { display: flex; flex-wrap: wrap; gap: var(--space-8); margin-top: var(--space-16); }
.booking-widget__fallback-contact .btn { flex: 1 1 130px; }
.booking-widget__confirmation { padding: var(--space-16); }
.booking-widget__confirmation[hidden] { display: none; }

.booking-modal-overlay { position: fixed; inset: 0; background: rgba(21, 26, 46, 0.55); z-index: 300; display: flex; align-items: center; justify-content: center; padding: var(--space-20); }
.booking-modal-overlay[hidden] { display: none; }
/* Sized to match .booking-modal exactly (same max-width/width) so the two
   boxes coincide — this is what .booking-modal__close is now positioned
   against, instead of the full-viewport overlay (#180: on wide screens
   the overlay-relative button ended up far from the centered, narrower
   card). */
.booking-modal-frame { position: relative; max-width: 560px; width: 100%; }
.booking-modal { position: relative; width: 100%; background: var(--bg-card); border-radius: var(--radius-card); max-height: 94vh; overflow-y: auto; padding: var(--space-8) var(--space-12); box-shadow: 0 20px 60px rgba(21, 26, 46, 0.35); }
.booking-widget__contact-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-8); }
/* min-width: 0 overrides the grid item default (min-width: auto), which
   otherwise refuses to shrink a track below its content's min-content
   width — an <input> with no intrinsic width constraint of its own was
   forcing each 1fr column to ~238px regardless of the actual available
   space, so the second column (phone/email, flight-number/arrival-time)
   visually overflowed the card on any viewport narrower than that. */
.booking-widget__contact-field { display: flex; flex-direction: column; gap: var(--space-8); min-width: 0; }
@media (max-width: 479px) {
  .booking-widget__contact-row { grid-template-columns: 1fr; }
}
/* Positioned relative to .booking-modal-frame, not .booking-modal itself —
   the modal card has overflow-y: auto for its own scrolling content,
   which also clips the x-axis per the CSS overflow computed-value rule
   (setting one axis non-visible forces the other to behave as non-visible
   too), so a button positioned outside the card's box via a negative
   offset would have been clipped if it lived inside .booking-modal.
   Living in the frame instead puts it visibly outside the white card,
   over the dark backdrop, immune to the card's own scroll/clipping — and
   because the frame is sized to match the card exactly, the button stays
   right at the card's corner regardless of viewport width. */
/* A solid circular background (not "background: none") because the
   overlay behind it is only a 55%-opacity tint over whatever page
   content sits underneath — contrast against that isn't guaranteed, so
   the button needs its own guaranteed-contrast surface regardless of
   what's behind it. */
/* z-index: 3, not 1 — now that .booking-modal-frame keeps the button
   pinned to the card's own top-right corner at every viewport width, it
   always overlaps the sticky step header (z-index: 2, and .booking-modal
   never establishes its own stacking context, so the two compete
   directly). At z-index: 1 the header's opaque navy painted over the
   button's ✕ glyph on every open — only the outer ring showed. */
.booking-modal__close { position: absolute; top: var(--space-20); right: var(--space-20); display: flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; border: none; border-radius: var(--radius-pill); background: var(--ink-900); font-size: 1.1rem; cursor: pointer; color: #fff; z-index: 3; }
.booking-modal__close:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.booking-modal__step[hidden] { display: none; }
.booking-modal__step-header { display: flex; align-items: center; gap: var(--space-4); }
.booking-modal__back { border: none; background: none; color: var(--accent-ink); font-weight: 500; cursor: pointer; min-width: 44px; min-height: 44px; font-size: 1.25rem; }
.booking-modal__back:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.booking-modal__recap { color: var(--text-secondary); margin-top: calc(-1 * var(--space-8)); }
.booking-modal .booking-widget__calendar { margin-top: var(--space-4); }
.booking-modal .booking-modal__next { margin-top: var(--space-4); padding: var(--space-12) var(--space-24); }
/* The full-bleed hairline stack this used to produce is replaced by the
   grouped field cards in the "booking modal — depth pass (#162)" block
   below; the groups now carry their own padding. */
.booking-modal .booking-widget__cta { padding: var(--space-12) var(--space-24); margin-top: var(--space-8); }
/* Airport-transfer pop-up (#30): the recap sits straight under the <h3>
   here (no step-header to hug), so drop the shared negative margin-top
   that would otherwise pull it onto the title, and give the fields
   below some room. */
#lotnisko-modal-form .booking-modal__recap { margin-top: var(--space-8); margin-bottom: var(--space-16); }
/* type="time" carries its own control chrome — keep it the same width
   as the other .text-input fields it shares a row with. */
#lotnisko-modal-form input[type="time"] { width: 100%; }

@media (max-width: 599px) {
  .booking-modal-overlay { align-items: flex-end; padding: 0; }
  .booking-modal-frame { max-width: none; }
  .booking-modal { max-height: 88vh; border-radius: var(--radius-sheet) var(--radius-sheet) 0 0; }
}

/* ══════════════════════════════════════════════════════════════════════
   BOOKING MODAL — DEPTH PASS (#162)

   Both modal steps read flat because nothing in them was a *surface*:
   the modal was one white sheet, every group was separated only by a
   1px hairline running edge to edge, and the calendar's available cells
   were white-on-white (a transparent border was their entire visual
   identity, so "available" and "the card behind it" looked the same).
   The fix is the relationship the rest of the site already uses — a
   page-coloured ground with real white cards on it (.offer-card,
   .testimonial-card, .booking-widget) — applied inside the modal, plus
   a recessed well for the calendar grid so availability becomes
   raised-vs-sunken rather than one hairline vs another.

   Colour rules honoured throughout (spec "The rule that matters"):
   --accent-bright appears only as a fill under --ink-900 text (the
   primary CTA, untouched here) or as a foreground on --ink-900 (the
   focus rings inside the navy header band). Every accent foreground on
   a light surface is --accent-ink. Prices stay --text-primary. --ember
   stays decorative (the offer dot), never an interactive state.
   ══════════════════════════════════════════════════════════════════════ */

/* --- Shell ------------------------------------------------------------
   --modal-pad is a single source of truth: the header band cancels it
   with a negative margin to go full-bleed, and the mobile calendar
   breaks out of it the same way, so the two can never drift apart. */
.booking-modal {
  --modal-pad: var(--space-24);
  background: var(--bg-page);
  padding: 0 var(--modal-pad) var(--modal-pad);
  border-radius: var(--radius-sheet);
  box-shadow: 0 2px 6px rgba(21, 26, 46, 0.08), 0 32px 80px -24px rgba(21, 26, 46, 0.5);
  /* A platform scrollbar with its own opaque track cuts a pale 15px strip
     down the right edge, straight through the header band's rounded
     corner. A thin thumb on a transparent track keeps the card's edge
     intact on the platforms that draw persistent scrollbars. */
  scrollbar-width: thin;
  scrollbar-color: var(--line-input) transparent;
}
.booking-modal::-webkit-scrollbar { width: 10px; }
.booking-modal::-webkit-scrollbar-track { background: transparent; }
.booking-modal::-webkit-scrollbar-thumb {
  background: var(--line-input);
  background-clip: content-box;
  border: 3px solid transparent;
  border-radius: var(--radius-pill);
}

/* --- Header band ------------------------------------------------------
   The step title becomes the modal's own navy chrome — same voice as
   the nav and footer — and stays put while the (long) details step
   scrolls under it. Right padding clears the close button, which is
   positioned against the overlay and can land on top of the card. */
.booking-modal__step > .h3,
.booking-modal__step-header {
  position: sticky;
  top: 0;
  z-index: 2;
  margin: 0 calc(-1 * var(--modal-pad)) var(--space-12);
  padding: var(--space-12) calc(var(--modal-pad) + var(--space-40)) var(--space-12) var(--modal-pad);
  background: var(--ink-900);
  color: var(--on-dark);
  border-radius: var(--radius-sheet) var(--radius-sheet) 0 0;
}
/* The back button already supplies the left inset in this variant. */
.booking-modal__step-header { padding-left: var(--space-8); }
/* --accent-ink measures 3.13:1 on --ink-900 — too faint to be either
   text or a focus ring on the band, so both switch to the dark-surface
   tokens. */
.booking-modal__step-header .booking-modal__back { color: var(--on-dark); }
.booking-modal__step-header .booking-modal__back:hover { color: var(--accent-bright); }
.booking-modal__step-header .booking-modal__back:focus-visible,
.booking-modal__close:focus-visible { outline-color: var(--accent-bright); }
/* The close button now sits over navy as often as over the scrim — a
   white hairline keeps its edge readable against both. */
.booking-modal__close { box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.45), 0 8px 20px -8px rgba(21, 26, 46, 0.7); }

/* The chosen date/slot is the one fact the details step is built on —
   it gets its own strip instead of sitting as grey caption text. */
#booking-modal-recap {
  margin: calc(-1 * var(--space-8)) 0 var(--space-16);
  padding: var(--space-8) var(--space-12);
  background: var(--accent-wash);
  border-radius: var(--radius-input);
  color: var(--text-primary);
  font-weight: 500;
}

/* --- Calendar card ---------------------------------------------------- */
.booking-modal .booking-widget__calendar {
  margin-top: 0;
  padding: var(--space-12);
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  box-shadow: 0 1px 2px rgba(21, 26, 46, 0.04), 0 14px 30px -22px rgba(21, 26, 46, 0.35);
}
.booking-modal .booking-widget__calendar-header { gap: var(--space-4); }
/* Month arrows read as controls now, not as two loose glyphs. */
.booking-modal .calendar-nav-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--bg-card);
  box-shadow: 0 1px 2px rgba(21, 26, 46, 0.05);
  transition: border-color var(--duration-fast) var(--ease-in),
              color var(--duration-fast) var(--ease-in),
              background-color var(--duration-fast) var(--ease-in),
              box-shadow var(--duration-fast) var(--ease-in),
              transform var(--duration-fast) var(--ease-in);
}
.booking-modal .calendar-nav-btn:hover {
  border-color: var(--accent-ink);
  color: var(--accent-ink);
  background: var(--accent-wash);
  transform: translateY(-1px);
  box-shadow: 0 10px 20px -12px rgba(11, 117, 117, 0.7);
}
.booking-modal .calendar-nav-btn:active { transform: translateY(0); box-shadow: none; }

/* Month/year: native chrome stripped for a drawn control, same recipe
   already proven on .lotnisko-select, but with --accent-ink (not
   --accent-bright) as the light-surface hover border. */
.calendar-month-select,
.calendar-year-select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  min-height: 44px;
  padding: var(--space-8) var(--space-32) var(--space-8) var(--space-12);
  border: 1px solid var(--line);
  background-color: var(--bg-card);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.75 6 6.25 11 1.75' stroke='%234a5268' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-12) center;
  background-size: 12px 8px;
  box-shadow: 0 1px 2px rgba(21, 26, 46, 0.05);
  transition: border-color var(--duration-fast) var(--ease-in),
              background-color var(--duration-fast) var(--ease-in),
              box-shadow var(--duration-fast) var(--ease-in);
}
.calendar-month-select::-ms-expand,
.calendar-year-select::-ms-expand { display: none; }
/* background-color, not the `background` shorthand the old hover used —
   the shorthand would wipe the chevron out. */
.calendar-month-select:hover,
.calendar-year-select:hover {
  background-color: var(--accent-wash);
  border-color: var(--accent-ink);
  box-shadow: 0 10px 20px -12px rgba(11, 117, 117, 0.7);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.75 6 6.25 11 1.75' stroke='%230b7575' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.calendar-month-select:focus,
.calendar-year-select:focus { border-color: var(--accent-ink); }

/* --- Calendar grid: a recessed well ----------------------------------
   This is the actual fix for "no distinction beyond a thin border".
   Unavailable days sink into the sunken ground and lose their border
   entirely; available days are raised white chips; the selected day is
   a solid navy chip. Three genuinely different surfaces, not three
   shades of grey. --bg-sunken's text rule allows --text-primary /
   --text-secondary only, which is what the cells and the weekday header
   use here (--text-muted would fail at 4.36:1). */
.booking-modal .calendar-weekdays {
  gap: var(--space-4);
  padding-inline: var(--space-8);
  margin-top: var(--space-12);
  color: var(--text-secondary);
  font-weight: 500;
}
.booking-modal .calendar-grid {
  gap: var(--space-4);
  margin-top: var(--space-8);
  padding: var(--space-8);
  background: var(--bg-sunken);
  border-radius: var(--radius-input);
  box-shadow: inset 0 1px 3px rgba(21, 26, 46, 0.09);
}
.booking-modal .calendar-cell {
  border-color: var(--line);
  box-shadow: 0 1px 2px rgba(21, 26, 46, 0.08);
  transition: background-color var(--duration-fast) var(--ease-in),
              border-color var(--duration-fast) var(--ease-in),
              box-shadow var(--duration-fast) var(--ease-in),
              transform var(--duration-fast) var(--ease-in);
}
.booking-modal .calendar-cell:hover:not([aria-disabled="true"]):not([aria-selected="true"]) {
  background: var(--accent-wash);
  border-color: var(--accent-ink);
  transform: translateY(-1px);
  box-shadow: 0 10px 18px -10px rgba(11, 117, 117, 0.75);
}
.booking-modal .calendar-cell[aria-disabled="true"] {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  font-weight: 400;
}
/* Today reads as a ring rather than the old navy inset, which was the
   same colour as the selected fill and blurred the two states. */
.booking-modal .calendar-cell[data-today="true"] {
  box-shadow: inset 0 0 0 2px var(--accent-ink), 0 1px 2px rgba(21, 26, 46, 0.08);
}
.booking-modal .calendar-cell[aria-selected="true"] {
  border-color: var(--ink-900);
  font-weight: 600;
  box-shadow: 0 8px 16px -8px rgba(21, 26, 46, 0.65);
}
.booking-modal .calendar-cell[data-empty="true"] { background: transparent; border-color: transparent; box-shadow: none; }
/* Decorative only (spec: the ember discount marker never becomes a
   cell's fill, border or state) — just big enough to see, with a ring
   in whatever the cell's own surface is so it reads on both. */
.booking-modal .calendar-cell .offer-dot { width: 6px; height: 6px; bottom: 5px; box-shadow: 0 0 0 2px var(--bg-card); }
.booking-modal .calendar-cell[aria-selected="true"] .offer-dot { box-shadow: 0 0 0 2px var(--ink-900); }
.booking-modal .calendar-legend { flex-wrap: wrap; gap: var(--space-12); margin-top: var(--space-12); }
.booking-modal .calendar-empty { margin-top: var(--space-8); }

/* --- Slot picker ------------------------------------------------------
   Was a plain bordered box; now the site's standard card recipe (1px
   --line + a 3px --ink-900 top edge, per the premium bar) so the second
   decision on this step carries as much weight as the first. */
.booking-modal .booking-widget__option-card {
  margin-top: var(--space-16);
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-top: 3px solid var(--ink-900);
  box-shadow: 0 1px 2px rgba(21, 26, 46, 0.04), 0 14px 30px -22px rgba(21, 26, 46, 0.35);
}
/* Same raised-chip-in-a-recessed-well language as the calendar grid, so
   the two decisions on this step read as one system. */
.booking-modal .booking-widget__option-card > .slot-toggle-row {
  margin: var(--space-12);
  padding: var(--space-8);
  gap: var(--space-8);
  background: var(--bg-sunken);
  border-radius: var(--radius-input);
  box-shadow: inset 0 1px 3px rgba(21, 26, 46, 0.09);
}
.booking-modal .slot-toggle {
  flex: 1 1 140px;
  padding: var(--space-12) var(--space-16);
  border-color: var(--line);
  box-shadow: 0 1px 2px rgba(21, 26, 46, 0.06);
  transition: background-color var(--duration-fast) var(--ease-in),
              border-color var(--duration-fast) var(--ease-in),
              box-shadow var(--duration-fast) var(--ease-in),
              transform var(--duration-fast) var(--ease-in);
}
.booking-modal .slot-toggle:hover:not([aria-disabled="true"]):not([aria-pressed="true"]) {
  background: var(--accent-wash);
  border-color: var(--accent-ink);
  transform: translateY(-1px);
  box-shadow: 0 12px 22px -12px rgba(11, 117, 117, 0.75);
}
.booking-modal .slot-toggle .tnum { display: block; font-size: 1.125rem; font-weight: 600; }
.booking-modal .slot-toggle__sub { margin-top: var(--space-4); font-size: 0.8125rem; color: var(--text-secondary); }
.booking-modal .slot-toggle[aria-pressed="true"] { box-shadow: 0 10px 20px -8px rgba(21, 26, 46, 0.6); }
.booking-modal .slot-toggle[aria-pressed="true"] .slot-toggle__sub { color: var(--on-dark-soft); }
.booking-modal .slot-toggle[aria-disabled="true"] { border-color: transparent; box-shadow: none; }
.booking-modal .booking-modal__next { margin-top: var(--space-20); }

/* --- Details step: field groups ---------------------------------------
   Each logical group is a card on the modal's ground, so pickup / name /
   phone+email / promo / note / terms / total are seven read-at-a-glance
   blocks instead of one undifferentiated stack of hairlines. Inputs
   keep a white parent on purpose: --line-input is only legal at 3:1 on
   --bg-card, not on --bg-page. */
.booking-modal .booking-widget__pickup,
.booking-modal .booking-widget__contact,
.booking-modal .booking-widget__summary {
  padding: var(--space-8) var(--space-12);
  margin-bottom: var(--space-8);
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  box-shadow: 0 1px 2px rgba(21, 26, 46, 0.04);
}
/* Promo/note/terms are secondary and optional — the populated "Your
   details" step didn't fit the viewport without scrolling once all six
   groups got the full card treatment (measured 209px of real overflow
   at a 907px-tall window, #162 follow-up). Keeping the strongest
   treatment on the two decision-heavy groups (who/where) and the money
   card, and reverting these three to a light hairline instead of a
   full padded/shadowed card, closes the gap without giving up the
   grouping/rhythm fix those three still get. */
.booking-modal .booking-widget__promo,
.booking-modal .booking-widget__terms {
  padding: var(--space-8) 0 0;
  border-top: 1px solid var(--line);
}
.booking-modal .booking-widget__pickup,
.booking-modal .booking-widget__contact,
.booking-modal .booking-widget__promo,
.booking-modal .booking-widget__terms { gap: var(--space-8); }
.booking-modal .booking-widget__promo { display: flex; flex-direction: column; }
.booking-modal .booking-widget__promo label { font-weight: 500; }
.booking-modal .booking-widget__promo .text-input { width: 100%; }
.booking-modal .booking-widget__contact-row { gap: var(--space-12); }
/* Bigger hit area for the one control that gates the whole CTA; white
   on --accent-ink is 5.5:1, so the checked state stays legal. */
.booking-modal .checkbox-row input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent-ink); }
.booking-modal .checkbox-row a { color: var(--accent-ink); }

/* The total is the money moment — it gets the card recipe's navy top
   edge and real type size. Prices stay --text-primary (colour means
   interactive). */
.booking-modal .booking-widget__summary { border-top: 3px solid var(--ink-900); }
.booking-modal .summary-lines div { padding: var(--space-4) 0; }
.booking-modal .summary-total { margin-top: var(--space-8); padding-top: var(--space-12); }
.booking-modal .summary-total-amount { font-size: 1.5rem; }

/* Validation: a hairline border-colour swap was the entire error signal.
   Now the field itself gets a danger halo and the message a marker, so
   an error is visible without reading. White on --danger is 6.5:1. */
.booking-modal .text-input[aria-invalid="true"] { box-shadow: 0 0 0 3px rgba(179, 38, 30, 0.14); }
/* #contact-reach-error and the static caption right after it say the
   identical sentence ("Podaj telefon lub e-mail." / "Enter a phone
   number or email.") — the caption is the neutral always-there hint,
   the error is the same text with a marker badge once it's shown, so
   showing both stacks one sentence twice. Hide the plain caption once
   the error is visible; nothing is lost, since the error already says
   the same thing more visibly. */
#contact-reach-error:not([hidden]) + .caption { display: none; }
.booking-modal .field-error:not([hidden]) {
  display: flex;
  align-items: flex-start;
  gap: var(--space-8);
  font-weight: 500;
}
.booking-modal .field-error:not([hidden])::before {
  content: "!";
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin-top: 1px;
  border-radius: 50%;
  background: var(--danger);
  color: #fff;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}
/* Submit failure (mailto never opened) — this node is created by the
   widget's JS and had no styling at all before. */
.booking-modal__error {
  margin-top: var(--space-12);
  padding: var(--space-12) var(--space-16);
  background: var(--bg-card);
  border: 1px solid var(--danger);
  border-left: 3px solid var(--danger);
  border-radius: var(--radius-input);
  color: var(--danger);
  font-weight: 500;
}
.booking-modal__error[hidden] { display: none; }

/* --- The "not yet" CTA ------------------------------------------------
   Keeps the documented disabled pair (--disabled-bg / --disabled-text,
   5.11:1) rather than inventing a colour, but stops reading as a dead
   grey slab: a dashed edge and a checklist glyph make the label what it
   actually is — a list of what's still missing — and the button keeps
   real button height so the step doesn't end on something smaller than
   the fields above it. */
.booking-modal .booking-widget__cta[aria-disabled="true"],
.booking-modal .booking-modal__next[aria-disabled="true"] {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-12);
  padding: var(--space-16) var(--space-20);
  border: 1px dashed var(--disabled-text);
  text-align: left;
  line-height: 1.35;
  box-shadow: inset 0 1px 3px rgba(21, 26, 46, 0.08);
}
.booking-modal .booking-widget__cta[aria-disabled="true"]::before,
.booking-modal .booking-modal__next[aria-disabled="true"]::before {
  content: "";
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  background-color: var(--disabled-text);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 6h11M9 12h11M9 18h11'/%3E%3Cpath d='m3 5.5 1.5 1.5L7 4'/%3E%3Cpath d='m3 11.5 1.5 1.5L7 10'/%3E%3Ccircle cx='4.5' cy='18' r='1.6'/%3E%3C/svg%3E") no-repeat center / contain;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 6h11M9 12h11M9 18h11'/%3E%3Cpath d='m3 5.5 1.5 1.5L7 4'/%3E%3Cpath d='m3 11.5 1.5 1.5L7 10'/%3E%3Ccircle cx='4.5' cy='18' r='1.6'/%3E%3C/svg%3E") no-repeat center / contain;
}
/* Enabled: the accent-fill/navy-text primary pill, at full button
   height so the step closes on something as substantial as the group
   cards above it. */
.booking-modal .booking-widget__cta:not([aria-disabled="true"]),
.booking-modal .booking-modal__next:not([aria-disabled="true"]) {
  padding: var(--space-16) var(--space-24);
}

/* Confirmation / fallback panels are direct children of the modal, so
   they need the top inset the header band would otherwise provide. */
.booking-modal > .booking-widget__confirmation,
.booking-modal > .inquiry-form__confirmation,
.booking-modal > .inquiry-form__fallback {
  margin-top: var(--modal-pad);
  padding: var(--space-24);
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-top: 3px solid var(--ink-900);
  border-radius: var(--radius-card);
  box-shadow: 0 1px 2px rgba(21, 26, 46, 0.04), 0 14px 30px -22px rgba(21, 26, 46, 0.35);
}

@media (prefers-reduced-motion: reduce) {
  .booking-modal .calendar-cell,
  .booking-modal .calendar-nav-btn,
  .booking-modal .slot-toggle,
  .calendar-month-select,
  .calendar-year-select { transition: none; }
  .booking-modal .calendar-cell:hover,
  .booking-modal .calendar-nav-btn:hover,
  .booking-modal .slot-toggle:hover { transform: none; }
}

@media (max-width: 599px) {
  .booking-modal { --modal-pad: var(--space-16); }
}
/* Below sm the seven 44px touch targets plus their gaps own almost the
   whole viewport (7 × 44 = 308px of a 360px screen), so the calendar
   card breaks out of the modal's padding rather than squeezing the
   cells below the spec's 44px minimum. */
@media (max-width: 479px) {
  .booking-modal .booking-widget__calendar {
    margin-inline: calc(-1 * var(--modal-pad));
    padding-inline: var(--space-4);
    border-left: none;
    border-right: none;
    border-radius: 0;
  }
  .booking-modal .calendar-grid { padding: var(--space-4); }
  .booking-modal .calendar-weekdays { padding-inline: var(--space-4); }
}

@media (max-width: 1023px) {
  .mobile-booking-bar { position: fixed; left: 0; right: 0; bottom: 0; background: var(--bg-card); border-top: 1px solid var(--line); padding: var(--space-12) var(--space-20) calc(var(--space-12) + env(safe-area-inset-bottom)); display: flex; align-items: center; justify-content: space-between; gap: var(--space-16); z-index: 20; transform: translateY(0); transition: transform var(--duration-base) var(--ease-in); }
  .mobile-booking-bar[hidden] { display: none !important; }
  .mobile-booking-bar.mobile-booking-bar--hidden { transform: translateY(100%); }
  .mobile-booking-bar .btn { flex-shrink: 0; }

  /* Below the desktop grid breakpoint, main.offer-page has no layout of
     its own (plain block flow), so the booking widget sits wherever
     .offer-rail happens to fall in the DOM — after the pickup map and
     reviews, many screens below the gallery. `order` moves it to sit
     right under the photos without touching DOM order, which the
     desktop grid above still depends on for its row auto-placement. */
  main.offer-page { display: flex; flex-direction: column; }
  main.offer-page > .offer-rail { order: 1; }
  /* Now redundant: the widget right above already carries the same CTA
     (id="booking-open-cta") plus vehicle/quantity pickers the shortcut
     skips straight past. */
  main.offer-page > .offer-mobile-cta { display: none; }
  main.offer-page > .offer-direct-booking,
  main.offer-page > .offer-intro,
  main.offer-page > .offer-timeline,
  main.offer-page > .offer-included,
  main.offer-page > .offer-bring,
  main.offer-page > .offer-pickup,
  main.offer-page > .offer-reviews,
  main.offer-page > .offer-faq,
  main.offer-page > .offer-related,
  main.offer-page > .offer-contact { order: 2; }
}
body.booking-modal-open { overflow: hidden; }

@media (min-width: 1024px) {
  .mobile-booking-bar { display: none !important; }
  .booking-widget { position: sticky; top: calc(var(--nav-h) + var(--space-8)); max-width: 400px; margin: 0 0 0 auto; align-self: start; }
  /* Desktop-only compaction so the rail's natural height matches the
     gallery's fixed 471.75px (see the .offer-gallery grid-template-rows
     override below) — never a touch target: .stepper__btn/.calendar-cell/
     etc. all keep their 44px min-width/min-height untouched. Mobile keeps
     the original, roomier spacing (this block only applies ≥1024px).
     Tuned empirically against kulig-sylwestrowy post-gallery-fix (rail
     471.31px vs gallery 471.75px, 0.44px off — sub-pixel, as exact as the
     box model allows) after "Termin: nie wybrano" was removed from the
     rail — that removal alone gave back most of the height budget, so
     only the widget's own padding and one font-size still need trimming;
     every other row already matches the original, uncompacted spacing. */
  .booking-widget { padding: var(--space-16); }
  .booking-widget .price-hero { font-size: 1.25rem; }
  main.offer-page { display: grid; grid-template-columns: minmax(0, 1fr) 420px; gap: var(--space-24); align-items: start; }
  main.offer-page > .offer-title-block, main.offer-page > .offer-intro, main.offer-page > .offer-timeline, main.offer-page > .offer-included, main.offer-page > .offer-bring, main.offer-page > .offer-pickup, main.offer-page > .offer-reviews, main.offer-page > .offer-faq, main.offer-page > .offer-related, main.offer-page > .offer-contact { grid-column: 1 / -1; }
  main.offer-page > .offer-gallery, main.offer-page > .offer-mobile-cta, main.offer-page > .offer-direct-booking { grid-column: 1; }
  /* One fixed gallery height on every trip page — this is what stops the
     blank strip under the rail, and it is a gallery bug, not a rail bug.

     Why the heights ever differed: the ≥768px rule sizes these two rows
     `minmax(160px, 1fr)`, and in an auto-height grid `1fr` is resolved from
     each item's max-content contribution. A <picture> here is `height: 100%`
     against an indefinite row, so it falls back to its content — an <img>
     whose max-content height is the column width times the REAL intrinsic
     ratio of whichever srcset candidate the browser fetched. The
     `<img width/height>` attributes in offer.html are hardcoded constants,
     identical on all 34 trip pages, so they hide this completely: only the
     actual photo files differ, and the row height silently tracked them.
     Measured at the capped 1250px layout (columns 371/185.5/185.5px):
     termy-chocholowskie 471.75px (driven by photo 2, a real 800x1000, so
     185.5 x 5/4 = 231.875px per row) but kulig-sylwestrowy 658.99px (driven
     by photo 1, a real 800x1421 hero: 371 x 1421/800 = 659px across both
     rows). 32 of 34 trip pages sat somewhere other than 471.75px.

     A fixed track size is definite, so item contributions stop being
     consulted at all — no photo can move the row again. 231.875px is the
     reference page's own row height at the capped width, so
     termy-chocholowskie is unchanged there and every other trip now matches
     it: 2 x 231.875 + 8px row gap = 471.75px, the height the rail's
     compaction above was tuned against. Being a fixed px value it also holds
     for the whole 1024-1250px range, where the gallery used to be pinned
     short by the ≥768px `min-height: 420px` (now inert, 471.75 > 420) while
     the rail stayed ~472 — so the rail matches at every desktop width, not
     just above 1250px.

     Taller photos crop rather than overflow: .offer-gallery picture already
     carries height:100% + overflow:hidden and .offer-gallery img
     object-fit:cover. Desktop-only on purpose — below 1024px there is no
     rail to match, so the ≥768px `minmax(160px, 1fr)` sizing is left alone.

     The two airport-transfer (vehicle-tier) pages used to be exempted here:
     their rail was a full inline contact form, taller than their own
     667.45px auto-height gallery, and normalising the gallery down to
     471.75px would have grown the residual gap below the rail. That
     inline form moved into a modal (#30 reopen, PR #95) and the rail lost
     its cash-note and fallback-sentence lines (#96, #97), so the rail is
     now a short calculator card — applying the same fixed height here
     (#99) leaves a smaller rail-to-direct-booking gap than standard trip
     pages already carry (measured ~148px vs. ~338px on a standard page at
     the 1250px cap), not a bigger one. No more exemption needed. */
  main.offer-page > .offer-gallery {
    grid-template-rows: 231.875px 231.875px;
  }
  /* Column 1 has exactly two rail-adjacent rows here (.offer-gallery, then
     .offer-direct-booking) before everything else goes full-width — `span 2`
     lets the rail's height be measured against their combined height
     instead of inflating the gallery's row alone. `span 2` is the load-
     bearing exact value: `grid-row: 2 / -1` looks equivalent but is NOT —
     `-1` resolves against the EXPLICIT row grid, which is empty here (no
     grid-template-rows is set), so `-1` means line 1, not "last row," and
     resolves to row 1 (swapped because start > end per the grid spec) —
     that threw the rail to the very top of the page. Any span > 2 pushes
     the next full-width section (.offer-intro) down with an empty row.
     Rare tall-rail trips (the airport-transfer vehicle-tier calculator)
     can still leave a smaller residual gap below .offer-direct-booking —
     acceptable, and much smaller than the gap this replaces. */
  main.offer-page > .offer-rail { grid-column: 2; grid-row: 2 / span 2; align-self: start; }
  /* .offer-direct-booking's own margin-top (mobile's only spacing
     mechanism, since main.offer-page is plain block flow below 1024px)
     stacked on top of the grid's own row gap on desktop — zero it here,
     desktop-only. */
  main.offer-page > .offer-direct-booking { margin-top: 0; }
}

/* ── Homepage main layout ─────────────────────────────────────────────
   .offer-page (line 258) is trip-page-only per Task 1, so it supplies no
   horizontal padding/max-width here — and the homepage couldn't reuse it
   anyway: .home-hero has to reach both viewport edges while every other
   section stays inside the same shell the nav/footer inners use.

   So rather than padding <main> and clawing the hero back out with
   negative margins (or with 100vw, which overshoots by the scrollbar
   width under `scrollbar-gutter: stable both-edges`), <main> is a
   three-column grid: gutter / content / gutter. Sections sit in the
   content column; the hero spans full-start → full-end.

   1202px = 1250px (the .site-nav__inner / .site-footer__inner max-width)
   minus its two var(--space-24) gutters — i.e. the exact content-box
   width those inners resolve to, so homepage section content lines up
   with the nav and footer at every viewport. */
main.home-page {
  display: grid;
  grid-template-columns:
    [full-start] 1fr
    [content-start] min(1202px, 100% - 2 * var(--space-24)) [content-end]
    1fr [full-end];
}
main.home-page > * { grid-column: content; }
main.home-page > .home-hero { grid-column: full; }

/* ── Homepage hero ─────────────────────────────────────────────────────
   Ambient photo crossfade in the background — deliberately NOT a
   carousel: one fixed headline/CTA over all photos, no nav chrome, no
   per-photo captions, no zoom/pan. See spec's Motion-section amendment
   for why this is allowed at all. */
.home-hero {
  position: relative;
  min-height: 560px;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  /* Full-bleed box, container-aligned contents: the inline padding is
     the grid gutter above, recomputed here (percentages resolve against
     the full-width grid area) so the <h1> starts on the same vertical
     line as .home-offers/.home-testimonials/etc. */
  padding: var(--space-24) max(var(--space-24), (100% - 1202px) / 2);
}
.home-hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.home-hero__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--duration-ambient) linear;
}
.home-hero__media img.is-active { opacity: 1; }
.home-hero__media::after {
  /* Light scrim, mostly for mood — the photo is meant to stay the focal
     point, not a dark wash. Real text legibility against whichever of
     the 4 rotating photos is brightest (the kulig sleigh shot's
     overcast-white sky is the worst case) comes from the text-shadow on
     .home-hero__content below, not this gradient. */
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(21, 26, 46, 0.1) 0%, rgba(21, 26, 46, 0.22) 45%, rgba(21, 26, 46, 0.75) 100%);
}
.home-hero__content {
  position: relative;
  z-index: 1;
  max-width: 640px;
  /* No inline padding — .home-hero's own inline padding already places
     this on the container's left edge. */
  padding: var(--space-48) 0 var(--space-8);
}
.home-hero__content h1, .home-hero__content .lede {
  /* Guarantees legibility over the brightest photo regardless of what's
     directly behind any given glyph — the scrim gradient above can't do
     this alone since it varies by vertical position, not by photo. */
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55), 0 2px 18px rgba(0, 0, 0, 0.4);
}
.home-hero__content .lede {
  /* This is a two-line tagline, not body copy — bigger and bolder than
     the shared .lede default (used elsewhere as plain descriptive
     text) so it reads as a second headline, not a footnote. */
  font-size: clamp(1.25rem, 1.1rem + 0.75vw, 1.625rem);
  font-weight: 600;
  line-height: 1.35;
}
.home-hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-12); margin-top: var(--space-24); }
/* Fixed so the PL/EN builds render the same width — "Zobacz wycieczki"
   (PL) is ~176px at this font/padding, "See our trips" (EN) only
   ~143px; without a shared min-width the button visibly grows/shrinks
   between language builds even though nothing else on the page moves. */
.home-hero__actions .btn--primary { min-width: 180px; }
.home-hero__actions .btn--secondary { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.4); color: var(--on-dark); }
.home-hero__actions .btn--secondary:hover { background: rgba(255, 255, 255, 0.16); border-color: var(--on-dark); }
.home-hero__trust { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-8) var(--space-20); margin: var(--space-24) 0 0; padding: 0; color: var(--on-dark-soft); font-size: 0.875rem; }
/* Icon-only social buttons — same rgba-on-dark treatment as
   .home-hero__actions' outlined secondary button, so they read as
   related chrome rather than a bolted-on widget. Shared by the hero
   and the footer's "Follow us" column (both on-dark contexts). */
.social-links { display: flex; gap: var(--space-12); margin-top: var(--space-16); }
.social-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-40);
  height: var(--space-40);
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.4);
  color: var(--on-dark);
  transition: background var(--duration-fast) var(--ease-in), border-color var(--duration-fast) var(--ease-in);
}
.social-icon svg { width: 18px; height: 18px; }
.social-icon:hover, .social-icon:focus-visible { background: var(--accent-bright); border-color: var(--accent-bright); }
.social-icon:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .home-hero__media img { transition: none; }
}
@media (min-width: 768px) {
  .home-hero { min-height: 680px; padding: var(--space-64) max(var(--space-24), (100% - 1202px) / 2); }
  .home-hero__content { padding: var(--space-64) 0 var(--space-16); }
}

/* ── Homepage offer grid ──────────────────────────────────────────────
   All trips, GetYourGuide-style thumbnail cards. Static grid, no
   carousel, no photo-zoom-on-hover (same ban as the trip-page gallery). */
.home-offers { padding: var(--space-64) 0 var(--space-32); }
/* Lato/Zima filter (#27) sits on the heading's row on desktop, wraps to
   its own line on narrow viewports rather than squeezing the heading. */
.home-offers__header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-12); }
/* .slot-toggle-row's own padding is sized for its booking-widget context
   (a card interior) — zero it out here, this row sits directly in the
   page, not inside a bordered card. */
.home-offers__header .season-filter.slot-toggle-row { padding: 0; }
/* Fixed so both toggle pills render at the same width regardless of
   language/label — the widest label ("Summer", EN) is ~84px at this
   font/padding, vs. "Lato"/"Zima" (PL) at ~61-64px; without a shared
   min-width the pair visibly shrinks between language builds. */
.season-filter .slot-toggle { min-width: 88px; }
.offer-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-24); margin-top: var(--space-24); }
/* Grid items stretch to the row's tallest card by default (no
   align-items override on .offer-grid) — the flex chain below
   (card -> link -> body) carries that full height down to the CTA,
   so "Zobacz szczegóły" pins to the same bottom edge in every card
   of a row regardless of whether that card's own title wraps to one
   line or two. */
.offer-card { display: flex; flex-direction: column; height: 100%; border-radius: var(--radius-card); overflow: hidden; background: var(--bg-card); border: 1px solid var(--line); transition: box-shadow var(--duration-base) var(--ease-in), transform var(--duration-base) var(--ease-in); }
/* Same gotcha as .btn[hidden] above: this rule's own display:flex beats
   the UA [hidden] default, so the season filter's card.hidden = true
   would silently do nothing without this override. */
.offer-card[hidden] { display: none; }
.offer-card:hover { transform: translateY(-2px); box-shadow: 0 12px 28px -16px rgba(21, 26, 46, 0.3); }
.offer-card__link { display: flex; flex-direction: column; flex: 1; text-decoration: none; color: inherit; }
.offer-card__media { position: relative; aspect-ratio: 4 / 3; overflow: hidden; }
/* <picture> is inline by default, which would leave the img's height: 100%
   with no definite parent height to resolve against — same reason
   .offer-gallery picture is sized explicitly. */
.offer-card__media picture { display: block; width: 100%; height: 100%; }
.offer-card__media img { width: 100%; height: 100%; object-fit: cover; }
.offer-card__season { position: absolute; top: var(--space-12); left: var(--space-12); background: var(--ink-900); color: var(--on-dark); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; padding: var(--space-4) var(--space-12); border-radius: var(--radius-pill); }
.offer-card__body { display: flex; flex-direction: column; flex: 1; padding: var(--space-16) var(--space-20) var(--space-20); }
/* min-height reserves space for 2 lines always (the longest current
   title, e.g. "Kraina Lodu i Ścieżka w Koronach Drzew", already wraps
   to 2 at this card width) so 1-line titles don't leave the price
   sitting higher than its row-mates — same reasoning as the CTA's
   margin-top: auto below, just solving it above the price instead of
   below it. */
.offer-card__title { font-size: 1.0625rem; line-height: 1.3; min-height: 2.6em; margin: 0 0 var(--space-8); }
.offer-card__price { color: var(--text-secondary); margin: 0; }
.offer-card__price-unit { font-size: 0.8125rem; }
.offer-card__price-secondary { color: var(--text-secondary); font-size: 0.8125rem; margin: var(--space-4) 0 0; }
.offer-card__cta { display: inline-block; margin-top: auto; padding-top: var(--space-12); color: var(--accent-ink); font-weight: 600; font-size: 0.9375rem; }
@media (min-width: 640px) {
  .offer-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .offer-grid { grid-template-columns: repeat(4, 1fr); }
}

/* ── Trip page "related trips" mini-grid ──────────────────────────────
   Same .offer-card markup/styling as the homepage grid, just a shorter
   grid (at most 3 cards, see renderRelatedTripsHtml) so it tops out at
   3 columns rather than 4 — a 4th empty track would leave a gap on the
   common 2-related-trips case's own row instead of centering it, but
   3 columns still reads correctly whether 2 or 3 cards are present. */
.related-trips-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-24); margin-top: var(--space-24); }
@media (min-width: 640px) {
  .related-trips-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .related-trips-grid { grid-template-columns: repeat(3, 1fr); }
}

/* ── Homepage testimonials / transport ──────────────────────────────── */
.home-testimonials, .home-transport { padding: var(--space-48) 0; border-top: 1px solid var(--line); }
/* Same column/gap breakpoints as .offer-grid (2 up at 640px, 4 up at
   1024px) so transport cards render at the same width as offer cards
   instead of stretching 3-wide across the full content column — with
   only 3 cards, the 4-up tier just leaves its 4th track unused. */
.transport-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-24); margin-top: var(--space-24); }
/* Continuous sliding strip (not a paged grid): .testimonial-track holds
   the card list duplicated once (see renderTestimonialsSectionHtml), and
   the keyframe scrolls exactly one set's width (translateX(-50%)) so the
   loop is seamless. The viewport clips overflow and is the hover/focus
   target that pauses the animation for reading. prefers-reduced-motion
   turns it into a plain manually-scrollable strip instead of forcing
   motion on users who've asked to avoid it. */
/* Redesigned 2026-09-13 (#147 second pass). Mateusz rejected the first
   version — "does not feel premium at all, they are sliding too fast and
   look awful" — and the three causes were all structural, not taste:
   1. Speed: 6s per card width (~53px/s) whipped quotes past before they
      could be read. Now 13s (~29px/s), set in build-home.js.
   2. Hard edges: cards were sliced flat at the viewport's left/right
      edge. A gradient overlay at each edge now fades them in and out
      instead (see the note below on why it is an overlay and not a
      mask), which is what makes a marquee read as designed rather than
      accidentally clipped.
   3. Dead space: flex stretch sized EVERY card to the single longest
      quote, so short quotes sat above ~250px of blank white. Cards are
      wider with smaller, looser quote type (which compresses the
      longest/shortest range), and the attribution is pinned to the card
      bottom so the remaining slack reads as deliberate padding.
   The rest is credibility/polish: real elevation instead of a bare 1px
   box, a 5-star row (every one of these is a real 5-star Google review
   — see build-home.js), an avatar initial, and a watermark quote mark.
   --testimonial-fade scales the edge fade with the viewport so it
   doesn't eat half a phone screen.

   The fade is two gradient overlays, NOT mask-image on the viewport,
   even though mask-image is the tidier way to write it. The track is
   ~23,000px wide (30 cards rendered twice) — well past Chrome's 16,384px
   max texture size — and masking an ancestor of a layer that big forces
   it into one surface it cannot rasterize: tested here, and the
   compositor stopped producing frames entirely (the page rendered but
   would not even screenshot). Overlays paint two small fixed-size
   rectangles instead and cost nothing. Do not "simplify" this back to
   mask-image without re-testing on the full 30-card strip.

   The overlays fade to --bg-page because this section has no background
   of its own; if it ever gets one, these two gradients have to match it. */
.testimonial-track-viewport {
  /* Kept modest on purpose: the track runs edge to edge, so there is
     always a card under the fade — at 6vw it washed out the first few
     characters of a fully-visible card and read as a rendering fault
     rather than as depth. */
  --testimonial-fade: clamp(20px, 4vw, 72px);
  position: relative;
  margin-top: var(--space-32);
  overflow: hidden;
  /* The marquee needs overflow:hidden, which would also clip the card
     shadows top/bottom — this padding gives them room to show. */
  padding: var(--space-12) 0 var(--space-24);
}
.testimonial-track-viewport::before,
.testimonial-track-viewport::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--testimonial-fade);
  z-index: 1;
  pointer-events: none;
}
/* The transparent end is spelled as rgba(--bg-page, 0) rather than the
   `transparent` keyword so the ramp can't interpolate through
   transparent *black* and leave a grey smudge over the cards. */
.testimonial-track-viewport::before { left: 0; background: linear-gradient(to right, var(--bg-page), rgba(238, 240, 244, 0)); }
.testimonial-track-viewport::after { right: 0; background: linear-gradient(to left, var(--bg-page), rgba(238, 240, 244, 0)); }
/* Linear on purpose: an eased timing function on an infinite marquee
   visibly stutters at every loop boundary (it decelerates into the
   seam, then jumps back to full speed). Comfort comes from the slower
   duration, not from easing. */
.testimonial-track { display: flex; align-items: stretch; gap: var(--space-24); width: max-content; animation-name: testimonial-marquee; animation-timing-function: linear; animation-iteration-count: infinite; }
.testimonial-track-viewport:hover .testimonial-track,
.testimonial-track-viewport:focus-within .testimonial-track { animation-play-state: paused; }
@keyframes testimonial-marquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .testimonial-track { animation: none; }
  .testimonial-track-viewport { overflow-x: auto; }
  /* Edge fades dropped with the animation: in the static scrollable
     strip the end cards would sit permanently under a fade instead of
     passing through it, which just looks like half-dimmed broken
     content — and the right-hand one would hide the fact that there is
     more to scroll to. */
  .testimonial-track-viewport::before,
  .testimonial-track-viewport::after { display: none; }
}
@media (min-width: 640px) {
  .transport-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
  .transport-grid { grid-template-columns: repeat(4, 1fr); }
}
.testimonial-card { position: relative; display: flex; flex-direction: column; flex: 0 0 auto; width: 360px; background: var(--bg-card); border: 1px solid var(--line); border-radius: var(--radius-sheet); padding: var(--space-24); box-shadow: 0 1px 2px rgba(21, 26, 46, 0.04), 0 14px 30px -20px rgba(21, 26, 46, 0.35); transition: box-shadow var(--duration-base) var(--ease-in), transform var(--duration-base) var(--ease-in); }
/* Motion pauses on hover (see the viewport rule above), so the card is
   stationary by the time this lift applies — it never fights the slide. */
.testimonial-card:hover { transform: translateY(-2px); box-shadow: 0 2px 4px rgba(21, 26, 46, 0.05), 0 18px 36px -18px rgba(21, 26, 46, 0.38); }
/* Watermark quote mark, drawn as a mask rather than ::after text so the
   glyph can't be announced by a screen reader as stray punctuation, and
   so it can be tinted with a token instead of a hardcoded fill. */
.testimonial-card::after { content: ""; position: absolute; top: var(--space-24); right: var(--space-24); width: 28px; height: 22px; pointer-events: none; background-color: var(--accent-wash); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 24'%3E%3Cpath d='M13 24V13.6C13 6.1 16.7 1.6 23.6 0L25 3.2C20.7 5 18.6 7.6 18.4 11H23v13zM0 24V13.6C0 6.1 3.7 1.6 10.6 0L12 3.2C7.7 5 5.6 7.6 5.4 11H10v13z'/%3E%3C/svg%3E") no-repeat center / contain; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 24'%3E%3Cpath d='M13 24V13.6C13 6.1 16.7 1.6 23.6 0L25 3.2C20.7 5 18.6 7.6 18.4 11H23v13zM0 24V13.6C0 6.1 3.7 1.6 10.6 0L12 3.2C7.7 5 5.6 7.6 5.4 11H10v13z'/%3E%3C/svg%3E") no-repeat center / contain; }
/* One repeating star tile rather than 5 inline SVGs — the card list is
   rendered twice for the seamless loop, so inline stars would mean 300
   copies of the same path in the HTML. Tinted via background-color +
   mask so it uses --ember instead of a colour baked into the data URI.
   Fixed 5/5 for every card: these are all real 5-star Google reviews.
   The meaning is carried by role="img" + aria-label on the element. */
.testimonial-card__rating { display: block; width: 110px; height: 20px; flex: 0 0 auto; background-color: var(--ember); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 20'%3E%3Cpath d='M11 1.6l2.6 5.27 5.82.85-4.21 4.1 1 5.8L11 14.88l-5.21 2.74 1-5.8-4.21-4.1 5.82-.85z'/%3E%3C/svg%3E") repeat-x left center / 22px 20px; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 20'%3E%3Cpath d='M11 1.6l2.6 5.27 5.82.85-4.21 4.1 1 5.8L11 14.88l-5.21 2.74 1-5.8-4.21-4.1 5.82-.85z'/%3E%3C/svg%3E") repeat-x left center / 22px 20px; }
/* padding-right keeps the first line clear of the watermark glyph.
   The 8-line clamp is what keeps the card height sane: every card in the
   row is the same height (flex stretch), so the single longest review
   sized ALL of them, and unclamped that was a 10-line/408px card leaving
   ~150px of blank white under every 4-line quote. 8 lines caps the row
   at 357px while truncating only 2 of the 30 real reviews (measured in
   browser, both PL and EN) — the rest are untouched. Raising or removing
   it puts the dead space straight back. */
.testimonial-card__quote { font-family: "Bricolage Grotesque", "Bricolage Grotesque Fallback", sans-serif; font-size: 1rem; line-height: 1.6; color: var(--text-primary); margin: var(--space-16) 0 0; padding-right: var(--space-8); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 8; line-clamp: 8; overflow: hidden; }
/* margin-top:auto pins the attribution to the bottom edge of the card.
   Cards are equal height (flex stretch) because a ragged-bottom row
   looks broken while sliding, so short quotes leave slack above this
   row — pinned, that slack reads as padding instead of a gap. */
.testimonial-card__footer { display: flex; align-items: center; gap: var(--space-12); margin-top: auto; padding-top: var(--space-20); }
.testimonial-card__avatar { display: grid; place-items: center; flex: 0 0 auto; width: 40px; height: 40px; border-radius: var(--radius-pill); background: var(--accent-wash); color: var(--accent-ink); font-weight: 600; font-size: 0.9375rem; }
.testimonial-card__who { display: flex; flex-direction: column; min-width: 0; }
.testimonial-card__name { font-weight: 600; font-size: 0.9375rem; color: var(--text-primary); }
.testimonial-card__source { display: inline-flex; align-items: center; gap: var(--space-4); font-size: 0.8125rem; color: var(--text-muted); }
.testimonial-card__google-icon { width: 14px; height: 14px; flex: 0 0 auto; }
@media (max-width: 480px) {
  .testimonial-card { width: 280px; }
}
/* Mirrors .offer-card: media + body inside one full-card link, so the
   whole card is clickable through to the partner site, not just the
   trailing "Zobacz stronę" text. */
.transport-card { display: flex; flex-direction: column; height: 100%; border-radius: var(--radius-card); overflow: hidden; background: var(--bg-card); border: 1px solid var(--line); transition: box-shadow var(--duration-base) var(--ease-in), transform var(--duration-base) var(--ease-in); }
.transport-card:hover { transform: translateY(-2px); box-shadow: 0 12px 28px -16px rgba(21, 26, 46, 0.3); }
.transport-card__link { display: flex; flex-direction: column; flex: 1; text-decoration: none; color: inherit; }
.transport-card__media { position: relative; aspect-ratio: 4 / 3; overflow: hidden; }
.transport-card__media picture { display: block; width: 100%; height: 100%; }
.transport-card__media img { width: 100%; height: 100%; object-fit: cover; }
.transport-card__body { display: flex; flex-direction: column; flex: 1; padding: var(--space-16) var(--space-20) var(--space-20); }
.transport-card__title { font-size: 1.0625rem; margin: 0 0 var(--space-8); }
.transport-card__description { color: var(--text-secondary); font-size: 0.9375rem; margin: 0 0 var(--space-12); }
.transport-card__cta { display: inline-block; margin-top: auto; padding-top: var(--space-12); color: var(--accent-ink); font-weight: 600; font-size: 0.9375rem; }

/* Deliberately not navy — .offer-direct-booking above already spent
   this page's one navy content band (see spec's Positioning/Design
   system: at most one per page). Centered/elevated to match that same
   box's treatment (see the "Center and elevate" pass on
   .offer-direct-booking) — bigger padding, centered flex layout,
   heading/body/button-row all centered instead of hugging the left
   edge of a mostly-empty band. */
.home-contact {
  background: var(--bg-sunken);
  border-radius: var(--radius-card);
  padding: var(--space-64) var(--space-32);
  margin: var(--space-48) 0 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.home-contact .body-text { margin: var(--space-16) auto 0; }

/* ── /transport/ page title block ─────────────────────────────────────
   The deck was `.body-text .offer-deck`, which sets `max-width: none` —
   on a trip page that's harmless (the deck sits in a ~760px content
   column beside the booking rail), but this page's <main> is the
   homepage's full 1202px shell, so the deck ran ~150 characters per
   line. It's a two-sentence tagline, not body copy: `.lede` at a
   deliberate measure instead. */
.transport-hero { padding: var(--space-48) 0 var(--space-32); }
.transport-hero__heading { max-width: 20ch; }
.transport-hero__deck { margin-top: var(--space-16); color: var(--text-secondary); max-width: 42rem; }
@media (min-width: 768px) {
  .transport-hero { padding: var(--space-64) 0 var(--space-40); }
  .transport-hero__heading { max-width: 24ch; }
}

/* Jump links to the five detail sections. Reuses the existing .chip pill
   (same geometry/colours as the trip-page meta pills) — the anchor state
   is the only thing a static .chip doesn't already carry. */
.transport-index { display: flex; flex-wrap: wrap; gap: var(--space-8); margin-top: var(--space-24); }
a.transport-index__link { text-decoration: none; transition: background-color var(--duration-fast) var(--ease-in), color var(--duration-fast) var(--ease-in); }
a.transport-index__link:hover { background: var(--accent-wash); color: var(--accent-ink); }
a.transport-index__link:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }

/* ── /transport/ page detail sections ─────────────────────────────────
   Heading + body copy + optional package list(s) + optional FAQ + one
   external CTA per section. Reuses .included-list (existing, trip-page
   "W cenie" bullets) for package items and .faq-item (existing) for FAQ
   — no new list/accordion styling needed.

   Layout: a single column on mobile; at lg a head rail (index, heading,
   partner) beside a body column. The rail is what fixes the "narrow
   text" problem — prose stays at its 34rem measure, but that measure now
   sits in a ~740px column with a filled rail beside it, instead of
   hugging the left edge of an empty 1202px band. */
.transport-service, .group-service { padding: var(--space-48) 0; scroll-margin-top: calc(var(--nav-h) + var(--space-12)); }
/* Same 3px accent-ink rule the trip-page timeline uses to open a node —
   it marks where a numbered step starts, which is exactly what this is. */
.transport-service__head::before, .group-service__head::before { content: ""; display: block; width: var(--space-40); height: 3px; border-radius: var(--radius-pill); background: var(--accent-ink); margin-bottom: var(--space-16); }
.transport-service__index, .group-service__index { color: var(--text-muted); margin-bottom: var(--space-8); font-variant-numeric: tabular-nums; }
.transport-service__partner { color: var(--text-muted); margin-top: var(--space-12); }
/* One photo under the section title, only on sections that have one
   (data/integracja*.json's `photo` field) — same media-box recipe as
   .offer-card__media, just its own rounding/spacing here. */
.group-service__photo { margin-top: var(--space-16); aspect-ratio: 4 / 3; border-radius: var(--radius-card); overflow: hidden; }
.group-service__photo picture { display: block; width: 100%; height: 100%; }
.group-service__photo img { width: 100%; height: 100%; object-fit: cover; }
/* Paragraphs had no separation at all before (base.css zeroes every <p>
   margin and nothing put it back here), which is most of why five short
   sections read as one wall. */
.transport-service__body, .group-service__body { margin-top: var(--space-20); }
.transport-service__body > .body-text + .body-text, .group-service__body > .body-text + .body-text { margin-top: var(--space-16); }
@media (min-width: 1024px) {
  .transport-service__body, .group-service__body { margin-top: 0; }
  .transport-service, .group-service {
    padding: var(--space-80) 0;
    display: grid;
    /* The body column is the fixed one and the head rail flexes, not the
       other way round: prose is capped at 34rem, so a fixed rail would
       push more and more empty space to the RIGHT of the copy as the
       viewport grows — which is exactly what made this page read as a
       narrow ribbon in an empty band. Capping the body at 40rem instead
       parks the slack in the left margin, where it reads as a margin,
       and keeps the FAQ/package cards flush with the container's right
       edge at every width. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 40rem);
    gap: var(--space-64);
    align-items: start;
  }
  /* Tracks the copy on the long FAQ sections. Same offset the booking
     widget uses so both sticky things clear the nav identically. */
  .transport-service__head, .group-service__head { position: sticky; top: calc(var(--nav-h) + var(--space-8)); }
}

.transport-service__packages { display: grid; gap: var(--space-20); grid-template-columns: 1fr; margin-top: var(--space-32); }
@media (min-width: 768px) { .transport-service__packages { grid-template-columns: 1fr 1fr; } }
/* Cards, not bare lists: two itemised packages side by side read as an
   offer to compare, where two unboxed <ul>s read as one run-on list.
   Same card recipe as .offer-card/.transport-card. */
.transport-service__package { background: var(--bg-card); border: 1px solid var(--line); border-radius: var(--radius-card); padding: var(--space-24); }
.transport-service__package-title { font-size: 1rem; }
/* Hanging indent: several of these items wrap, and the shared
   .included-list bullet is inline, so line 2 was starting under the
   bullet rather than under the first word. */
.transport-service__package .included-list li { display: flex; gap: var(--space-8); }
.transport-service__package .included-list li::before { margin-right: 0; flex: none; }
.group-service__body .included-list li { display: flex; gap: var(--space-8); }
.group-service__body .included-list li::before { margin-right: 0; flex: none; }
.legal-section .included-list li { display: flex; gap: var(--space-8); }
.legal-section .included-list li::before { margin-right: 0; flex: none; }
/* Same 3px accent-ink mark the transport/group service cards use to
   open a numbered step — legal sections are numbered too, so without
   it they read as bare text instead of structured content. */
.legal-section h2::before { content: ""; display: block; width: var(--space-40); height: 3px; border-radius: var(--radius-pill); background: var(--accent-ink); margin-bottom: var(--space-16); }
.legal-section h2 { margin-bottom: var(--space-16); }
/* margin-bottom has no effect here — it collapses against the next
   sibling's own margin-top from the rule below (max(8,16) = 16px) —
   so only the top gap before a new subsection is real; that's the
   intended rhythm, kept as one rule instead of a misleading pair. */
.legal-section h3 { margin-top: var(--space-24); }
/* Also matches .legal-content's own direct children (the privacy
   policy's intro paragraphs sit there, not inside a .legal-section). */
.legal-content > * + *, .legal-section > * + * { margin-top: var(--space-16); }
/* Sitewide .body-text caps at 34rem for card/testimonial copy — too
   narrow for a long-form legal document read start to finish, where
   it left most of the 1202px content column empty on the right.
   Covers .included-list too so a section's list doesn't run wider
   than its own paragraphs. */
.legal-content .body-text, .legal-content .included-list { max-width: 46rem; }

/* The FAQ is 10 rows on two of these sections. Boxed, it reads as one
   object with a scannable edge; unboxed it was 10 loose hairlines that
   made the whole page look like a support document. */
.transport-service__faq { background: var(--bg-card); border: 1px solid var(--line); border-radius: var(--radius-card); padding: var(--space-8) var(--space-24) var(--space-16); margin-top: var(--space-32); }
.transport-service__faq > .h3 { padding: var(--space-16) 0 var(--space-4); }
.transport-service__faq .faq-item summary {
  list-style: none;
  display: flex;
  align-items: baseline;
  gap: var(--space-16);
  border-radius: var(--radius-input);
}
.transport-service__faq .faq-item summary::-webkit-details-marker { display: none; }
.transport-service__faq .faq-item summary:hover { color: var(--accent-ink); }
.transport-service__faq .faq-item summary:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 4px; }
.transport-service__faq .faq-item summary::after {
  content: "+";
  margin-left: auto;
  flex: none;
  font-family: "Outfit", "Outfit Fallback", sans-serif;
  font-weight: 400;
  font-size: 1.375rem;
  line-height: 1;
  color: var(--accent-ink);
}
.transport-service__faq .faq-item[open] summary::after { content: "\2013"; }
.transport-service__faq .faq-item[open] summary { color: var(--accent-ink); }

.transport-service__cta { margin-top: var(--space-32); }

/* ── /integracja/ inquiry form ─────────────────────────────────────────
   Same box treatment as .booking-widget (card bg, navy top border) so it
   reads as this page's one interactive/contact element, same weight as
   the booking widget is on a trip page. Fields reuse .text-input/.small/
   .field-error as-is — no new form-control styling needed. */
.inquiry-form { display: grid; gap: var(--space-16); max-width: 40rem; margin-top: var(--space-24); background: var(--bg-card); border: 1px solid var(--line); border-top: 3px solid var(--ink-900); border-radius: var(--radius-card); padding: var(--space-24); }
.inquiry-form[hidden] { display: none; }
.inquiry-form label { display: block; margin-bottom: var(--space-4); }
.inquiry-form .text-input { width: 100%; }
.inquiry-form__row { display: grid; gap: var(--space-16); grid-template-columns: 1fr; }
@media (min-width: 640px) {
  .inquiry-form__row { grid-template-columns: 1fr 1fr; }
}
.inquiry-form__confirmation { margin-top: var(--space-24); max-width: 40rem; }

/* ── /narty/ page ──────────────────────────────────────────────────────
   The calculator is this page's one interactive object, and the first
   two passes both left it unboxed: loose text on the bare page ground,
   while the *contact form below it* was a proper card. The least
   important element on the page looked more designed than the most
   important one. Every approved equivalent on this site is boxed —
   .booking-widget, .inquiry-form, .transport-service__package/__faq —
   so the calculator is now one card too, on the same recipe
   (bg-card + hairline + 3px ink-900 top edge).

   Inside that card, three zones at lg: choose (controls + price + CTA),
   detail (what's included + footnotes), and the photo full-bleed to the
   card's right edge. Splitting choose/detail is what fixes the controls:
   .stepper-row is `justify-content: space-between`, so in the old 640px
   column every label sat ~350px away from its own control and the rows
   read as a broken form. At ~20rem they read as the booking widget's
   rows do, which is the width they were designed at. */
.narty-calculator { border-top: 0; }
/* Same 3px accent-ink opener /transport/'s sections use — with the
   intro folded in below the heading (see .narty-calculator__intro),
   this replaces the section hairline rather than adding to it. */
.narty-calculator__head::before { content: ""; display: block; width: var(--space-40); height: 3px; border-radius: var(--radius-pill); background: var(--accent-ink); margin-bottom: var(--space-16); }
/* Was its own `.offer-intro` section: one 34rem paragraph alone in the
   1202px band, i.e. premium-bar item #1 verbatim. It's a two-sentence
   deck about the packages, so it belongs under this heading, at the
   hero deck's measure, with the card immediately below anchoring the
   width — not floating in a band of its own. */
.narty-calculator__intro { margin-top: var(--space-12); color: var(--text-secondary); max-width: 42rem; }

.narty-calculator__panel {
  margin-top: var(--space-32);
  /* Grid at every width, not only at lg: below lg the photo is a single
     column item that `order` has to be able to move to the top of the
     card, and `order` is inert on a plain block container. */
  display: grid;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-top: 3px solid var(--ink-900);
  border-radius: var(--radius-card);
  /* Clips the photo to the card's rounded corners. */
  overflow: hidden;
}
.narty-calculator__form { padding: var(--space-24); }
.narty-calculator__choose, .narty-calculator__detail { display: flex; flex-direction: column; }
/* Every one of these gaps used to be 0: .narty-calculator__body was a
   flex column with no gap over children whose margins base.css zeroes,
   so the price sat flush against the includes heading and the footnotes
   fused onto the last bullet. Nothing here may rely on element margins. */
.narty-calculator__choose { gap: var(--space-24); }
.narty-calculator__detail { gap: var(--space-24); margin-top: var(--space-32); }

/* Contiguous rows, not detached hairlines: the old `gap: var(--space-4)`
   pushed each row's own border-top 4px clear of the row above it, so
   four separators floated in the whitespace. The horizontal padding goes
   too — .stepper-row's 16px inset is for a full-bleed widget row, and
   here it only doubles up on the card's own padding. */
.narty-calculator__controls { border-bottom: 1px solid var(--line); }
.narty-calculator__controls .stepper-row { padding-left: 0; padding-right: 0; gap: var(--space-16); }
/* Aligns the toggle buttons' right edge with the steppers' — the 12px
   inset from .slot-toggle-row left the two control types 12px apart. */
.narty-calculator__controls .slot-toggle-row { padding: 0; justify-content: flex-end; }

/* The price is the entire point of this page and it was rendering at
   32px — identical to the .h2 beside it, 10px more than the .h3 it
   touched. Now it's the strongest contrast on the page: a dark readout
   inside the white card, using the ink-900/on-dark pairing the pressed
   .slot-toggle already establishes. */
.narty-calculator__price {
  background: var(--ink-900);
  border-radius: var(--radius-card);
  padding: var(--space-20) var(--space-24);
  color: var(--on-dark);
}
.narty-calculator__price-label { color: var(--on-dark-muted); }
.narty-calculator__price .price-hero { margin-top: var(--space-8); font-size: clamp(2.5rem, 2rem + 2vw, 3.25rem); color: var(--on-dark); }
/* Says what the number is for ("2 osoby · 1 dzień"), written by
   narty-calculator.js on every render — without it the price is a bare
   figure with no stated basis. */
.narty-calculator__price-sub { margin-top: var(--space-8); color: var(--on-dark-soft); }

.narty-calculator__cta { width: 100%; }
.narty-calculator__includes .included-list { margin-top: var(--space-16); }
/* Hanging indent, same fix the transport package lists needed: the
   shared bullet is inline, so wrapped items restarted under the bullet. */
.narty-calculator__includes .included-list li { display: flex; gap: var(--space-8); }
.narty-calculator__includes .included-list li::before { margin-right: 0; flex: none; }
/* Four footnotes rendered as bare <p>s, and base.css zeroes every <p>
   margin, so they ran together as one grey block hard against the last
   bullet — premium-bar item #2. */
.narty-calculator__notes { color: var(--text-muted); border-top: 1px solid var(--line); padding-top: var(--space-16); }
.narty-calculator__notes p + p { margin-top: var(--space-8); }

/* Photo: full-bleed to the card's edge rather than a free-floating
   rounded slab in dead space. It also stops being upscaled — the source
   derivatives are 380px wide (build-narty.js declares exactly that in
   `sizes`), and the old `minmax(0, 1fr)` column stretched them to 498px,
   i.e. a 1.3x blow-up of an already-small file. The column is pinned to
   380px so the file is never asked for more than it has. */
.narty-calculator__media { background: var(--bg-sunken); }
.narty-calculator__media-item { height: 100%; }
.narty-calculator__media-item picture { display: block; height: 100%; }
.narty-calculator__media-item img { width: 100%; height: 100%; object-fit: cover; display: block; }

@media (max-width: 1023px) {
  /* Card header on narrow screens. The old 3/4 portrait was ~470px tall
     on a phone — most of a viewport spent on one photo. */
  .narty-calculator__media { order: -1; aspect-ratio: 4 / 3; }
}

@media (min-width: 1024px) {
  .narty-calculator { padding: var(--space-80) 0; }
  .narty-calculator__panel { grid-template-columns: minmax(0, 1fr) 380px; align-items: stretch; }
  .narty-calculator__form {
    padding: var(--space-40);
    display: grid;
    /* The controls column is the fixed one: .stepper-row rows need a
       widget-sized measure to read correctly, and the includes list has
       the long items, so the slack belongs on the detail side. */
    grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
    gap: var(--space-40);
    align-items: start;
  }
  .narty-calculator__detail { margin-top: 0; }
}

/* Words-you'll-hear glossary (EN homepage only — see scripts/lib/glossary.js).
   Reuses existing surface/spacing/radius tokens, no new design-system
   additions needed. */
.words-youll-hear {
  background: var(--bg-sunken);
  border-radius: var(--radius-card);
  padding: var(--space-24);
  margin-top: var(--space-24);
}
.words-youll-hear__heading { font-size: 1rem; margin: 0 0 var(--space-8); }
.words-youll-hear__list { margin: 0; padding-left: 1.2em; color: var(--text-secondary); }
.words-youll-hear__list li { margin-bottom: var(--space-4); }
.words-youll-hear__list em { font-style: italic; color: var(--text-primary); }

/* ============================================
   COOKIE NOTICE BAR
   ============================================ */
.z4y-cookie-notice {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 250;
  background: var(--ink-900);
  color: var(--on-dark-soft);
  box-shadow: 0 -8px 24px -12px rgba(21, 26, 46, 0.45);
}
.z4y-cookie-notice__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  max-width: 1250px;
  margin: 0 auto;
  padding: var(--space-16) var(--space-24) calc(var(--space-16) + env(safe-area-inset-bottom));
}
.z4y-cookie-notice__actions {
  display: flex;
  gap: var(--space-8);
  flex-shrink: 0;
}
.z4y-cookie-notice__text {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.5;
  flex: 1 1 260px;
  min-width: 0;
}
.z4y-cookie-notice__text a { color: var(--accent-bright); text-decoration: underline; }
.z4y-cookie-notice__text a:hover { color: var(--on-dark); }
.z4y-cookie-notice__text a:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 2px; }
.z4y-cookie-notice__btn {
  flex-shrink: 0;
  font-family: "Outfit", "Outfit Fallback", sans-serif;
  font-weight: 600;
  font-size: 0.875rem;
  line-height: 1.2;
  padding: var(--space-8) var(--space-20);
  border-radius: var(--radius-pill);
  border: none;
  cursor: pointer;
  background: var(--accent-bright);
  color: var(--ink-900);
  transition: filter var(--duration-fast) var(--ease-in), transform var(--duration-fast) var(--ease-in);
}
.z4y-cookie-notice__btn:hover { filter: brightness(1.08); transform: translateY(-1px); }
.z4y-cookie-notice__btn:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 2px; }
.z4y-cookie-notice__btn--decline {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.4);
  color: var(--on-dark);
}
.z4y-cookie-notice__btn--decline:hover { background: rgba(255, 255, 255, 0.16); border-color: var(--on-dark); filter: none; transform: none; }
.z4y-cookie-notice__btn--decline:focus-visible { outline: 2px solid var(--on-dark); outline-offset: 2px; }
