/* ============================================================
   ANIJAM — Core design system
   Everything bespoke. Tailwind handles layout only.
   ============================================================ */

/* ---------- 1. TOKENS ---------- */
:root {
  /* Palette pulled straight from the logo */
  --jam:        #7B3F8C;
  --jam-bright: #A855C4;
  --jam-deep:   #3D1A47;
  --jam-ink:    #24102B;
  --crust:      #E0995A;
  --crust-dark: #A85A42;
  --cream:      #FFF6EA;
  --cream-dim:  #F3E4D2;
  --ink:        #180C1D;

  /* Rubik + Golos Text: стандартна кирилица (не български глифови форми) —
     Sofia Sans рендерираше д като g, к като k и т.н. и беше труден за четене */
  --font-display: 'Rubik', 'Golos Text', system-ui, sans-serif;
  --font-body:    'Golos Text', system-ui, sans-serif;
  --font-mono:    'JetBrains Mono', ui-monospace, monospace;

  /* Motion */
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out-quart: cubic-bezier(0.76, 0, 0.24, 1);
  --ease-back: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* Скала на радиусите — беше 8 различни стойности, набивани на ръка */
  --radius-sm:   12px;  /* дребни чипове, инпути */
  --radius-card: 24px;  /* всички карти/повърхности */
  --radius-lg:   28px;  /* билетът, големите панели */
  --radius-pill: 99px;  /* бутони, бейджове */

  /* Скала на времетраенията — беше 17 различни (вкл. и 0.3s, и 0.3s) */
  --dur-fast:  0.3s;   /* цвят, opacity, дребни състояния */
  --dur-base:  0.45s;  /* стандартен hover/transform */
  --dur-slow:  0.6s;   /* по-едри движения, reveal-и */
  --dur-slower: 0.9s;  /* влизане в кадър */

  --nav-h: 84px;
}

/* ---------- 2. RESET / BASE ---------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  /* Нативен скрол. Имаше lerp скролър, който преправяше wheel събитията —
     махнат: чупеше Firefox (deltaMode: 1 = редове, не пиксели), убиваше
     скрола във вложени контейнери и се биеше с anchor скрола. Нативният
     е точен за CSS scroll/view timelines и работи с всяко входно устройство. */
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--cream);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

::selection { background: var(--jam-bright); color: var(--cream); }

/* Scrollbar */
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: var(--jam-ink); }
::-webkit-scrollbar-thumb {
  background: linear-gradient(var(--jam), var(--crust));
  border-radius: var(--radius-pill);
}

/* ---------- 3. TYPE SCALE ---------- */
.display {
  font-family: var(--font-display);
  font-weight: 900;
  line-height: 1.04;
  letter-spacing: -0.015em;
  text-wrap: balance;
}
.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--crust);
}
/* Rubik е по-компактен от Unbounded — скалата може да диша повече */
.fluid-xl { font-size: clamp(3rem, 10.5vw, 10rem); }
/* капът е 4.8rem, а не 5.2 — „конвенция." преливаше от 5-колонния грид на 1440 */
.fluid-lg { font-size: clamp(2.2rem, 5.4vw, 4.8rem); }
.fluid-md { font-size: clamp(1.55rem, 3.6vw, 2.9rem); }

/* ---------- 4. TEXTURE LAYERS ---------- */
/* Film grain — animated, sits above everything, ignores pointer */
/* inset was -150%: 4x the viewport per axis = 16x the area, promoted to its own
   compositor layer by the infinite transform. On a 3x phone that is roughly
   190 MB of layer against 15 MB here — the same arithmetic (and the same fix)
   as the holding page in functions/_lib/comingsoon.js. The keyframes translate
   at most 4%, so a 10% overhang is provably enough. */
.grain {
  position: fixed;
  inset: -10%;
  z-index: 9998;
  pointer-events: none;
  opacity: 0.035; /* was .05 — grain over body text cost legibility */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grain-shift 0.7s steps(1) infinite;
}
@keyframes grain-shift {
  0%   { transform: translate(0, 0); }
  20%  { transform: translate(-3%, 2%); }
  40%  { transform: translate(2%, -4%); }
  60%  { transform: translate(-2%, -2%); }
  80%  { transform: translate(4%, 3%); }
  100% { transform: translate(0, 0); }
}

/* Reusable soft light bloom */
.bloom {
  position: absolute;
  border-radius: 50%;
  filter: blur(90px);
  pointer-events: none;
  opacity: 0.5;
}

/* ---------- 5. THEMED POINTER ----------
   Нативен `cursor: url()` вместо предишните JS кръгчета (.cursor-dot/.cursor-ring).
   Онези висяха на rAF с lerp 0.16 → пръстенът видимо изоставаше от истинския
   курсор; `cursor: none` беше само на body/a/button, така че над input/textarea
   се виждаха ДВА курсора; а pointerout от дете на връзка местеше is-hot и той
   мигаше. Нативният курсор няма нито един от тези проблеми: 0ms закъснение,
   рисува се от компоузитора, работи над input-и, iframe-и и извън прозореца.

   Стрелката е ANIJAM: остър ъглов силует, jam-bright пълнеж, crust контур и
   капка сладко на опашката. Hover → crust пълнеж с cream контур.
   Хотспотът е върхът на стрелката (2 2) и ТРЯБВА да съвпада с върха в SVG-то —
   ако движиш пътя, движи и числата. Fallback-ът след запетаята не е украса:
   Safari не поддържа SVG курсори и пада на нея. */
:root {
  --cursor-arrow: url('../img/cursor.svg') 2 2, auto;
  --cursor-hot:   url('../img/cursor-hot.svg') 2 2, pointer;
}
@media (hover: hover) and (pointer: fine) {
  body { cursor: var(--cursor-arrow); }

  a, button, summary, label,
  [data-hot], [role='button'],
  .surface--hoverable, [data-tilt] { cursor: var(--cursor-hot); }

  /* текстовите полета пазят системния I-beam — там курсорът е инструмент, не декор */
  input:not([type='checkbox']):not([type='radio']):not([type='submit']):not([type='button']),
  textarea, select { cursor: auto; }
  input[type='checkbox'], input[type='radio'],
  input[type='submit'], input[type='button'] { cursor: var(--cursor-hot); }

  :disabled, [aria-disabled='true'] { cursor: not-allowed; }
}

/* ---------- 6. PRELOADER ---------- */
.preloader {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: grid;
  place-items: center;
  background: var(--jam-ink);
  transition: opacity var(--dur-slow) ease, visibility var(--dur-slow);
}
.preloader.is-done { opacity: 0; visibility: hidden; }
.preloader__inner { text-align: center; width: min(390px, 74vw); }
.preloader__logo {
  width: 156px;
  margin: 0 auto 32px;
  animation: pl-bob 1.6s var(--ease-in-out-quart) infinite;
}
@keyframes pl-bob {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50%      { transform: translateY(-14px) rotate(3deg); }
}
.preloader__bar {
  height: 3px;
  background: rgba(255, 246, 234, 0.12);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.preloader__fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, var(--jam-bright), var(--crust));
  border-radius: var(--radius-pill);
  transition: width var(--dur-fast) ease;
}
.preloader__pct {
  margin-top: 14px;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.2em;
  color: var(--crust);
}

/* ---------- 7. SCROLL PROGRESS (jam bar) ---------- */
.scroll-jam {
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  width: 100%;
  z-index: 9997;
  transform-origin: 0 50%;
  transform: scaleX(0);
  background: linear-gradient(90deg, var(--crust), var(--jam-bright), var(--crust));
  /* No animation without a scroll timeline to drive it. Safari (all versions
     before 26) ignores animation-timeline, and the shorthand then leaves a
     0s animation with fill-mode both — which lands on the `to` keyframe
     immediately and paints the bar at scaleX(1) forever, reading as a stuck
     loading bar above the nav on every page. Hide it unless it can animate;
     .split-onscroll at section 11 already guards the same way. */
  display: none;
}
@supports (animation-timeline: scroll(root block)) {
  .scroll-jam {
    display: block;
    animation: jam-grow linear both;
    animation-timeline: scroll(root block);
  }
}
@keyframes jam-grow { to { transform: scaleX(1); } }

/* ---------- 8. NAV ---------- */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 900;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 clamp(1.25rem, 4vw, 3.5rem);
  transition: transform var(--dur-base) var(--ease-out-expo), background-color var(--dur-fast) ease, -webkit-backdrop-filter var(--dur-fast) ease, backdrop-filter var(--dur-fast) ease;
}
.nav.is-stuck {
  background: rgba(24, 12, 29, 0.72);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
          backdrop-filter: blur(18px) saturate(140%);
  border-bottom: 1px solid rgba(224, 153, 90, 0.16);
}
.nav.is-hidden { transform: translateY(-100%); }
.nav__brand { display: flex; align-items: center; gap: 12px; }
.nav__brand img {
  width: 44px;
  transition: transform var(--dur-base) var(--ease-back);
}
.nav__brand:hover img { transform: rotate(-12deg) scale(1.12); }
.nav__wordmark {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.05rem;
  letter-spacing: 0;
}
.nav__links { display: flex; align-items: center; gap: clamp(1rem, 2.5vw, 2.25rem); }
.nav__link {
  position: relative;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 6px 0;
  color: var(--cream-dim);
  transition: color var(--dur-fast) ease;
}
.nav__link::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  height: 2px; width: 100%;
  background: var(--crust);
  transform: scaleX(0);
  transform-origin: 100% 50%;
  transition: transform var(--dur-base) var(--ease-out-expo);
}
.nav__link:hover { color: var(--cream); }
.nav__link:hover::after { transform: scaleX(1); transform-origin: 0 50%; }
.nav__link.is-active { color: var(--crust); }
.nav__link.is-active::after { transform: scaleX(1); }

/* Езиков превключвател BG/EN */
.nav__lang {
  border: 1px solid rgba(255, 246, 234, 0.25);
  border-radius: var(--radius-pill);
  padding: 6px 13px;
  font-weight: 700;
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
.nav__lang::after { content: none; }
.nav__lang:hover { border-color: var(--crust); color: var(--crust); }

/* Mobile menu */
.nav__burger {
  display: none;
  width: 44px; height: 44px;
  position: relative;
  z-index: 950;
}
.nav__burger span {
  position: absolute;
  left: 10px;
  width: 24px; height: 2px;
  background: var(--cream);
  transition: transform var(--dur-fast) var(--ease-out-expo), opacity var(--dur-fast) ease;
}
.nav__burger span:nth-child(1) { top: 17px; }
.nav__burger span:nth-child(2) { top: 25px; }
.nav__burger.is-open span:nth-child(1) { transform: translateY(4px) rotate(45deg); }
.nav__burger.is-open span:nth-child(2) { transform: translateY(-4px) rotate(-45deg); }

.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 890;
  background: var(--jam-deep);
  display: grid;
  place-items: center;
  clip-path: circle(0% at calc(100% - 42px) 42px);
  transition: clip-path var(--dur-slow) var(--ease-in-out-quart);
  visibility: hidden;
  /* inset:0 alone sizes to the LAYOUT viewport on iOS, so with the toolbars
     expanded the bottom item ("English →") lands under the browser chrome. */
  height: 100vh;
  height: 100svh;
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.mobile-menu.is-open {
  clip-path: circle(150% at calc(100% - 42px) 42px);
  visibility: visible;
}
.mobile-menu ul { list-style: none; padding: 0; margin: 0; text-align: center; }
.mobile-menu li { overflow: hidden; }
.mobile-menu a {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.6rem, 7.5vw, 2.75rem);
  letter-spacing: 0;
  padding: 6px 0;
  transform: translateY(110%);
  transition: transform var(--dur-slow) var(--ease-out-expo), color var(--dur-fast) ease;
}
.mobile-menu.is-open a { transform: translateY(0); }
.mobile-menu.is-open li:nth-child(1) a { transition-delay: 0.12s; }
.mobile-menu.is-open li:nth-child(2) a { transition-delay: 0.18s; }
.mobile-menu.is-open li:nth-child(3) a { transition-delay: 0.24s; }
.mobile-menu.is-open li:nth-child(4) a { transition-delay: var(--dur-fast); }
.mobile-menu.is-open li:nth-child(5) a { transition-delay: 0.36s; }
.mobile-menu a:hover { color: var(--crust); }

@media (max-width: 860px) {
  .nav__links { display: none; }
  .nav__burger { display: block; }
}

/* ---------- 9. BUTTONS ---------- */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 18px 34px;
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: 0.84rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  overflow: hidden;
  isolation: isolate;
  will-change: transform;
}
.btn__label { position: relative; z-index: 2; transition: color var(--dur-fast) ease; }
.btn::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  transform: translateY(101%);
  transition: transform var(--dur-base) var(--ease-out-expo);
}
.btn:hover::before { transform: translateY(0); }

.btn--jam {
  background: var(--jam);
  color: var(--cream);
  box-shadow: 0 10px 40px -12px rgba(123, 63, 140, 0.9);
}
.btn--jam::before { background: var(--crust); }
.btn--jam:hover .btn__label { color: var(--ink); }

.btn--ghost {
  border: 1.5px solid rgba(255, 246, 234, 0.28);
  color: var(--cream);
}
.btn--ghost::before { background: var(--cream); }
.btn--ghost:hover .btn__label { color: var(--ink); }

.btn--crust {
  background: var(--crust);
  color: var(--ink);
}
.btn--crust::before { background: var(--jam-bright); }
.btn--crust:hover .btn__label { color: var(--cream); }

/* ---------- 10. TEXT REVEAL (split by JS) ---------- */
.split-line {
  /* inline-block: думите текат в естествени редове (block слагаше
     всяка дума на свой ред); клипваме само вертикално за reveal-а —
     overflow: hidden режеше дълги думи като „конвенция" хоризонтално */
  display: inline-block;
  overflow-y: clip;
  overflow-x: visible;
}
.split-word {
  display: inline-block;
  transform: translateY(110%);
  will-change: transform;
}
.split-ready .split-word {
  animation: word-rise 0.9s var(--ease-out-expo) both;
  animation-delay: calc(var(--i) * var(--stagger, 45ms));
}
/* hero заглавията се цепят по букви — по-стегната вълна */
.split-chars { --stagger: 55ms; }
@keyframes word-rise { to { transform: translateY(0); } }

/* Scroll-triggered variant */
@supports (animation-timeline: view()) {
  .split-onscroll .split-word {
    animation: word-rise 0.9s var(--ease-out-expo) both;
    animation-timeline: view();
    animation-range: entry 0% entry 70%;
    animation-delay: 0s;
  }
}

/* ---------- 11. GENERIC SCROLL REVEALS ---------- */
.reveal { opacity: 0; transform: translateY(48px); filter: blur(10px); }
.reveal.is-in {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition: opacity var(--dur-slower) var(--ease-out-expo), transform var(--dur-slower) var(--ease-out-expo),
              filter var(--dur-slower) var(--ease-out-expo);
  transition-delay: var(--d, 0s);
}
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; filter: none; }
}

/* ---------- 12. MARQUEE ---------- */
.marquee {
  display: flex;
  overflow: hidden;
  user-select: none;
  gap: 0;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.marquee__track {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 2.5rem;
  padding-right: 2.5rem;
  min-width: 100%;
  animation: marquee-run 28s linear infinite;
}
.marquee:hover .marquee__track { animation-play-state: paused; }
@keyframes marquee-run { to { transform: translateX(-100%); } }
.marquee__item {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.3rem, 3vw, 2.4rem);
  letter-spacing: 0;
  white-space: nowrap;
  display: flex;
  align-items: center;
  gap: 2.5rem;
}
.marquee__item .dot {
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--crust);
  flex-shrink: 0;
}
.marquee__item.is-outline {
  color: rgba(168, 85, 196, 0.28);
  -webkit-text-stroke: 2px var(--jam-bright);
}

/* ---------- 13. JAM DRIP DIVIDER ---------- */
.drip {
  display: block;
  width: 100%;
  height: clamp(54px, 7.5vw, 116px);
  /* Дрипът маркира ЕДИН преход: тъмна секция → тонирана секция. Плътната част
     е долу и се слива с тона отдолу, а вълната се качва нагоре в тъмното.

     `scaleY(-1)`: пътищата в четирите SVG-та са запълнени ОТГОРЕ (`M0,0 H1440
     V34 …`), тоест обърнати спрямо това как се ползват — плътната плоча падаше
     върху тъмната секция, а прозрачната половина стоеше върху тонираната. Оттам
     идваше „ненапълненият" splash: вълна, която виси, без сладко под нея.
     Огледалото е точно и се пише веднъж, вместо да се пренаписват на ръка осем
     безие пътя. Ако някога начертаеш пътя правилно — махни трансформацията и
     обърни знака на сянката. */
  transform: scaleY(-1);
  margin-bottom: -1px;
  position: relative;
  /* БЕЗ `z-index: 1`. Дрипът се вдигаше над следващата секция и блумовете ѝ
     (напр. crust блумът в CTA-то на Билети) спираха рязко на долния му ръб —
     пак хоризонтална черта. С естествения ред на рисуване секцията отдолу
     минава над вълната и светлината ѝ я облива, вместо да се реже в нея. */
  /* БЕЗ drop-shadow. Филтърът обливаше целия силует, включително правия долен
     ръб — а той съвпада с границата на елемента, така че ореолът се рисуваше
     ИЗВЪН кутията и се четеше като светла черта точно на спойката с тона отдолу.
     Проверено: с `filter:none` спойката е чиста. Ако някога поискаш ореол, той
     трябва да е само по гребена (отделен stroke по пътя), не сянка на формата. */
}

/* ---------- 14. PARALLAX RAIL (the headline feature) ---------- */
.rail-section {
  position: relative;
  height: 420vh;
}
/* Three explicit rows: heading / track / hint. Previously the heading was
   position:absolute at top:14vh while the cards were centred in a 100vh flex —
   two independent systems that collided at 1080p (6px between them) and left
   217px dead at the bottom. One grid owns the vertical rhythm now. */
.rail-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  row-gap: clamp(0.75rem, 2.2vh, 2rem);
  padding-block: clamp(4rem, 10vh, 7.5rem) clamp(1.5rem, 3.5vh, 2.75rem);
  overflow: hidden;
}
.rail-head {
  padding-inline: 8vw;
  z-index: 10;
  pointer-events: none;
}
.rail {
  /* card height drives width, so the track can never outgrow the viewport */
  --card-h: clamp(230px, min(58vh, 100vh - 340px), 600px);
  position: relative;
  align-self: center;
  display: flex;
  align-items: center;
  gap: clamp(1.25rem, 2.4vw, 2.75rem);
  padding-inline: 8vw;
  counter-reset: railcard;
  will-change: transform;
}
/* the thread of the day — only ever visible in the gaps between cards */
.rail::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 246, 234, 0.16) 5%,
                                      rgba(255, 246, 234, 0.16) 95%, transparent);
}
.rail__card {
  --accent: var(--crust);
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  height: var(--card-h);
  aspect-ratio: 3 / 4;
  counter-increment: railcard;
  border-radius: var(--radius-card);
  overflow: hidden;
  /* Riso comic block: flat deep fill, one thick accent frame per card, no
     gradient "photo" mush. Accent rotates crust / jam-bright / cream. */
  background: var(--jam-ink);
  border: 2px solid rgba(224, 153, 90, 0.44); /* fallback: color-mix is Safari 16.2+ */
  border: 2px solid color-mix(in srgb, var(--accent) 44%, transparent);
  transition: transform var(--dur-base) var(--ease-out-expo),
              border-color var(--dur-fast) ease;
}
/* 3-tone duotone rotation, read off the existing .ph-N class via :has() so the
   markup stays untouched and copy-export is unaffected. */
.rail__card:has(.ph-1), .rail__card:has(.ph-4), .rail__card:has(.ph-7) { --accent: var(--crust); }
.rail__card:has(.ph-2), .rail__card:has(.ph-5), .rail__card:has(.ph-8) { --accent: var(--jam-bright); }
.rail__card:has(.ph-3), .rail__card:has(.ph-6), .rail__card:has(.ph-9) { --accent: var(--cream); }
/* Was nth-child(even) translateY(56px). It read as broken alignment rather
   than rhythm — captions landed 56px apart on one baseline row. */
.rail__card:hover {
  transform: translateY(-10px);
  border-color: var(--accent);
}
/* index numeral, cropped by the card edge — set via counter so it stays out
   of the DOM and out of the copy-export extraction.
   Must be ::before: [data-glow]::after owns the cursor glow and would win. */
/* Small solid index tag, top-left — was a giant faint ghost numeral (a template
   tell). Now a printed-label mono chip in the card accent. */
.rail__card::before {
  content: counter(railcard, decimal-leading-zero);
  position: absolute;
  top: 16px; left: 18px;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.8rem;
  line-height: 1;
  letter-spacing: 0.12em;
  color: var(--accent);
  z-index: 4;
  pointer-events: none;
}
.rail__card figcaption {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 20px 20px 22px;
  /* time on top (hero), title beneath — DOM order is title→meta, so reverse */
  display: flex;
  flex-direction: column-reverse;
  gap: 2px;
  border-top: 1.5px solid rgba(224, 153, 90, 0.3); /* fallback: color-mix is Safari 16.2+ */
  border-top: 1.5px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: linear-gradient(transparent, rgba(18, 8, 22, 0.55));
  z-index: 3;
}
.rail__card .cap-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.02rem;
  line-height: 1.2;
  letter-spacing: 0;
  color: rgba(255, 246, 234, 0.88); /* fallback: color-mix is Safari 16.2+ */
  color: color-mix(in srgb, var(--cream) 88%, transparent);
}
/* the time is the hero of a schedule card */
.rail__card .cap-meta {
  font-family: var(--font-mono);
  font-size: 1.55rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  text-transform: none;
  color: var(--accent);
  margin: 0;
}
.rail__hint {
  justify-self: center;
  align-self: end;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 246, 234, 0.68);
  z-index: 5;
  pointer-events: none;
}

/* Procedural "photo" fills — no external images, all CSS */
.ph {
  position: absolute;
  inset: 0;
  z-index: 1;
  background-blend-mode: overlay, normal;
}
.ph::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.42'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
  opacity: 0.5;
}
.ph-1 { background: radial-gradient(circle at 30% 25%, #C77DDF, transparent 55%), linear-gradient(150deg, #7B3F8C, #24102B); }
.ph-2 { background: radial-gradient(circle at 70% 30%, #E0995A, transparent 52%), linear-gradient(200deg, #A85A42, #3D1A47); }
.ph-3 { background: radial-gradient(circle at 45% 70%, #A855C4, transparent 58%), linear-gradient(120deg, #3D1A47, #7B3F8C); }
.ph-4 { background: radial-gradient(circle at 60% 20%, #FFD9A8, transparent 50%), linear-gradient(170deg, #E0995A, #7B3F8C); }
.ph-5 { background: radial-gradient(circle at 25% 60%, #7B3F8C, transparent 55%), linear-gradient(30deg, #24102B, #A855C4); }
.ph-6 { background: radial-gradient(circle at 75% 65%, #E0995A, transparent 55%), linear-gradient(220deg, #7B3F8C, #24102B); }
.ph-7 { background: radial-gradient(circle at 40% 40%, #C77DDF, transparent 60%), linear-gradient(90deg, #A85A42, #3D1A47); }
.ph-8 { background: radial-gradient(circle at 55% 75%, #A855C4, transparent 50%), linear-gradient(300deg, #E0995A, #24102B); }
.ph-9 { background: radial-gradient(circle at 35% 30%, #C77DDF, transparent 55%), linear-gradient(250deg, #3D1A47, #24102B); }

/* Rail cards override the gradient "photo" with a printed halftone-dot field in
   the card accent, masked to bleed from the top-right corner. Higher specificity
   than .ph-N so it wins inside the rail; other pages keep their .ph gradients. */
.rail__card .ph {
  background-color: transparent;
  background-image: radial-gradient(var(--accent) 21%, transparent 24%);
  background-size: 12px 12px;
  background-position: 0 0;
  opacity: 0.42;
  -webkit-mask-image: radial-gradient(125% 95% at 82% 4%, #000 0%, transparent 60%);
          mask-image: radial-gradient(125% 95% at 82% 4%, #000 0%, transparent 60%);
}
.rail__card .ph::after { content: none; }  /* kill the fractal-noise overlay */

/* Лента с факти под билета (Билети).
   Беше ред, залепен вляво в карта на цялата ширина — две трети празно. Сега
   всеки факт държи равна колона и се чете отгоре надолу: число, после етикет,
   точно както броячът по-долу на страницата. Един и същ похват = една система.

   Разделителите са ПРОСВЕТВАЩИЯТ фон през 1px фугите на грида, а не отделен
   елемент. Затова работят сами: вертикални хайрлайни на широк екран,
   хоризонтални щом колоните се пренесат — без media query и без празен
   <span>, който да виси накрая на реда. */
.factbar {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1px;
  border: 1px solid rgba(255, 246, 234, 0.14);
  border-radius: 1.25rem;
  overflow: hidden;
  background: rgba(255, 246, 234, 0.14);
}
.factbar__item {
  padding: clamp(1.35rem, 3.2vw, 2rem) clamp(1.25rem, 3vw, 2rem);
  text-align: center;
  background: linear-gradient(180deg, rgba(255, 246, 234, 0.055), rgba(255, 246, 234, 0.018));
}
.factbar__n {
  display: block;
  color: var(--crust);
  font-size: clamp(2.6rem, 6vw, 3.9rem);
  line-height: 0.9;
}
/* Етикетът НЕ е в цвета на числото: два еднакво ярки реда се четат като едно
   петно. Числото свети, етикетът се дръпва назад. */
.factbar__label {
  display: block;
  margin-top: 0.8rem;
  color: rgba(255, 246, 234, 0.62);
  /* Кирилицата е широка: „от отваряне до последната песен" опира ръбовете на
     390px екран. Смъква се с една идея, вместо да се пренася на два реда. */
  font-size: clamp(0.68rem, 2.6vw, 0.8rem);
}

/* Silhouette layer that floats inside each card for depth */
.ph-figure {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  opacity: 0.9;
}
.ph-figure svg { width: 62%; height: auto; }

/* ---------- 15a. PICTOGRAM SYSTEM ----------
   One stroke weight, round caps, no fills. Replaces the eight near-identical
   avatar silhouettes (circle head + dome body) that read as stock filler. */
.card__art {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  padding-bottom: 24%;          /* sit in the upper-middle, clear of the caption */
  pointer-events: none;
}
.card__art svg {
  width: 44%;
  height: auto;
  fill: none;
  stroke: var(--accent);
  stroke-width: 4.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---------- 15b. "TO BE ANNOUNCED" ----------
   An empty lit stage, not an invented person. The old placeholder drew a
   faceless body, which both looked generic and implied a guest we have not
   booked — every guest here is deliberately unannounced. */
.tba {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.tba svg {
  width: 54%;
  height: auto;
  fill: none;
  stroke: rgba(255, 246, 234, 0.34);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* the spotlight waiting on the floor */
.tba::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 14%;
  width: 46%;
  height: 13%;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(ellipse at center,
              rgba(255, 246, 234, 0.38), rgba(255, 246, 234, 0.06) 62%, transparent 74%);
  filter: blur(5px);
}

/* ---------- 15. CARDS / SURFACES ---------- */
.surface {
  position: relative;
  border-radius: var(--radius-card);
  background: linear-gradient(160deg, rgba(255, 246, 234, 0.07), rgba(255, 246, 234, 0.02));
  border: 1px solid rgba(255, 246, 234, 0.1);
  -webkit-backdrop-filter: blur(8px);
          backdrop-filter: blur(8px);
  overflow: hidden;
}
.surface--hoverable {
  transition: transform var(--dur-base) var(--ease-out-expo),
              border-color var(--dur-fast) ease;
}
/* Повдигането важи само за карти БЕЗ tilt. При [data-tilt] JS-ът пише inline
   transform и двете се биеха за едно и също свойство: картата подскачаше -8px
   при hover и падаше обратно щом мръднеш мишката. Там повдигането е в матрицата. */
.surface--hoverable:not([data-tilt]):hover { transform: translateY(-8px); }
.surface--hoverable:hover { border-color: rgba(224, 153, 90, 0.45); }

/* Animated conic border via @property */
@property --edge {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}
.edge-glow {
  position: relative;
  isolation: isolate;
}
.edge-glow::before {
  content: '';
  position: absolute;
  inset: -1.5px;
  z-index: -1;
  border-radius: inherit;
  background: conic-gradient(from var(--edge),
    transparent 0deg, var(--jam-bright) 60deg, var(--crust) 120deg, transparent 200deg, transparent 360deg);
  animation: edge-spin 5s linear infinite;
  opacity: 0;
  transition: opacity var(--dur-base) ease;
}
.edge-glow:hover::before { opacity: 1; }
@keyframes edge-spin { to { --edge: 360deg; } }

/* ---------- 16. TILT ---------- */
/* Закачено за [data-tilt], а НЕ за клас .tilt: JS-ът чете атрибута, а нито един
   елемент нямаше класа — затова preserve-3d не се прилагаше (и .tilt__layer беше
   сплескан), а липсата на transition правеше накланянето „щракащо". */
[data-tilt] {
  transform-style: preserve-3d;
  transition: transform var(--dur-base) var(--ease-out-expo);
}
[data-tilt].is-tilting {
  transition: transform 0.1s linear;
  will-change: transform; /* само докато се накланя — иначе държи слой за нищо */
}
.tilt__layer { transform: translateZ(40px); }

/* ---------- 17. FORM ---------- */
/* Labels sit ABOVE the control and stay there. The previous version floated the
   label into the box, which is Material Design's filled text field — rounded
   top, underline bottom. Nothing else on this site is shaped like that: the
   page is pills, ticket stubs and fully-rounded cards, so a form built from a
   different vocabulary reads as borrowed. It also left every empty field
   looking half-full, because the label was parked where the value belongs, and
   a floating label stops being readable the moment somebody starts typing. */
.field { position: relative; }
.field__label {
  display: block;
  margin-bottom: 9px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 246, 234, 0.58);
  transition: color var(--dur-fast) ease;
}
/* The label warms up with its own field — cheap, and it makes a long form feel
   like it is tracking you rather than sitting there. */
.field:focus-within .field__label { color: var(--crust); }
.field.has-error .field__label { color: #FF9E9E; }

.field input, .field textarea, .field select {
  width: 100%;
  min-height: 52px;
  padding: 14px 16px;
  background: rgba(255, 246, 234, 0.045);
  border: 1px solid rgba(255, 246, 234, 0.16);
  border-radius: 14px;
  color: var(--cream);
  font-family: var(--font-body);
  font-size: 1rem;
  outline: none;
  /* iOS draws a residual inner shadow inside a custom-radius field otherwise.
     Tailwind's preflight only resets appearance for buttons. */
  -webkit-appearance: none;
          appearance: none;
  transition: border-color var(--dur-fast) ease,
              background-color var(--dur-fast) ease,
              box-shadow var(--dur-fast) ease;
}

/* Safari/iOS Keychain autofill paints its own pale background and it cannot be
   overridden with background-color — only with an inset shadow. Without this,
   cream text lands on a near-white ground and the booker cannot read back the
   email address the whole double-opt-in flow depends on. */
.field input:-webkit-autofill,
.field input:-webkit-autofill:hover,
.field input:-webkit-autofill:focus,
.field textarea:-webkit-autofill {
  -webkit-text-fill-color: var(--cream);
  -webkit-box-shadow: 0 0 0 1000px #241329 inset;
  caret-color: var(--cream);
  /* The colour is applied via a transition Safari never finishes, which is the
     only reliable way to defeat the yellow flash on repaint. */
  transition: background-color 9999s ease-out 0s;
}
.field textarea { min-height: 150px; resize: vertical; }
.field input:hover, .field textarea:hover { border-color: rgba(255, 246, 234, 0.28); }

/* A real ring, not a hue change on a hairline. WCAG 2.2 asks for focus you
   cannot miss, and 1.5px going from grey to orange is missable — especially
   against this background, and especially for the person who most needs it. */
.field input:focus, .field textarea:focus, .field select:focus {
  border-color: var(--crust);
  background: rgba(255, 246, 234, 0.075);
  box-shadow: 0 0 0 3px rgba(224, 153, 90, 0.2);
}
.field.has-error input, .field.has-error textarea {
  border-color: #FF6B6B;
  background: rgba(255, 107, 107, 0.05);
}
.field.has-error input:focus, .field.has-error textarea:focus {
  box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.2);
}

/* The consent checkbox sits in a .field as well, but it is a visually-hidden
   input behind a custom box (.check__box) — none of the text-input chrome above
   belongs on it, and without this it quietly inherited a 52px height and a
   focus ring nobody could see. */
.field input[type="checkbox"], .field input[type="radio"] {
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}

/* Errors carry a glyph as well as a colour: colour alone is not an indicator
   for the ~8% of men who cannot rely on it. */
.field__err {
  display: flex;
  align-items: flex-start;
  gap: 7px;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  letter-spacing: 0.04em;
  line-height: 1.45;
  color: #FF9E9E;
  margin-top: 8px;
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  transition: opacity var(--dur-fast) ease, max-height var(--dur-fast) ease;
}
.field__err::before {
  content: '!';
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin-top: 1px;
  border-radius: 50%;
  border: 1px solid currentColor;
  font-size: 0.62rem;
  font-weight: 700;
  line-height: 13px;
  text-align: center;
}
.field.has-error .field__err { max-height: 4em; opacity: 1; }
/* Standalone error slots (form-level, Turnstile) have no .field wrapper to flip
   has-error — they show whenever they carry text. */
.field__err:not(:empty) { max-height: 4em; opacity: 1; }
.field__err:empty::before { content: none; }

/* ---------- 18. UTILITIES ---------- */
/* A11y skip link — visually hidden until keyboard-focused */
.skip-link {
  position: fixed;
  top: 10px; left: 10px;
  z-index: 10001;
  padding: 12px 20px;
  border-radius: var(--radius-pill);
  background: var(--crust);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 700;
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease-out-expo);
}
.skip-link:focus-visible { transform: translateY(0); }

/* Keyboard focus — one visible ring for everything clickable. Inputs keep
   their own border treatment (.field input:focus); checkbox has its own. */
a:focus-visible, button:focus-visible, summary:focus-visible, [tabindex="0"]:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 3px;
  border-radius: 4px;
}
/* Visually-hidden copy (split headings keep a readable twin for AT). */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

.wrap { width: min(1240px, 92vw); margin-inline: auto; }
.wrap-wide { width: min(1600px, 94vw); margin-inline: auto; }
.stroke-text {
  /* faint fill under the outline — pure transparent was unreadable over busy areas */
  color: rgba(255, 246, 234, 0.12);
  /* -webkit-text-stroke draws CENTRED on the glyph outline, so half of it goes
     INWARD. Rubik at 900 has very tight Cyrillic counters (compare б, и, л, е
     with their latin cousins) and a 2.5px stroke closed them outright: „билет"
     rendered with slivers and notches instead of holes, which read as broken
     letterforms rather than as a heavy font. Fixed by giving the counters room
     — weight 700, not 900 — and a thinner stroke. The word is set at the same
     size as its solid neighbour, so the lighter weight is invisible; only the
     counters change. Do not push the weight back up without re-checking
     Cyrillic. */
  font-weight: 700;
  -webkit-text-stroke: 2px var(--cream);
  /* No paint-order: it changes nothing in Chromium for HTML text, but Firefox
     honours it and would paint the translucent fill OVER the stroke's inner
     half, so the two engines disagreed for no benefit. */
}
.jam-underline {
  background-image: linear-gradient(var(--crust), var(--crust));
  background-repeat: no-repeat;
  /* Thickness in em, not px: this class sits on 15px footer links (40 of them)
     and on 24px display text (8). A fixed 8px was tuned for the big ones and on
     the small ones drew a bar straight THROUGH the letters — it read as a
     highlighter or a strikethrough, not an underline. 0.22em keeps the chunky
     jam-swipe weight at display size and scales down to a proper rule on body
     text. Anchored at 100%, i.e. the bottom of the content box, so it clears
     descenders instead of crossing the baseline like 92% did. */
  background-position: 0 100%;
  background-size: 0% 0.22em;
  transition: background-size var(--dur-slow) var(--ease-out-expo);
}
.jam-underline:hover { background-size: 100% 0.22em; }

.section { padding-block: clamp(6rem, 14vw, 11rem); position: relative; }

/* Deliberately NOT content-visibility:auto. The intrinsic-size estimate differs
   from these sections' real heights, so rendering one mid-scroll shifted
   everything below it — jumping the scrollbar and leaving reveals unfired.
   The pages are small enough that the paint saving wasn't worth the jank. */
.perf-contain { contain: layout style; }

/* ---------- 18a. FUSED BANDS ----------
   Tinted sections used to be flat slabs of --jam-deep butted against the --ink
   body, so zoomed out the page read as separate stacked blocks with hard
   horizontal seams. The tint now fades in and out at its own edges, so
   neighbouring sections bleed into each other instead of tiling. */
/* Footer Instagram card — was a bare mono label over a text link, which read as
   leftover debris once the other five networks came out. It is the one channel
   we actually have, so it gets presented as an actual card. */
.ig-card {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 13px 18px 13px 13px;
  border-radius: var(--radius-card);
  border: 1px solid rgba(255, 246, 234, 0.14);
  background: linear-gradient(150deg, rgba(255, 246, 234, 0.07), rgba(255, 246, 234, 0.02));
  transition: border-color var(--dur-fast) ease, background var(--dur-fast) ease;
}
.ig-card:hover {
  border-color: rgba(224, 153, 90, 0.5);
  background: linear-gradient(150deg, rgba(255, 246, 234, 0.11), rgba(255, 246, 234, 0.03));
}
.ig-card__icon {
  flex: 0 0 auto;
  width: 40px; height: 40px;
  display: grid;
  place-items: center;
  border-radius: 13px;
  color: var(--cream);
  background: linear-gradient(135deg, var(--jam-bright), var(--crust));
}
.ig-card__icon svg { width: 21px; height: 21px; }
.ig-card__text { display: flex; flex-direction: column; line-height: 1.3; }
.ig-card__label {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(255, 246, 234, 0.62);
}
.ig-card__handle {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--cream);
}
.ig-card__go {
  margin-left: 10px;
  color: var(--crust);
  transition: transform var(--dur-fast) var(--ease-out-expo);
}
.ig-card:hover .ig-card__go { transform: translate(3px, -3px); }

.band { position: relative; isolation: isolate; }
.band::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(180deg,
              transparent 0%,
              var(--jam-deep) 15%,
              var(--jam-deep) 85%,
              transparent 100%);
}
/* a band that opens the page or runs into the footer only needs one soft edge */
.band--top::before    { background: linear-gradient(180deg, var(--jam-deep) 70%, transparent 100%); }
.band--bottom::before { background: linear-gradient(180deg, transparent 0%, var(--jam-deep) 30%); }

/* ---------- 18b. ГРАНИЦИ МЕЖДУ БАНДИТЕ ----------
   `.band` се появява от прозрачно през първите 15% и изтънява през последните
   15%. Това е правилно само когато отгоре и отдолу има тъмна секция. На две
   места не беше:

   1) Под дрип. Плътната част на вълната опира в секция, която в този момент е
      още прозрачна — вълната висеше над голия --ink вместо да е гребен на
      тонираната зона. Точно „ненапълненият" splash.
   2) Две тонирани секции една след друга (`#reserve` → `#included` на Билети)
      се събираха с изтъняване срещу поява и между тях се получаваше тъмно
      прищипване, все едно някой е забравил да оцвети ивица.

   Съседни селектори, за да не се преписва в 6 файла и да не се забрави при нов
   дрип. `:has()` е чист прогресивен ъпгрейд: без него се връща старото
   преливане — по-слабо, но не счупено. */
.drip + .band::before {
  background: linear-gradient(180deg,
              var(--jam-deep) 0%,
              var(--jam-deep) 85%,
              transparent 100%);
}
.band:has(+ .band)::before {
  background: linear-gradient(180deg,
              transparent 0%,
              var(--jam-deep) 15%,
              var(--jam-deep) 100%);
}
.band + .band::before {
  background: linear-gradient(180deg,
              var(--jam-deep) 0%,
              var(--jam-deep) 85%,
              transparent 100%);
}
/* Секция, обградена от тонирани съседи, е плътна от край до край. */
.band + .band:has(+ .band)::before { background: var(--jam-deep); }

/* Floating idle animation */
.float { animation: float-idle 7s ease-in-out infinite; }
.float--slow { animation-duration: 11s; }
.float--delay { animation-delay: -3.5s; }
@keyframes float-idle {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-22px) rotate(4deg); }
}

/* Gooey filter host */
.goo { filter: url(#goo-filter); }

/* ---------- 19. MICRO-INTERACTIONS ---------- */
/* блясък, който минава през бутона при hover */
.btn::after {
  content: '';
  position: absolute;
  top: -20%; bottom: -20%;
  left: -80%;
  width: 55%;
  z-index: 1;
  transform: skewX(-22deg);
  background: linear-gradient(90deg, transparent, rgba(255, 246, 234, 0.28), transparent);
  transition: left var(--dur-slow) var(--ease-out-expo);
  pointer-events: none;
}
.btn:hover::after { left: 130%; }

/* ореол, следващ курсора върху карти — JS пише --gx/--gy */
[data-glow] { position: relative; }
[data-glow]::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-fast) ease;
  background: radial-gradient(320px circle at var(--gx, 50%) var(--gy, 50%),
              rgba(232, 163, 79, 0.18), transparent 65%);
}
[data-glow]:hover::after { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .split-word { transform: none !important; }
}

/* ---------- 20. JAM FLIP CARD ----------
   Hover a guest card and it becomes a slice of toast: the bread drops in, the
   ANIJAM jam splats across it, then the whole card flips over and its own photo
   and text come back on the bread. The jam ends up face-down, which is why the
   back is plain crumb — that is how a flipped slice actually works.

   The bread and jam are the real logo, traced to vectors by tools/logo/trace.mjs
   and pulled in as one <use> each from bread-sprite.svg, so eight cards on a page
   cost one copy of the geometry.

   Everything is a transition, never a keyframe. Keyframes restart from zero when
   you re-enter a card mid-sequence; transitions interpolate from wherever the
   value currently is, so flicking the pointer across a row of cards stays smooth.
   Each property therefore declares its timing twice — once in the resting rule
   (the way out) and once under :hover (the way in). ---------------- */

@property --ajm-splat {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

.jam-flip {
  --ajm-k: 1.25;                                /* one knob for the whole sequence */
  --ajm-cover: 1.5;                             /* contain to cover, JS per card */
  --ajm-e-pop: cubic-bezier(0.24, 1.36, 0.44, 1);
  --ajm-e-flip: cubic-bezier(0.62, 0.02, 0.28, 1);
  --ajm-e-splat: cubic-bezier(0.18, 0.9, 0.32, 1);
  position: relative;
  perspective: 1500px;
}

/* .surface carries backdrop-filter: blur(8px), which has to re-blur the whole
   card every frame of the turn. Measured on the guest grid it is the single
   biggest cost in the sequence (31.8 -> 36.3 fps with it off) and it buys
   nothing here: the cards sit on a flat band, so blurring that backdrop changes
   no pixel by more than 6/255, and a second later the bread covers it anyway.
   Scoped to .is-jam-flip, which JS only sets on cards it actually upgraded. */
.is-fine-pointer .is-jam-flip:hover,
.is-jam-flip:focus-within,
.is-jam-flip.is-held {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* ---- the turn ---- */
.jam-flip__inner {
  position: relative;
  transform-style: preserve-3d;
  transition: transform calc(380ms * var(--ajm-k)) var(--ajm-e-flip) calc(0ms * var(--ajm-k));
}
.is-fine-pointer .jam-flip:hover .jam-flip__inner,
.jam-flip:focus-within .jam-flip__inner,
.is-jam-flip.is-held .jam-flip__inner {
  transform: rotateY(180deg);
  transition: transform calc(520ms * var(--ajm-k)) var(--ajm-e-flip) calc(560ms * var(--ajm-k));
}

.jam-flip__face {
  border-radius: inherit;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.jam-flip__face--front { position: relative; z-index: 1; }
.jam-flip__face--back {
  position: absolute;
  inset: 0;
  overflow: hidden;
  transform: rotateY(180deg);
}

/* ---- the card's own content, stepping aside for the bread ---- */
.jam-flip__content {
  visibility: visible;
  transition: opacity calc(300ms * var(--ajm-k)) ease calc(260ms * var(--ajm-k)),
              transform calc(380ms * var(--ajm-k)) var(--ease-out-expo) calc(260ms * var(--ajm-k)),
              visibility 0s linear 0s;
}
.is-fine-pointer .jam-flip:hover .jam-flip__face--front .jam-flip__content,
.jam-flip:focus-within .jam-flip__face--front .jam-flip__content,
.is-jam-flip.is-held .jam-flip__face--front .jam-flip__content {
  opacity: 0;
  transform: scale(0.965);
  /* Stop painting it once it has finished fading — it stays in the layer tree
     for the whole turn otherwise, and by then the bread covers it completely.
     The visibility step is delayed to land exactly when the fade ends. */
  visibility: hidden;
  transition: opacity calc(200ms * var(--ajm-k)) ease 0ms,
              transform calc(380ms * var(--ajm-k)) var(--ease-out-expo) 0ms,
              visibility 0s linear calc(200ms * var(--ajm-k));
}

/* ---- the slice ---- */
.jam-flip__bread {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: grid;
  place-items: center;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.68) rotate(-10deg);
  transition: opacity calc(220ms * var(--ajm-k)) ease calc(300ms * var(--ajm-k)), transform calc(320ms * var(--ajm-k)) var(--ajm-e-pop) calc(300ms * var(--ajm-k));
}
.is-fine-pointer .jam-flip:hover .jam-flip__bread,
.jam-flip:focus-within .jam-flip__bread,
.is-jam-flip.is-held .jam-flip__bread {
  opacity: 1;
  transform: none;
  transition: opacity calc(200ms * var(--ajm-k)) ease calc(0ms * var(--ajm-k)), transform calc(380ms * var(--ajm-k)) var(--ajm-e-pop) calc(0ms * var(--ajm-k));
}

/* Ratio box locked to the artwork, so the mask percentages below land in the
   same frame as the drawing however the card itself is proportioned. */
.jam-flip__art {
  position: relative;
  width: 100%;
  transition: transform calc(380ms * var(--ajm-k)) var(--ajm-e-flip) calc(0ms * var(--ajm-k));
}
.jam-flip__art::before { content: ''; display: block; padding-top: var(--ajm-ratio, 111.6034%); }
.jam-flip__art > * { position: absolute; inset: 0; width: 100%; height: 100%; }
.jam-flip__loaf,
.jam-flip__jam svg { display: block; width: 100%; height: 100%; }
/* The slice grows from contain to cover on the turn - motivated by the flip
   bringing it towards you, and it lands full-bleed for the back face. */
.is-fine-pointer .jam-flip:hover .jam-flip__art,
.jam-flip:focus-within .jam-flip__art,
.is-jam-flip.is-held .jam-flip__art {
  transform: scale(var(--ajm-cover));
  transition: transform calc(520ms * var(--ajm-k)) var(--ajm-e-flip) calc(560ms * var(--ajm-k));
}

.jam-flip__crumbs {
  transform-box: fill-box;
  transform-origin: 50% 50%;
  opacity: 0;
  transform: translate(-16px, -12px) scale(0.35) rotate(-20deg);
  transition: opacity calc(180ms * var(--ajm-k)) ease calc(260ms * var(--ajm-k)), transform calc(260ms * var(--ajm-k)) ease calc(260ms * var(--ajm-k));
}
.is-fine-pointer .jam-flip:hover .jam-flip__crumbs,
.jam-flip:focus-within .jam-flip__crumbs,
.is-jam-flip.is-held .jam-flip__crumbs {
  opacity: 1;
  transform: none;
  transition: opacity calc(200ms * var(--ajm-k)) ease calc(150ms * var(--ajm-k)), transform calc(440ms * var(--ajm-k)) var(--ajm-e-pop) calc(150ms * var(--ajm-k));
}

/* ---- the splat ----
   Seven ellipses over the lettering, each opening from nothing as --ajm-splat
   climbs. The black stop trails the transparent one by 30%, so at 0% the solid
   band has no extent at all and the mask is genuinely empty - running the stops
   the other way round leaves a blob of jam showing at rest. */
.jam-flip__jam {
  --ajm-splat: 0%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-composite: source-over;
          mask-composite: add;
  -webkit-mask-image:
    radial-gradient(50% 37% at 52% 18%, #000 calc(var(--ajm-splat) - 30%), #0000 var(--ajm-splat)),
    radial-gradient(47% 35% at 27% 35%, #000 calc(var(--ajm-splat) - 34%), #0000 calc(var(--ajm-splat) - 4%)),
    radial-gradient(47% 35% at 77% 33%, #000 calc(var(--ajm-splat) - 34%), #0000 calc(var(--ajm-splat) - 4%)),
    radial-gradient(44% 34% at 43% 57%, #000 calc(var(--ajm-splat) - 40%), #0000 calc(var(--ajm-splat) - 10%)),
    radial-gradient(44% 34% at 71% 63%, #000 calc(var(--ajm-splat) - 44%), #0000 calc(var(--ajm-splat) - 14%)),
    radial-gradient(41% 32% at 32% 75%, #000 calc(var(--ajm-splat) - 48%), #0000 calc(var(--ajm-splat) - 18%)),
    radial-gradient(41% 32% at 61% 79%, #000 calc(var(--ajm-splat) - 52%), #0000 calc(var(--ajm-splat) - 22%));
          mask-image:
    radial-gradient(50% 37% at 52% 18%, #000 calc(var(--ajm-splat) - 30%), #0000 var(--ajm-splat)),
    radial-gradient(47% 35% at 27% 35%, #000 calc(var(--ajm-splat) - 34%), #0000 calc(var(--ajm-splat) - 4%)),
    radial-gradient(47% 35% at 77% 33%, #000 calc(var(--ajm-splat) - 34%), #0000 calc(var(--ajm-splat) - 4%)),
    radial-gradient(44% 34% at 43% 57%, #000 calc(var(--ajm-splat) - 40%), #0000 calc(var(--ajm-splat) - 10%)),
    radial-gradient(44% 34% at 71% 63%, #000 calc(var(--ajm-splat) - 44%), #0000 calc(var(--ajm-splat) - 14%)),
    radial-gradient(41% 32% at 32% 75%, #000 calc(var(--ajm-splat) - 48%), #0000 calc(var(--ajm-splat) - 18%)),
    radial-gradient(41% 32% at 61% 79%, #000 calc(var(--ajm-splat) - 52%), #0000 calc(var(--ajm-splat) - 22%));
  transition: --ajm-splat calc(240ms * var(--ajm-k)) ease calc(200ms * var(--ajm-k));
}
.is-fine-pointer .jam-flip:hover .jam-flip__jam,
.jam-flip:focus-within .jam-flip__jam,
.is-jam-flip.is-held .jam-flip__jam {
  --ajm-splat: 132%;
  transition: --ajm-splat calc(400ms * var(--ajm-k)) var(--ajm-e-splat) calc(200ms * var(--ajm-k));
}

/* The jam lands slightly high and wide, then settles. The mask sits on the
   wrapper rather than here, so the drawing spreads underneath it instead of
   sliding along with it. */
.jam-flip__jam svg {
  transform: translateY(-5%) scaleX(1.06) scaleY(0.9);
  transform-origin: 50% 30%;
  transition: transform calc(240ms * var(--ajm-k)) ease calc(200ms * var(--ajm-k));
}
.is-fine-pointer .jam-flip:hover .jam-flip__jam svg,
.jam-flip:focus-within .jam-flip__jam svg,
.is-jam-flip.is-held .jam-flip__jam svg {
  transform: none;
  transition: transform calc(460ms * var(--ajm-k)) var(--ajm-e-pop) calc(200ms * var(--ajm-k));
}

/* ---- the back: crumb side up ---- */
.jam-flip__face--back {
  --ajm-slice: #F6E7D1;                /* warm crumb, not the logo's paper white */
  --ajm-crust: #DFA061;
  color: #2E1633;
  /* The slice is an irregular shape, so covering a rectangular card still
     leaves the corners short. Crust underneath means those corners read as more
     bread rather than as the dark card showing through a hole. */
  background: radial-gradient(130% 110% at 50% 38%, #E3A566, #AE6D3D);
}
.jam-flip__back-bread {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.jam-flip__back-bread svg { display: block; width: 100%; height: 100%; }
/* The front card is edge-to-edge on a dark ground; on bread the same content has
   to sit inside the slice with crumb showing around it, and the portrait has to
   give back enough height for the text to fit without spilling off the card. */
.jam-flip__face--back .jam-flip__content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 18px;
}
/* Takes whatever height the text leaves rather than a fixed ratio — the eight
   cards carry different amounts of copy, and this way none of them overflow the
   slice or leave a slab of bare crumb under the paragraph. */
.jam-flip__face--back .guest__portrait {
  flex: 1 1 auto;
  aspect-ratio: auto;
  min-height: 0;
  border-radius: 13px;
  overflow: hidden;
  border: 3px solid rgba(255, 250, 242, 0.9);
  box-shadow: 0 10px 24px rgba(60, 28, 20, 0.28);
}
/* The text used to sit straight on the slice, where the ground swings from
   cream crumb to dark crust shadow underneath a single line — the colour was
   never the problem, the backdrop was. Giving it its own paper label fixes the
   contrast by making it constant, and reads as a tag stuck on the toast. */
.jam-flip__face--back .p-6 {
  margin-top: 11px;
  padding: 13px 15px 15px;
  border-radius: 13px;
  background: rgba(255, 250, 242, 0.95);
  border: 1px solid rgba(120, 70, 40, 0.14);
  box-shadow: 0 8px 20px rgba(60, 28, 20, 0.22);
}
.jam-flip__face--back .display { color: #2E1633; }        /* 15:1 on the label */
.jam-flip__face--back .eyebrow { color: #6E3480; }        /* 8:1 */
.jam-flip__face--back p {
  color: rgba(46, 22, 51, 0.88) !important;               /* 11:1 */
  font-size: 0.84rem;
  line-height: 1.5;
  margin-top: 0.6rem;
}
.jam-flip__face--back .booth-no,
.jam-flip__face--back .tag {
  background: rgba(46, 22, 51, 0.82);
  border-color: rgba(255, 246, 234, 0.28);
  color: #FFF6EA;
}

/* ---- concealed until opened ----
   A confirmed guest keeps their face hidden behind a decoy panel, and the flip
   is what pays it off: photos and name only exist on the crumb side.

   The swap only happens once JS has actually built the flip. Without it — no
   script, reduced motion — .guest__cover stays hidden and the card is just the
   guest, because a reveal nobody can trigger is worse than no reveal.

   The real block is clipped, NOT display:none. The back face is an inert clone
   the a11y tree never sees, so display:none here would put her name out of reach
   of a screen reader entirely and take it off the page for crawlers with it.
   Clipped means sighted visitors get the game and everyone else just gets read
   the answer. */
.guest__cover { display: none; }
.is-jam-flip .jam-flip__face--front .guest__cover { display: block; }

/* The decoy is one of her own photos with the detail destroyed in the file — the
   asset that ships is 96px wide and under 2 kB, so there is nothing to recover
   by saving it. Enough of a figure survives to read as a person standing there.

   Resolution and blur trade against each other here. Crushing harder (48px) and
   blurring more hid her better but read as a smear; a wider source with a light
   bake and a lighter CSS blur reads as a photograph out of focus, which is what
   it should look like. */
.guest__veil { background: var(--jam-ink); }
.guest__veil-shot {
  position: absolute;
  /* overscanned, or the blur leaves a soft transparent rim on all four sides */
  top: -8%;
  left: -8%;
  width: 116%;
  height: 116%;
  object-fit: cover;
  filter: blur(7px);
}
/* Pulls the photo back onto the site's palette and darkens it enough for the
   glyph to sit on top. Under .tba (z-index 2), over the photo. */
.guest__veil::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    radial-gradient(115% 85% at 50% 20%, rgba(168, 85, 196, 0.4), transparent 64%),
    linear-gradient(180deg, rgba(36, 16, 43, 0.42), rgba(36, 16, 43, 0.8));
}
.guest__veil .tba svg { stroke: rgba(255, 246, 234, 0.5); }
.guest__veil .tba::after { content: none; }   /* the photo already lights itself */
.is-jam-flip .jam-flip__face--front .guest__reveal {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Nothing to look at in a 1px box — the reel only earns its frames on the back. */
.jam-flip__face--front { --ajm-reel-run: paused; }

/* ---- press and hold, for the pointers that cannot hover ----
   Touch runs the identical sequence off .is-held, which JS puts on the card for
   as long as a finger is down. The hover selectors above are gated behind
   .is-fine-pointer for a reason: mobile browsers latch :hover onto the last
   thing you tapped, and without the gate a tap would leave a card stuck
   face-down until you tapped something else. */
.jam-flip__hint {
  position: absolute;
  z-index: 3;                      /* under the bread, which is 4 */
  top: 14px;
  left: 14px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 12px;
  border-radius: 99px;
  font-family: var(--font-mono);
  font-size: 0.56rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  background: rgba(24, 12, 29, 0.72);
  border: 1px solid rgba(224, 153, 90, 0.35);
  color: var(--crust);
  pointer-events: none;
  transition: opacity 0.35s ease, transform 0.35s var(--ease-out-expo);
}
.jam-flip__hint::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--crust);
  animation: ajm-hold-pulse 1.9s ease-in-out infinite;
}
@keyframes ajm-hold-pulse {
  0%, 100% { opacity: 0.3; transform: scale(0.65); }
  50%      { opacity: 1;   transform: scale(1); }
}
/* Says it once. After the first hold anywhere on the page the visitor knows, and
   a row of chips repeating the instruction is just noise. */
.is-fine-pointer .is-jam-flip:hover .jam-flip__hint,
.is-jam-flip.is-held .jam-flip__hint,
.ajm-held-once .jam-flip__hint {
  opacity: 0;
  transform: translateY(-4px);
}

/* A long press on a photo otherwise raises the save-image callout on iOS and a
   text selection on Android, both of which land right on top of the reveal. */
@media (hover: none) {
  .is-jam-flip {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
            user-select: none;
  }
}

/* Nothing here is load-bearing: hold and hover are both optional ways in, and
   reduced-motion users get the plain card. JS enforces it, this is the
   belt-and-braces half. */
@media (prefers-reduced-motion: reduce) {
  .jam-flip__inner { transform: none !important; }
  .jam-flip__bread { display: none; }
  .jam-flip__hint { display: none; }
  .jam-flip__face--front .jam-flip__content { opacity: 1 !important; transform: none !important; visibility: visible !important; }
}

/* ---------- 21. GUEST PHOTO REEL ----------
   A confirmed guest gets photographs where an unannounced one gets the empty
   stage (.tba). The card has room for exactly one portrait and every shot is
   worth showing, so four share the frame and cross-fade.

   Deliberately pure CSS. jamFlip() clones the card's content onto the bread, and
   a cloned CSS animation keeps step with the original for free; a JS slideshow
   would have to find and drive both copies, and drift the moment one of them was
   created a frame later. One keyframe set drives both the photos and the dots.

   It idles until the card has been scrolled to at least once — .is-in comes from
   initReveals() and is never taken off again, so this only stops the reel from
   burning frames on a section the visitor has not reached yet. ------------- */
.reel {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--jam-ink);
}
.reel__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* per-shot, inline: these are four different crops of one person and the
     centre of a frame is rarely where her face is */
  object-position: 50% var(--y, 50%);
  opacity: 0;
  animation: ajm-reel var(--reel-dur, 15s) linear infinite;
  animation-play-state: var(--ajm-reel-run, paused);
}
/* Negative delays, so the cycle is already in its steady state on the first
   frame — positive ones would leave shots 2-4 blank for up to 11s. */
.reel__shot:nth-child(2) { animation-delay: calc(var(--reel-dur, 15s) * -0.75); }
.reel__shot:nth-child(3) { animation-delay: calc(var(--reel-dur, 15s) * -0.5); }
.reel__shot:nth-child(4) { animation-delay: calc(var(--reel-dur, 15s) * -0.25); }

/* The fade has to finish exactly on the quarter, not straddle it. A paused reel
   rests at wall-clock zero, where the other three sit 25/50/75% into their own
   cycles — and with the fade running past 25% the shot on that mark rests at 40%
   opacity, so a closed card would wait on a double exposure. Ending at 25% puts
   it at a clean nothing, and out-20-to-25 against in-95-to-100 still overlaps
   exactly, so the cross-fade itself is unchanged.

   It also buys the reveal a full 20% hold — the flip takes ~1.35s of that, and
   what is left is the beat where you actually look at the first photo. */
@keyframes ajm-reel {
  0%, 20%  { opacity: 1; }
  25%, 95% { opacity: 0; }
  100%     { opacity: 1; }
}

/* Four ticks, so it reads as a set of photos rather than one that keeps
   changing. Not buttons: anything clickable in the card makes jamFlip() skip it,
   because the flip would put the control face-down. */
.reel__dots {
  position: absolute;
  z-index: 4;
  left: 14px;
  bottom: 14px;
  display: flex;
  gap: 6px;
  pointer-events: none;
}
.reel__dots i {
  position: relative;
  width: 17px;
  height: 3px;
  border-radius: 99px;
  background: rgba(255, 246, 234, 0.3);
  box-shadow: 0 1px 3px rgba(24, 12, 29, 0.5);
}
/* the lit half is its own layer, so the tick animates on opacity like the photo
   above it instead of repainting a background-color */
.reel__dots i::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--crust);
  opacity: 0;
  animation: ajm-reel var(--reel-dur, 15s) linear infinite;
  animation-play-state: var(--ajm-reel-run, paused);
}
.reel__dots i:nth-child(2)::after { animation-delay: calc(var(--reel-dur, 15s) * -0.75); }
.reel__dots i:nth-child(3)::after { animation-delay: calc(var(--reel-dur, 15s) * -0.5); }
.reel__dots i:nth-child(4)::after { animation-delay: calc(var(--reel-dur, 15s) * -0.25); }

/* When to actually turn. Written as one inherited switch on the card rather than
   as competing `animation-play-state` rules, so the three cases read in order
   instead of fighting over specificity. :where() keeps the first two at zero.

   On a flip card the reel is part of the payoff, so it waits at photo one until
   the card is opened. On a plain card — no JS, reduced motion — there is nothing
   to open, so it just runs once you have scrolled to it. */
:where(.reveal.is-in) { --ajm-reel-run: running; }
:where(.is-jam-flip) { --ajm-reel-run: paused; }
.is-fine-pointer .is-jam-flip:hover,
.is-jam-flip.is-held { --ajm-reel-run: running; }

/* On the bread the portrait is a rounded window in the crumb, and a photo that
   cross-fades against dark card ink would flash dark there. */
.jam-flip__face--back .reel { background: #C98A55; }

/* Ези sent three photos, not four. Same 3.75s-a-shot cadence on a shorter
   11.25s cycle, with the quarter rule above transposed to thirds: the fade ends
   exactly on the third, so a paused reel (shots 2 and 3 rest 66.7%/33.3% into
   their own cycles) still waits at a clean single photo. */
.reel--3 .reel__shot,
.reel__dots--3 i::after {
  animation-name: ajm-reel-3;
  animation-duration: 11.25s;
}
.reel--3 .reel__shot:nth-child(2),
.reel__dots--3 i:nth-child(2)::after { animation-delay: calc(11.25s * -2 / 3); }
.reel--3 .reel__shot:nth-child(3),
.reel__dots--3 i:nth-child(3)::after { animation-delay: calc(11.25s * -1 / 3); }
@keyframes ajm-reel-3 {
  0%, 26.667%      { opacity: 1; }
  33.333%, 93.333% { opacity: 0; }
  100%             { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .reel__shot,
  .reel--3 .reel__shot,
  .reel__dots i::after,
  .reel__dots--3 i::after { animation: none; }
  .reel__shot:first-child,
  .reel__dots i:first-child::after { opacity: 1; }
}

/* ---------- 22a. HERO VIEWPORT HEIGHT ----------
   tw.css emits only `.min-h-screen{min-height:100vh}` and no dvh/svh variant.
   On iOS Safari 100vh is the LARGE viewport — the height with the toolbars
   collapsed — so on first load the hero is 60-115px taller than what is
   actually visible and the countdown and the "Скролни" cue sit below the fold.
   svh is the small viewport, which is what a full-height hero wants. Declared
   after tw.css so this wins; iOS 15.0-15.3 drops the line and keeps the 100vh
   above it. Same vh-then-svh pattern as .rail-sticky in section 12. */
.min-h-screen { min-height: 100svh; }

/* ---------- 23. TOUCH: UNLATCH HOVER, ADD PRESS FEEDBACK ----------
   Mobile Safari latches :hover onto the last element you touched and keeps it
   latched until you touch something else. Every decorative hover in this sheet
   therefore reads as a permanent state change on a phone: schedule cards stay
   lifted, the marquee stays stopped, and the submit button keeps its filled
   "hovered" look after every tap.

   The jam-flip sequence already solves this with .is-fine-pointer (section 20)
   and the stepper with @media (hover: hover) in tickets.css. This block does
   the same for the rest of the sheet in one place rather than gating twenty
   rules individually. Everything below is an unwind to the element's own
   resting value — no new styling, nothing that can reach a fine pointer. */
@media (hover: none) {
  .marquee:hover .marquee__track { animation-play-state: running; }

  .rail__card:hover { transform: none; border-color: rgba(224, 153, 90, 0.44); }
  .surface--hoverable:not([data-tilt]):hover { transform: none; }
  .surface--hoverable:hover { border-color: rgba(255, 246, 234, 0.1); }
  .ig-card:hover {
    border-color: rgba(255, 246, 234, 0.14);
    background: linear-gradient(150deg, rgba(255, 246, 234, 0.07), rgba(255, 246, 234, 0.02));
  }
  .ig-card:hover .ig-card__go { transform: none; }

  /* The fill and the sheen both park mid-animation otherwise, and the label
     colour must unwind with them or it is ink-on-jam and unreadable. */
  .btn:hover::before { transform: translateY(101%); }
  .btn:hover::after { left: -80%; }
  .btn--jam:hover .btn__label,
  .btn--ghost:hover .btn__label { color: var(--cream); }
  .btn--crust:hover .btn__label { color: var(--ink); }

  .nav__brand:hover img { transform: none; }
  .nav__link:hover { color: var(--cream-dim); }
  .nav__link:not(.is-active):hover::after { transform: scaleX(0); }
  .nav__lang:hover { border-color: rgba(255, 246, 234, 0.25); color: inherit; }
  .mobile-menu a:hover { color: inherit; }

  .jam-underline:hover { background-size: 0% 0.22em; }
  [data-glow]:hover::after { opacity: 0; }
  .edge-glow:hover::before { opacity: 0; }
  .field input:hover, .field textarea:hover { border-color: rgba(255, 246, 234, 0.16); }

  /* Tailwind's preflight sets -webkit-tap-highlight-color: transparent on
     <html>, and no .btn has an :active state — so with the hover fill unwound
     above, tapping the submit button gives literally no feedback until the
     network round trip starts. On a slow mobile connection that gap is what
     produces double submits. */
  .btn:active { transform: scale(0.97); }
  .btn:active .btn__label { opacity: 0.75; }
  .ig-card:active, .rail__card:active, .surface--hoverable:active { transform: scale(0.99); }
}
