/* =========================================================================
   NIBIZA — Sunday afternoon Ibiza vibes
   Vanilla CSS. No framework. Two system font stacks for hierarchy.
   Edit the custom properties below to re-skin the whole site.
   ========================================================================= */

:root {
  /* --- Sunset palette --- */
  --sand:        #fff6ec;
  --sand-deep:   #ffe9d4;
  --ink:         #2a1a22;
  --ink-soft:    #6b5560;
  --coral:       #ff784f;
  --coral-deep:  #ff5a8a;
  --coral-text:  #bd3b1f;  /* darker coral for small text on light bg — passes AA (~5.4:1) */
  --amber:       #ffb24d;
  --dusk:        #3a2d52;
  --dusk-deep:   #221836;

  /* --- Typography: distinct display vs body, both dependency-free --- */
  --font-display: "Young Serif", "Hoefler Text", "Iowan Old Style", Palatino,
                  Georgia, "Times New Roman", serif;
  --font-body:    ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
                  "Helvetica Neue", Arial, sans-serif;

  /* --- Scale & misc --- */
  --maxw: 1120px;
  --gutter: clamp(1.25rem, 4vw, 3rem);
  --radius: 18px;
  --shadow: 0 24px 60px -28px rgba(58, 24, 12, 0.45);
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* Self-hosted display face — single 27KB woff2, no external request.
   Young Serif ships one weight (400); display elements use it at 400. */
@font-face {
  font-family: "Young Serif";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/young-serif-latin.woff2") format("woff2");
}

/* --------------------------------------------------------- Reset-ish */
*, *::before, *::after { box-sizing: border-box; }
/* scroll-padding-top is what keeps an anchor jump clear of the fixed nav. The
   nav is out of flow, so without this the browser aligns the target's top edge
   with the viewport's and the nav paints over it. 5.5rem = 88px against a nav
   that computes to 71.2px (0.85rem padding x2 + the 2.75rem language button,
   its tallest child), leaving ~17px of air. Sections with the full
   clamp(4rem, 9vw, 7rem) top padding only ever survived this by accident, and
   the two carrying a trimmed top padding did not survive it at all. */
html { scroll-behavior: smooth; scroll-padding-top: 5.5rem; -webkit-text-size-adjust: 100%; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

body {
  margin: 0;
  font-family: var(--font-body);
  color: var(--ink);
  background: var(--sand);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }
a { color: inherit; }

h1, h2 { font-family: var(--font-display); font-weight: 400; line-height: 1.05; letter-spacing: -0.01em; margin: 0; }
em { font-style: italic; }

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

/* Visually hidden but announced. Used by the language-switch live region that
   main.js appends to <body>. Not display:none -- that removes it from the
   accessibility tree and nothing would be read out. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -999px; top: 0; z-index: 100;
  background: var(--ink); color: #fff; padding: 0.6rem 1rem; border-radius: 0 0 8px 0;
}
.skip-link:focus { left: 0; }

/* --------------------------------------------------------- Buttons */
.btn {
  --b: var(--ink);
  display: inline-flex; align-items: center; gap: 0.5ch;
  font-weight: 600; font-size: 0.98rem; text-decoration: none;
  padding: 0.85rem 1.5rem; border-radius: 999px; border: 1.5px solid transparent;
  cursor: pointer; transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease), background 0.25s;
  white-space: nowrap;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }
/* Ink, not white: #fff on --coral is 2.61:1 and fails AA badly. Ink clears
   6.34:1 at the coral end and 5.58:1 at the coral-deep end, so the brand ramp
   itself is untouched -- and it matches .totop, which already carries ink over
   this exact gradient. */
.btn--solid {
  background: linear-gradient(120deg, var(--coral), var(--coral-deep));
  color: var(--ink); box-shadow: 0 14px 30px -12px rgba(255, 90, 95, 0.7);
}
.btn--solid:hover { box-shadow: 0 18px 38px -12px rgba(255, 90, 95, 0.85); }
.btn--ghost { background: rgba(255,255,255,0.5); border-color: rgba(42,26,34,0.18); color: var(--ink); }
.btn--ghost:hover { background: #fff; }
.btn[disabled] { opacity: 0.6; cursor: progress; transform: none; box-shadow: none; }

/* --coral-deep reads at only 2.52-2.97:1 on the cream surfaces and fails the
   3:1 a focus indicator owes. --coral-text clears 4.69:1 on the deepest of
   them. The dark bands need the opposite treatment, so they override below. */
:focus-visible { outline: 3px solid var(--coral-text); outline-offset: 3px; border-radius: 4px; }
.hero :focus-visible,
.pagehead :focus-visible,
.footer :focus-visible,
.nav:not(.is-stuck) :focus-visible { outline-color: var(--sand); }

/* --------------------------------------------------------- Nav */
.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 50;
  transition: background 0.3s var(--ease), box-shadow 0.3s var(--ease);
  padding-block: 0.85rem;
}
/* Scrim so the white wordmark/links stay legible over any part of the hero photo.
   The controls sit roughly mid-bar, so a gradient that has already faded to
   transparent by then buys them nothing: it holds most of its weight through the
   control band and only finishes fading below the bar. Extended 1.5rem past the
   bottom edge so that tail falls outside the buttons rather than across them.
   Fades out once the bar gets its own solid background on scroll. */
.nav::before {
  content: ""; position: absolute; inset: 0 0 -1.5rem 0; z-index: -1; pointer-events: none;
  background: linear-gradient(to bottom,
    rgba(20, 10, 18, 0.78) 0%,
    rgba(20, 10, 18, 0.62) 58%,
    rgba(20, 10, 18, 0.28) 82%,
    rgba(20, 10, 18, 0) 100%);
  transition: opacity 0.3s var(--ease);
}
.nav.is-stuck {
  background: var(--sand);
  border-bottom: 1px solid rgba(42, 26, 34, 0.12);
  box-shadow: 0 6px 24px -16px rgba(42, 26, 34, 0.5);
}
.nav.is-stuck::before { opacity: 0; }
.nav__inner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.nav__brand {
  font-family: var(--font-display); font-weight: 400; font-size: 1.4rem;
  letter-spacing: 0.04em; text-decoration: none; color: #fff;
  display: inline-flex; align-items: center; gap: 0.5ch;
  /* Was 35.8px tall. The wordmark is a link to the top of the page. */
  min-height: 2.75rem;
}
.nav__mark { color: var(--coral); }
.nav__menu { display: flex; align-items: center; gap: clamp(1rem, 2.4vw, 2rem); }
.nav__menu a { text-decoration: none; font-weight: 500; color: #fff; position: relative; }
/* Over the dark hero the bar is transparent (light text); once stuck on sand, flip to ink. */
.nav.is-stuck .nav__brand, .nav.is-stuck .nav__menu a:not(.nav__cta) { color: var(--ink); }
.nav__menu a:not(.nav__cta)::after {
  content: ""; position: absolute; left: 0; bottom: -4px; height: 2px; width: 100%;
  background: var(--coral); transform: scaleX(0); transform-origin: left;
  transition: transform 0.25s var(--ease);
}
.nav__menu a:not(.nav__cta):hover::after,
.nav__menu a:not(.nav__cta).is-current::after { transform: scaleX(1); }
.nav.is-stuck .nav__menu a:not(.nav__cta).is-current { color: var(--coral-text); }
/* --sand on --coral is only 2.44:1, so these two states swap the label to ink
    (6.34:1) rather than keeping the light text they inherit. */
.nav__cta.is-current { background: var(--coral); color: var(--ink) !important; }
.nav__cta {
  background: var(--ink); color: var(--sand) !important; padding: 0.55rem 1.1rem;
  border-radius: 999px; transition: background 0.25s;
}
.nav__cta:hover { background: var(--coral); color: var(--ink) !important; }

/* Right-hand cluster: links + language toggle + mobile hamburger. */
.nav__end { display: flex; align-items: center; gap: clamp(1rem, 2.4vw, 2rem); }

/* Language switcher (NL / EN). Light over the hero, ink once the bar is stuck. */
.lang { display: inline-flex; align-items: center; gap: 0.15rem; }
.lang__btn {
  background: none; border: 0; cursor: pointer; font: inherit;
  font-weight: 600; font-size: 0.82rem; letter-spacing: 0.06em;
  /* Was opacity 0.62: white at 62% over the hero photograph measured ~1.9:1,
     and this is 13px text at the one control an English visitor needs. 0.88 plus
     the nav scrim clears AA over every region of the photo. */
  color: #fff; opacity: 0.88; line-height: 1;
  text-shadow: 0 1px 3px rgba(20, 10, 18, 0.55);
  /* Was 28x21px -- under even the WCAG 2.2 AA 24x24 floor. The grid keeps the
     label optically centred while the box carries a real target. */
  display: grid; place-items: center; min-width: 2.75rem; min-height: 2.75rem;
  transition: opacity 0.2s var(--ease), color 0.2s var(--ease);
}
.lang__btn:hover { opacity: 1; }
.lang__btn.is-active { opacity: 1; color: var(--coral); }
.lang__sep { color: #fff; opacity: 0.55; font-size: 0.82rem; user-select: none; }
.nav.is-stuck .lang__btn { color: var(--ink); }
.nav.is-stuck .lang__sep { color: var(--ink); }
.nav.is-stuck .lang__btn.is-active { color: var(--coral-text); }

/* 38x28 before: the control that opens the whole mobile nav was the smallest
   target on the page. The bars keep their size; the box grows around them. */
.nav__toggle {
  display: none; flex-direction: column; justify-content: center; align-items: center;
  gap: 5px; background: none; border: 0; cursor: pointer;
  padding: 0; min-width: 2.75rem; min-height: 2.75rem;
}
.nav__toggle span { width: 26px; height: 2px; background: #fff; border-radius: 2px; transition: transform 0.3s var(--ease), opacity 0.2s, background 0.2s; }
.nav.is-stuck .nav__toggle span { background: var(--ink); }
/* When the panel is open it sits behind the toggle (a light surface) → dark bars. */
.nav__toggle[aria-expanded="true"] span { background: var(--ink); }
.nav__toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav__toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* --------------------------------------------------------- Hero */
.hero {
  position: relative; min-height: 100svh; display: flex; align-items: center;
  color: #fff; overflow: hidden; background: var(--dusk-deep);
}
/* Real golden-hour photo carries the warmth; the veil keeps the sunset tint
   AND a left-side scrim so white headline copy stays legible (AA). */
.hero__photo {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover; object-position: center 40%;
}
.hero__veil {
  position: absolute; inset: 0; z-index: 1;
  background:
    linear-gradient(90deg, rgba(20,10,18,0.68) 0%, rgba(20,10,18,0.32) 48%, rgba(20,10,18,0) 88%),
    radial-gradient(120% 95% at 50% 122%, rgba(255,178,77,0.48) 0%, rgba(255,120,79,0.42) 34%, rgba(58,45,82,0.60) 82%, rgba(34,24,54,0.85) 100%);
}
.hero__sun {
  position: absolute; left: 50%; bottom: -38vmin; transform: translateX(-50%); z-index: 1;
  width: 78vmin; height: 78vmin; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,243,214,0.9) 0%, rgba(255,217,138,0.6) 38%, rgba(255,178,77,0) 70%);
  filter: blur(2px); opacity: 0.85; animation: rise 14s var(--ease) infinite alternate;
}
@keyframes rise { from { transform: translate(-50%, 4vmin); } to { transform: translate(-50%, -4vmin); } }
/* The blanket reduce rule only collapses the duration, which parks an alternating
   animation at whichever frame it reached. Pin the sun to its resting position
   instead -- same fix .totop::before carries. */
@media (prefers-reduced-motion: reduce) {
  .hero__sun { animation: none; transform: translate(-50%, 0); }
}
.hero__inner { position: relative; z-index: 2; padding-block: 8rem 6rem; max-width: 760px; }
.hero__eyebrow {
  text-transform: uppercase; letter-spacing: 0.28em; font-size: 0.78rem; font-weight: 600;
  margin: 0 0 1rem; opacity: 0.9;
}
/* overflow-wrap is the backstop: body has overflow-x:hidden, so any word too
   long for the viewport would be silently clipped rather than visibly broken. */
.hero__title {
  font-size: clamp(2.8rem, 8vw, 5.5rem); overflow-wrap: break-word;
  text-shadow: 0 8px 40px rgba(42,16,8,0.25);
  /* Only meaningful below the 900px nowrap breakpoint, where the headline
     actually wraps. Above it the single line makes balancing a no-op. */
  text-wrap: balance;
}
/* Wide screens: at the 5.5rem cap the headline is wider than .hero__inner's
   760px text column, and running past it is the composition — so the soft
   hyphen in "Zondag&shy;namiddagen," must NOT break there. Below this width the
   column is the viewport, where that break is what keeps the word on screen. */
@media (min-width: 900px) { .hero__title { white-space: nowrap; } }
.hero__title em { color: #fff; font-style: italic; }
.hero__lead { font-size: clamp(1.05rem, 1.8vw, 1.3rem); max-width: 48ch; margin: 1.5rem 0 2.2rem; opacity: 0.95; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.9rem; }
/* Sound toggle. Lighter than the two pills beside it — it is a utility, not a
   third call to action — and the bars move only while audio is actually on, so
   the control states what it is doing rather than labelling it. */
.soundbtn {
  display: inline-flex; align-items: center; gap: 0.55rem;
  padding: 0.62rem 1.15rem 0.62rem 0.95rem;
  font: inherit; font-size: 0.92rem; font-weight: 600; color: #fff;
  background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.36);
  border-radius: 999px; cursor: pointer;
  transition: background 0.25s var(--ease), border-color 0.25s var(--ease);
}
.soundbtn:hover { background: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.6); }
.soundbtn__bars { display: flex; align-items: flex-end; gap: 2px; height: 0.85rem; }
/* At rest the bars still read as an equalizer, not as an ellipsis. */
.soundbtn__bars i { width: 3px; height: 45%; border-radius: 1.5px; background: currentColor; }
.soundbtn__bars i:nth-child(2) { height: 100%; }
.soundbtn__bars i:nth-child(3) { height: 68%; }
.soundbtn[aria-pressed="true"] .soundbtn__bars i { animation: eq 0.62s var(--ease) infinite alternate; }
.soundbtn[aria-pressed="true"] .soundbtn__bars i:nth-child(2) { animation-duration: 0.44s; animation-delay: 0.1s; }
.soundbtn[aria-pressed="true"] .soundbtn__bars i:nth-child(3) { animation-duration: 0.78s; animation-delay: 0.2s; }
@keyframes eq { from { height: 22%; } to { height: 100%; } }
@media (prefers-reduced-motion: reduce) {
  .soundbtn[aria-pressed="true"] .soundbtn__bars i { animation: none; height: 100%; }
}
/* The player itself. Off-screen rather than display:none, which some browsers
   treat as a reason to suspend the audio. */
.soundframe {
  position: absolute; width: 1px; height: 1px;
  left: -9999px; top: auto; overflow: hidden; opacity: 0; pointer-events: none;
}
/* Was 13x36px. The glyph was also a bare ▾, which falls back to a missing-glyph
   box in fonts without U+25BE; it is an inline SVG now, like .totop's arrow. */
.hero__scroll {
  position: absolute; left: 50%; bottom: 1.6rem; transform: translateX(-50%); z-index: 2;
  display: grid; place-items: center;
  width: 2.75rem; height: 2.75rem;
  color: #fff; text-decoration: none; opacity: 0.85;
  animation: bob 2s ease-in-out infinite;
}
/* Below the events breakpoint the three hero actions stack, and at that width a
   pill-on-pill-on-pill reads as three equal calls to action -- the primary CTA
   stops being singular. The utility drops its pill here and becomes a text +
   equalizer control, which is what it always was on desktop by contrast. */
@media (max-width: 719px) {
  .soundbtn {
    padding: 0.5rem 0.2rem; background: none; border-color: transparent;
    text-decoration: underline; text-underline-offset: 4px;
    text-decoration-color: rgba(255, 255, 255, 0.45);
    min-height: 2.9rem;
  }
  .soundbtn:hover { background: none; border-color: transparent; text-decoration-color: #fff; }
}
@keyframes bob { 0%,100% { transform: translate(-50%, 0); } 50% { transform: translate(-50%, 8px); } }

/* --------------------------------------------------------- Sections */
.section { padding-block: clamp(4rem, 9vw, 7rem); }
/* Trims a section's own vertical padding. For a section sitting between bands
   that already carry padding of their own — the page head above, the tinted CTA
   below — the default section padding doubles the gap at both seams. */
.section--tight { padding-block: clamp(1.8rem, 4vw, 3rem); }
/* The same trim at one seam only, for two untinted sections that meet with no
   change of background: the doubled padding there reads as a void rather than a
   break. Applied as a pair -- the section above ends tight, the one below starts
   tight -- so the gap closes evenly instead of drifting off the spacing scale. */
.section--tight-end { padding-block-end: clamp(1.8rem, 4vw, 3rem); }
.section--tight-start { padding-block-start: clamp(1.8rem, 4vw, 3rem); }
.section--tint { background: var(--sand-deep); }
.section__title { font-size: clamp(1.9rem, 4.4vw, 3rem); text-wrap: balance; }
.section__head { max-width: 40ch; margin-bottom: 3rem; }
/* No .section__eyebrow rule here on purpose. Uppercase tracked kickers above
   section headings are banned by the One-Eyebrow Rule in DESIGN.md -- the hero
   keeps the site's single eyebrow (.hero__eyebrow). Don't re-add this class. */
.section__grid { display: grid; gap: clamp(1.5rem, 5vw, 4rem); grid-template-columns: 1fr; }
@media (min-width: 820px) { .section__grid { grid-template-columns: 0.9fr 1.1fr; align-items: start; } }

/* --------------------------------------------------------- About */
/* A low sun sits behind the headline — same radial motif as .hero__sun, dialled
   right down. It fills the empty half of the left column without adding an image. */
.about { position: relative; overflow: hidden; }
.about::before {
  content: ""; position: absolute; pointer-events: none;
  left: clamp(-9rem, -6vw, -2rem); top: 1rem;
  width: min(32rem, 62vw); aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,178,77,0.30) 0%, rgba(255,120,79,0.13) 46%, rgba(255,120,79,0) 72%);
}
/* Positioned, so they paint over the absolutely-positioned glow above. */
.about__intro, .about__copy { position: relative; }
/* Short headline against long copy: centring the two reads better than top-aligning. */
@media (min-width: 820px) { .about .section__grid { align-items: center; } }

/* Copy left, photo right. Wider text column than the shared 0.9fr/1.1fr default,
   because the photo carries itself at a smaller size. */
@media (min-width: 820px) { .about .section__grid { grid-template-columns: 1.1fr 0.9fr; } }
/* Headline and copy stack in one column now, so they need their own rhythm. */
.about__text { position: relative; display: grid; gap: clamp(1.2rem, 3vw, 1.8rem); }
.about__photo {
  position: relative; margin: 0;
  border-radius: calc(var(--radius) + 6px); overflow: hidden;
  box-shadow: var(--shadow);
}
/* Portrait source, cropped to 4:5 so a tall photo cannot stretch the section. */
.about__photo img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; }

.about__copy p { margin: 0 0 1.1rem; font-size: 1.08rem; }
.about__copy p:last-child { margin-bottom: 0; }

/* Closing strip: three cards spanning the full container, each capped with the
   sunset gradient. Values are set in the display face; labels reuse the
   small-caps idiom used by .roster__role. */
.about__facts {
  list-style: none; padding: 0; margin: clamp(2.6rem, 5vw, 3.8rem) 0 0;
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
.about__facts li {
  position: relative; overflow: hidden;
  background: #fff; border-radius: var(--radius);
  padding: 1.5rem 1.4rem 1.6rem;
  box-shadow: 0 18px 44px -32px rgba(58,24,12,0.5);
  font-family: var(--font-display); font-size: 1.12rem; line-height: 1.35;
}
.about__facts li::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
  background: linear-gradient(90deg, var(--coral), var(--amber));
}
.about__facts strong {
  display: block; margin-bottom: 0.5rem;
  font-family: var(--font-body); font-size: 0.7rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.14em; color: var(--coral-text);
}

/* --------------------------------------------------------- Page head (subpages) */
/* Subpages have no photo hero, but the fixed nav is white-on-transparent until it
   sticks — so every subpage opens on a dark band to keep the bar legible. */
.pagehead {
  position: relative; overflow: hidden; color: #fff; background: var(--dusk-deep);
  padding-block: clamp(7.5rem, 15vh, 10.5rem) clamp(3rem, 7vh, 4.5rem);
}
.pagehead::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(115% 135% at 50% 128%, rgba(255,243,214,0.55) 0%, rgba(255,178,77,0.48) 26%,
              rgba(255,120,79,0.40) 48%, rgba(58,45,82,0.72) 78%, rgba(34,24,54,0.92) 100%);
}
.pagehead .container { position: relative; }
.pagehead__title {
  font-family: var(--font-display); font-weight: 400; line-height: 1.05; letter-spacing: -0.01em;
  font-size: clamp(2.4rem, 6vw, 4rem); margin: 0; text-wrap: balance;
  text-shadow: 0 8px 40px rgba(42,16,8,0.25);
}
.pagehead__lead { max-width: 52ch; margin: 1.1rem 0 0; opacity: 0.92; font-size: clamp(1rem, 1.6vw, 1.15rem); }

/* --------------------------------------------------------- Roster (crew.html) */
/* Full-width profile rows, art and copy swapping sides on every other DJ. */
.roster { list-style: none; margin: 0; padding: 0; display: grid; gap: clamp(2.5rem, 6vw, 4.5rem); }
.roster__item { display: grid; gap: clamp(1.2rem, 3vw, 2.8rem); grid-template-columns: 1fr; align-items: center; }
@media (min-width: 760px) {
  .roster__item { grid-template-columns: 0.85fr 1.15fr; }
  .roster__item:nth-child(even) .roster__art { order: 2; }
  /* Mirror the entrance direction so alternating rows don't all slide in the same way. */
  .roster__item:nth-child(even).reveal.is-armed { transform: translateX(20px); }
  .roster__item:nth-child(odd).reveal.is-armed { transform: translateX(-20px); }
  /* The 1.15fr column rendered the art at 567px, which magnified the portraits
     cropped from group shots (Tim's source is 400px wide) to 1.42x and read as
     blurry. Cap the box so the layout never enlarges a photo past its own
     resolution, and keep each one flush to its outer edge so the alternating
     composition survives the cap. */
  .roster__art { max-width: 480px; width: 100%; justify-self: start; }
  .roster__item:nth-child(even) .roster__art { justify-self: end; }
}
.roster__art {
  position: relative; overflow: hidden; border-radius: var(--radius); box-shadow: var(--shadow);
  aspect-ratio: 4 / 3; display: grid; place-items: center;
  font-family: var(--font-display); font-size: clamp(3rem, 8vw, 5rem); color: rgba(255,255,255,0.92);
  background-image: linear-gradient(135deg, var(--c1, var(--coral)), var(--c2, var(--dusk)));
}
.roster__name { font-family: var(--font-display); font-weight: 400; font-size: clamp(1.8rem, 4vw, 2.6rem); line-height: 1.05; margin: 0; }
.roster__role { color: var(--coral-text); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.14em; margin: 0.5rem 0 1.1rem; }
.roster__bio { font-family: var(--font-display); font-size: clamp(1.1rem, 1.8vw, 1.3rem); line-height: 1.4; margin: 0 0 0.9rem; max-width: 40ch; }
.roster__long { color: var(--ink-soft); margin: 0 0 1.4rem; max-width: 46ch; }
/* Sits between the name/role and the bio, so the qualifier is read before the
   copy it qualifies. Not an opacity wash -- that would drop it below AA. */
.roster__tbc, .crewcard__tbc {
  margin: 0.15rem 0 0; font-size: 0.78rem; font-weight: 600;
  color: var(--ink-soft); font-style: italic;
}
.roster__tbc { margin: 0.35rem 0 1rem; }
/* Drop a real portrait in via the `photo` field in main.js; the gradient +
   initial behind it is the graceful fallback until you have one. */
.roster__photo {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; filter: grayscale(0.35) saturate(1.05) contrast(1.02);
}
/* The last block in a profile sets its own bottom margin to space whatever
   follows it. When nothing does -- a DJ with no links -- that margin is just a
   gap at the foot of the card. */
.roster__body > :last-child { margin-bottom: 0; }
.roster__links { display: flex; gap: 0.8rem; font-size: 0.9rem; font-weight: 600; }
.roster__links a { text-decoration: none; color: var(--ink); border-bottom: 2px solid var(--amber); padding-bottom: 1px; position: relative; }
.roster__links a::after { content: ""; position: absolute; inset: -0.7rem 0; }
.roster__links a:hover { color: var(--coral); border-color: var(--coral); }

/* Closing call-to-action on subpages: a left-aligned block reusing
   .contact__lead / .contact__social so the typography matches the rest of the
   site. On crew.html and listen.html it points at contact.html. */
.pagecta { max-width: 46ch; }
.pagecta__action { margin: 1.9rem 0 0; }

/* --------------------------------------------------------- Events */
.events { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.event {
  display: grid; gap: 0.4rem 1.6rem; align-items: center;
  grid-template-columns: 1fr; padding: 1.4rem 1.6rem;
  background: #fff; border-radius: var(--radius); box-shadow: var(--shadow);
  transition: transform 0.3s var(--ease);
}
.event:hover { transform: translateX(6px); }
@media (min-width: 720px) { .event { grid-template-columns: 8.5rem 1fr auto; } }
.event__date { font-family: var(--font-display); font-size: 1.1rem; font-weight: 400; }
.event__date span { display: block; font-family: var(--font-body); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--coral-text); }
.event__name { font-weight: 600; font-size: 1.1rem; }
.event__where { color: var(--ink-soft); font-size: 0.95rem; }
/* A venue with a map link. --coral-text rather than --coral: coral on cream is
   2.44:1 and this is small text. The underline is the affordance, since the
   line around it is not a link. inline-flex + min-height carries a real target
   without pushing the underline off the text. */
.event__map {
  color: var(--coral-text); text-decoration: underline; text-underline-offset: 3px;
  display: inline-flex; align-items: center; min-height: 2.75rem;
}
.event__map:hover { text-decoration-thickness: 2px; }
.event__cta { justify-self: start; }
/* "Provisional" is now said in words on the date cell, so the card no longer
   whispers it with a blanket opacity — which dropped its own text to ~3.4:1. */
.event--tbc .event__cta { font-style: italic; color: var(--ink-soft); }
/* Empty state: no date cell, so the row is copy + CTA rather than the 3-column
   event layout. Sits after the base rule above, which it overrides. */
@media (min-width: 720px) { .event--none { grid-template-columns: 1fr auto; } }
.event--none:hover { transform: none; }
/* Mobile: single column → center everything. MUST sit after the base rules above,
   because `.event__cta { justify-self: start }` would otherwise win on source order
   (media queries add no specificity). */
@media (max-width: 719px) {
  .event { justify-items: center; text-align: center; gap: 0.55rem; }
  .event__date { justify-self: center; }
  .event__cta { justify-self: center; }
}

/* No .event--past rule on purpose: a played party is the same kind of object as
   a booked one, sits in the same list, and wears the same paper card. Its CTA is
   what marks it -- "see the stories" rather than "ask us" -- not a second card
   treatment. The class stays as a hook. */
/* No confirmed date yet, so there is no date cell to reserve a column for.
   Sits after the base .event grid it overrides, like .event--none does. */
@media (min-width: 720px) { .event--nodate { grid-template-columns: 1fr auto; } }

/* --------------------------------------------------------- Listen */
/* The set, in order — not two equal blocks stacked. The page's own lead says
   "van namiddag tot schemering", so the list is built as that arc: a rail runs
   down the side from amber through coral to dusk, and each mix hangs off it at
   its own point in the afternoon. The sun goes down as you scroll.
   Still no card and no shadow — the player is a functional object, the name is
   the thing you read. */
.listen {
  max-width: 54rem; margin-inline: auto; position: relative;
  display: grid; gap: clamp(3.4rem, 8vw, 5.5rem);
}
.listen__item { position: relative; display: grid; gap: clamp(1rem, 2.5vw, 1.4rem); }

/* Below 780px there is no room for a margin column, so the arc collapses to a
   single dot set inline ahead of the name. */
@media (min-width: 780px) {
  .listen { padding-left: 4.75rem; }
  .listen::before {
    content: ""; position: absolute; left: 1.3rem; top: 0; bottom: 0; width: 1px;
    background: linear-gradient(180deg, var(--amber) 0%, var(--coral) 46%, var(--dusk) 100%);
    /* Masked at both ends so it reads as a drawn stroke rather than a border. */
    -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 7%, #000 86%, transparent 100%);
            mask-image: linear-gradient(180deg, transparent 0, #000 7%, #000 86%, transparent 100%);
  }
  /* --dot is set per article in the markup, so the arc extends with the list
     instead of being hardcoded to two nth-child rules. */
  .listen__item::before {
    content: ""; position: absolute; left: -3.79rem; top: 0.6rem;
    width: 0.72rem; height: 0.72rem; border-radius: 50%;
    background: var(--dot, var(--coral));
    box-shadow: 0 0 0 6px var(--sand); /* punched out of the rail, not laid on it */
  }
}
@media (max-width: 779px) {
  .listen__name::before {
    content: ""; display: inline-block; width: 0.5rem; height: 0.5rem;
    border-radius: 50%; margin-right: 0.65rem; vertical-align: middle;
    background: var(--dot, var(--coral)); transform: translateY(-0.12em);
  }
}

/* The name carries the head alone now; the profile link sits under the player,
   where an outbound belongs. A hairline that fades as it travels — a gradient
   rather than a flat rule, so it reads like a pen stroke, not a divider. */
.listen__head {
  padding-bottom: 0.7rem;
  border-bottom: 1px solid transparent;
  border-image: linear-gradient(90deg, rgba(42,26,34,0.42), rgba(42,26,34,0.05)) 1;
}
.listen__name {
  margin: 0;
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(2.1rem, 6vw, 3.2rem); line-height: 1; letter-spacing: -0.02em;
}
/* --coral-text, not --ink-soft over an amber rule: this clears 5.16:1 and the
   underline is drawn from the text colour, so the affordance carries its own
   contrast. Vertical padding lifts the target off the 22px it used to be. */
.listen__link { margin: 0; font-size: 0.9rem; }
.listen__link a {
  display: inline-flex; align-items: center; min-height: 2.75rem;
  color: var(--coral-text); text-decoration: underline;
  text-underline-offset: 4px; text-decoration-thickness: 1px;
}
.listen__link a:hover { text-decoration-thickness: 2px; }
/* Hairline instead of a shadow: the widget already carries its own weight, and
   a floating panel is what made this look like a component. */
.listen__player {
  display: block; width: 100%;
  border: 1px solid rgba(42,26,34,0.22); border-radius: 3px;
}

code { background: rgba(42,26,34,0.08); padding: 0.1rem 0.4rem; border-radius: 6px; font-size: 0.9em; }

/* --------------------------------------------------------- Gallery */
/* Photos and clips as prints laid on a low sun: the same radial the page head
   and the About section use, so the strip sits inside the palette instead of on
   bare cream. One frame at a time, the track is a scroll-snap container so it
   works with no JS at all; the counter and arrows below come from initGallery(). */
.gallery {
  position: relative;
  display: grid; gap: clamp(1.1rem, 3vw, 1.6rem);
  grid-template-areas: "aside" "view" "count";
}
.gallery::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  left: 50%; bottom: 2.5rem; transform: translateX(-50%);
  width: min(64rem, 96%); aspect-ratio: 2 / 1;
  background: radial-gradient(circle at 50% 100%, rgba(255, 178, 77, 0.40) 0%,
              rgba(255, 120, 79, 0.20) 42%, rgba(255, 120, 79, 0) 70%);
}
/* min-width: 0 is load-bearing, not tidying. Grid items default to
   min-width: auto, so the scroll track's intrinsic width pushes the single
   grid column wider than .gallery itself -- at 320px the whole block rendered
   321px inside a 280px parent and pushed the page 21px past the viewport,
   silently clipped by body { overflow-x: hidden }. It only crossed the
   threshold once the track held more than nine slides. */
.gallery > * { position: relative; z-index: 1; min-width: 0; }
.gallery__aside { grid-area: aside; margin-bottom: clamp(0.4rem, 1.5vw, 1rem); }
.gallery__aside .section__title { margin: 0; }
.gallery__viewport { grid-area: view; position: relative; min-width: 0; }
.gallery__track {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-auto-flow: column; grid-auto-columns: 100%;
  gap: clamp(0.7rem, 2vw, 1.1rem);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none; /* the controls replace it */
}
.gallery__track::-webkit-scrollbar { display: none; }
/* --coral on cream is 2.44:1; --coral-text clears 5.16:1. */
.gallery__track:focus-visible { outline: 3px solid var(--coral-text); outline-offset: 4px; border-radius: 4px; }
.gallery__slide { scroll-snap-align: start; display: grid; place-items: center; }
/* The print. A photo and a portrait clip are different shapes, so the frame
   takes the shape of what is in it rather than forcing both into one box. */
.gallery__frame {
  /* Width is set here, not on the media inside: a percentage width on the child
     of a fit-content box resolves against the box it is sizing, so the frame
     ended up wider than the print it was holding. */
  margin: 0; width: min(47rem, 100%);
  padding: clamp(0.45rem, 1.1vw, 0.7rem);
  background: #fff; border-radius: calc(var(--radius) + 8px);
  box-shadow: var(--shadow);
}
.gallery__frame img,
.gallery__frame video,
.gallery__frame iframe {
  display: block; border-radius: var(--radius); background: var(--dusk-deep);
}
.gallery__frame img {
  width: 100%; height: auto;
  aspect-ratio: 3 / 2; object-fit: cover; object-position: center;
}
/* Reels are portrait, so their print is narrower than a photo's by nature. */
.gallery__frame--clip { width: min(22.6rem, 100%); }
.gallery__video { width: 100%; aspect-ratio: 9 / 16; object-fit: cover; }
/* On a phone a full 9:16 clip is nearly twice the height of a landscape photo,
   and every slide shares the tallest one's height. Cropping the clip closer to
   3:4 there keeps the photos from floating in dead space. */
@media (max-width: 719px) { .gallery__video { aspect-ratio: 3 / 4; } }
/* Instagram's player, used only when a clip file is missing. It will not
   compress: below 304x544 it clips its own footer, so there is no height that
   both fits a 375px phone and keeps the embed intact. That is why the reel
   slides are commented out in index.html until the mp4s exist -- this rule is
   kept ready for them, not currently used by anything. */
.gallery__reel { width: 100%; height: 34rem; border: 0; background: #fff; }
/* The arrows flank the strip, on the same white-print material as the frames.
   At container width the prints leave a margin either side, so on a wide screen
   these sit in that margin rather than over a photo; on a phone the print fills
   the width and they overlap its edge, which is what the shadow is for. */
.gallery__arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  display: grid; place-items: center;
  width: 2.75rem; height: 2.75rem; padding: 0; cursor: pointer; /* 44px touch target */
  border-radius: 50%; color: var(--coral-text);
  background: #fff; border: 0;
  box-shadow: 0 12px 26px -14px rgba(58, 24, 12, 0.75);
  transition: color 0.25s var(--ease), background 0.25s var(--ease), opacity 0.25s var(--ease);
}
.gallery__arrow--prev { left: clamp(0.35rem, 1.2vw, 0.9rem); }
.gallery__arrow--next { right: clamp(0.35rem, 1.2vw, 0.9rem); }
/* Ink over coral (6.34:1); white here was 2.61:1. */
.gallery__arrow:hover:not(:disabled) { color: var(--ink); background: var(--coral); }
/* Kept in place rather than removed, so the pair does not jump at the ends. */
.gallery__arrow:disabled { opacity: 0.35; cursor: default; box-shadow: none; }
.gallery__arrow svg { width: 1.05rem; height: 1.05rem; }

/* --------------------------------------------------------- Contact */
.contact { display: grid; gap: clamp(1.8rem, 5vw, 3.5rem); grid-template-columns: 1fr; align-items: start; }
@media (min-width: 820px) { .contact { grid-template-columns: 1fr 1fr; } }
.contact__lead { font-size: 1.08rem; max-width: 42ch; }
/* gap carries the hit areas apart: the ::after below reaches 0.5rem past each
   side, so a 1.2rem gap would let neighbouring targets overlap. */
.contact__social { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 1.4rem; font-weight: 600; }
/* The amber underline has to stay tight to the text, so the target is grown with
   a pseudo-element rather than by padding the box (which would drag the border
   down with it). Same trick on .crewstrip__more and .roster__links. */
.contact__social a { text-decoration: none; border-bottom: 2px solid var(--amber); position: relative; }
/* Grown on both axes: the shortest label here is "Luc" at ~29px, so vertical
   expansion alone leaves it under the 44px floor on width. */
.contact__social a::after { content: ""; position: absolute; inset: -0.55rem -0.5rem; }
.contact__social a:hover { color: var(--coral); border-color: var(--coral); }

.signup { background: #fff; border-radius: var(--radius); padding: clamp(1.5rem, 4vw, 2.2rem); box-shadow: var(--shadow); }
.signup__label { display: block; font-weight: 600; font-size: 0.95rem; }
.signup__hint { margin: 0.25rem 0 0; font-size: 0.85rem; color: var(--ink-soft); }
.signup__trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; opacity: 0; }
.signup__row { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-top: 0.8rem; }
.signup__input,
.field__control {
  flex: 1 1 14rem; min-width: 0; padding: 0.85rem 1.1rem; font-size: 1rem; font-family: inherit;
  border: 1.5px solid rgba(42,26,34,0.2); border-radius: 999px; background: var(--sand);
  transition: border-color 0.2s, box-shadow 0.2s;
}
/* The soft tint, for a focus the browser does not consider keyboard-driven --
   in practice a <select> opened with the mouse. It is only 1.19:1, so it is an
   affordance, never the indicator: anything that matches :focus-visible gets the
   real ring below instead. */
.signup__input:focus,
.field__control:focus { border-color: var(--coral); box-shadow: 0 0 0 4px rgba(255,120,79,0.18); }
.signup__input:focus:not(:focus-visible),
.field__control:focus:not(:focus-visible) { outline: 0; }
/* One ring, not two. A text input always matches :focus-visible -- even on a
   mouse click -- so it used to draw the global 3px/3px-offset outline *and* this
   block's 4px tint at once: a heavy double band standing off a 999px pill, which
   read as an error state rather than a focused field. So the tint is dropped
   whenever the outline shows, and the outline is pulled in tight against the
   border to read as one thicker edge. --coral-text, not --coral: the indicator
   owes 3:1 and coral manages 2.44:1 on these cream surfaces. */
.signup__input:focus-visible,
.field__control:focus-visible {
  border-color: var(--coral-text);
  outline: 2px solid var(--coral-text); outline-offset: 1px;
  box-shadow: none;
}
.signup__note { margin: 0.9rem 0 0; min-height: 1.2em; font-size: 0.92rem; color: var(--coral-text); }
.signup__note.is-ok { color: #1c7d4d; }
/* Neutral tone: an instruction that is neither a success nor an error. */
.signup__note.is-info { color: var(--ink-soft); }
/* The mailto fallback puts real links in the note; they carry the same amber
   underline as every other inline link on the site. */
.signup__mailto { color: inherit; text-decoration: none; border-bottom: 2px solid var(--amber); }
.signup__mailto:hover { color: var(--coral-text); border-color: var(--coral-text); }
.signup__input[aria-invalid="true"],
.field__control[aria-invalid="true"] { border-color: var(--coral); }

/* --------------------------------------------------------- Message form */
/* The longer form on contact.html. Same paper card as .signup -- it is the same
   kind of object -- but stacked labelled fields rather than one inline row, so
   it gets its own layout and borrows .signup's control, note and trap rules
   through the shared selectors above. */
.msgform { background: #fff; border-radius: var(--radius); padding: clamp(1.5rem, 4vw, 2.2rem); box-shadow: var(--shadow); }
.msgform__fields { display: grid; gap: 1.1rem; }
/* Name and address sit side by side once there is room for both to stay
   comfortably wide; below that they stack like everything else. */
.msgform__pair { display: grid; gap: 1.1rem; grid-template-columns: 1fr; }
@media (min-width: 640px) { .msgform__pair { grid-template-columns: 1fr 1fr; } }
.msgform__action { margin: 1.4rem 0 0; }

/* align-content:start is load-bearing. Grid rows default to stretching into
   spare height, and the pair makes both columns as tall as the taller one -- so
   the field with no hint under it grew its own control to fill the difference,
   leaving the two inputs different heights and off the same baseline. Starting
   the rows keeps every control its natural size; the hint just hangs below. */
/* margin:0 because a .field is a <p> and this stylesheet has no paragraph reset
   -- the UA's 1em block margins were stacking on top of the grid gaps, so the
   spacing between fields was the gap plus two margins rather than the gap. */
.field { display: grid; align-content: start; gap: 0.35rem; min-width: 0; margin: 0; }
.field__label { font-weight: 600; font-size: 0.95rem; }
.field__hint { margin: 0; font-size: 0.85rem; color: var(--ink-soft); }
/* The pill radius belongs to single-line controls. A multi-line box with 999px
   corners bows its own edges, so the textarea takes the card radius instead. */
textarea.field__control { border-radius: var(--radius); min-height: 9.5rem; resize: vertical; line-height: 1.55; }
/* The native arrow stays: appearance:none would strip the only affordance the
   control has, and drawing a replacement means either a background image (a
   request) or an extra element. The platform control stays the platform
   control -- it just wears our padding, border and radius. */
select.field__control { cursor: pointer; }

/* --------------------------------------------------------- Footer */
.footer { background: var(--dusk-deep); color: rgba(255,255,255,0.8); padding-block: 2.2rem; }
.footer .muted { color: rgba(255,255,255,0.55); }
.footer__inner { display: flex; flex-wrap: wrap; gap: 0.6rem 1.5rem; align-items: center; justify-content: space-between; }
.footer__brand { font-family: var(--font-display); font-size: 1.15rem; letter-spacing: 0.04em; color: #fff; }

/* --------------------------------------------------------- Crew strip */
/* Home-page reduction of the roster: four names, no bios. Auto-fit rather than
   a fixed four-up so it folds to two and then one without a breakpoint. */
.crewstrip {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: clamp(1rem, 2.5vw, 1.6rem);
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
.crewcard { display: flex; align-items: center; gap: 0.9rem; }
/* Same material and gradient recipe as .roster__art, at listing scale. */
.crewcard__art {
  flex: 0 0 auto; width: 3.4rem; height: 3.4rem; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  font-family: var(--font-display); font-size: 1.35rem; color: #fff;
  background-image: linear-gradient(135deg, var(--c1, var(--coral)), var(--c2, var(--dusk)));
}
/* Fills the 3.4rem disc once a `photo:` is set in CREW; without it a portrait
   source renders at natural size inside the circle instead of cropping to it. */
.crewcard__art img { width: 100%; height: 100%; object-fit: cover; }
.crewcard__name { margin: 0; font-weight: 600; }
.crewcard__role { margin: 0.1rem 0 0; font-size: 0.86rem; color: var(--ink-soft); }
.crewstrip__more { margin: clamp(1.4rem, 3vw, 2rem) 0 0; }
.crewstrip__more a {
  color: var(--ink); text-decoration: none; position: relative;
  border-bottom: 2px solid var(--amber); padding-bottom: 1px; font-weight: 600;
}
.crewstrip__more a::after { content: ""; position: absolute; inset: -0.75rem 0; }
.crewstrip__more a:hover { color: var(--coral-text); border-color: var(--coral-text); }

/* --------------------------------------------------------- Back to top */
/* Hidden AND out of the tab order until JS adds .is-visible, so an invisible
   control is never focusable. The disc is a little sun -- the same radial motif
   as .hero__sun and .about::before, shrunk to 3.2rem: a hot core reading out to
   coral at the rim, a corona breathing past the edge, and rays turning slowly
   behind it. The arrow stays ink so it clears 4.5:1 over every stop of that ramp
   (6.3:1 at the coral rim, the worst of them); white would not. */
.totop {
  position: fixed; z-index: 40;
  right: clamp(1rem, 3vw, 2rem); bottom: clamp(1rem, 3vw, 2rem);
  width: 3.2rem; height: 3.2rem; border-radius: 50%;
  display: grid; place-items: center;
  color: var(--ink); text-decoration: none;
  background-image: radial-gradient(circle at 50% 38%,
    #ffe7b0 0%, var(--amber) 44%, var(--coral) 100%);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.45),
    0 10px 26px -12px rgba(255, 120, 79, 0.9),
    0 3px 10px -4px rgba(42, 26, 34, 0.35);
  opacity: 0; visibility: hidden; transform: translateY(12px) scale(0.9);
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease),
              box-shadow 0.3s var(--ease), visibility 0s linear 0.3s;
}
/* Corona and rays. Both are absolutely positioned, so they sit outside the
   grid flow and never shift the arrow, and both are pointer-events:none so the
   3.2rem disc stays the whole hit target. .totop is fixed and so makes its own
   stacking context -- the corona's negative z-index puts it behind the disc
   without escaping behind the page. */
.totop::before,
.totop::after {
  content: ""; position: absolute; border-radius: 50%; pointer-events: none;
}
/* The corona: the .hero__sun gradient, same three stops, scaled down. */
.totop::before {
  inset: -1.45rem; z-index: -1;
  background: radial-gradient(circle,
    rgba(255, 243, 214, 0.85) 0%, rgba(255, 217, 138, 0.45) 42%, rgba(255, 178, 77, 0) 72%);
  filter: blur(2px);
  animation: totop-corona 5.5s var(--ease) infinite alternate;
  transition: filter 0.3s var(--ease);
}
/* The rays: spokes from a repeating cone, masked to a ring so they start past
   the rim of the disc and fade out before the corona does. */
.totop::after {
  inset: -1.1rem; z-index: 0; opacity: 0.7;
  background: repeating-conic-gradient(from 0deg,
    rgba(255, 236, 190, 0.9) 0deg 2.5deg,
    rgba(255, 236, 190, 0) 2.5deg 15deg);
  -webkit-mask-image: radial-gradient(circle, transparent 56%, #000 67%, transparent 96%);
          mask-image: radial-gradient(circle, transparent 56%, #000 67%, transparent 96%);
  animation: totop-rays 26s linear infinite;
  transition: opacity 0.3s var(--ease);
}
@keyframes totop-corona {
  from { opacity: 0.75; transform: scale(0.94); }
  to   { opacity: 1;    transform: scale(1.06); }
}
@keyframes totop-rays { to { transform: rotate(1turn); } }

.totop.is-visible {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease),
              box-shadow 0.3s var(--ease), visibility 0s;
}
/* Lift the disc, nudge the arrow — the two together read as "up". */
.totop.is-visible:hover, .totop.is-visible:focus-visible { transform: translateY(-3px); }
.totop:hover {
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.6),
    0 18px 34px -14px rgba(255, 120, 79, 0.95),
    0 4px 12px -4px rgba(42, 26, 34, 0.4);
}
/* Flare on hover. The corona and rays both animate transform, so brightening
   them is done with filter and opacity -- a transform here would be overridden
   by the keyframes mid-cycle. */
.totop.is-visible:hover::before,
.totop.is-visible:focus-visible::before { filter: blur(2px) brightness(1.22); }
.totop.is-visible:hover::after,
.totop.is-visible:focus-visible::after { opacity: 1; }
.totop.is-visible:active { transform: translateY(-1px) scale(0.96); }
/* Relative, because the rays paint above the disc background: without a layer
   of its own the arrow would sit under them. */
.totop__arrow { display: block; position: relative; z-index: 1; transition: transform 0.3s var(--ease); }
.totop:hover .totop__arrow, .totop:focus-visible .totop__arrow { transform: translateY(-2px); }
/* Position readout. It sits in its own grid row rather than inside the viewport,
   so the viewport's height stays equal to the track's and the arrows, which are
   centred against it, do not drift downwards. Tabular numerals so the width does
   not jitter as the digits change. */
.gallery__count {
  grid-area: count; margin: 0; text-align: center;
  font-size: 0.82rem; font-weight: 600; letter-spacing: 0.06em;
  color: var(--ink-soft); font-variant-numeric: tabular-nums;
}

/* Ink ring, not coral — a coral outline would vanish into the button. */
.totop:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
/* Still sun for anyone who asked for less movement: the global reduce rule only
   collapses the duration, which would park the corona at whichever frame it
   landed on. Pin both to a settled state instead. */
@media (prefers-reduced-motion: reduce) {
  .totop::before { animation: none; opacity: 0.9; transform: none; }
  .totop::after { animation: none; }
}

/* reCAPTCHA Enterprise parks its badge bottom-right, which is exactly where
   .totop sits -- the sun would land on top of it. Send the badge to the other
   corner. Google styles the injected element itself from a <style> it appends at
   runtime, i.e. after this sheet, so !important is what makes any of this stick.

   Tucked away until you point at it: Google's own badge hides by sliding out on
   `right`, which we cannot mirror -- the logo is the LEFT 70px of the 256px
   element, so a slide to the left would leave "Privacy - Terms" showing and the
   mark off-screen. Clipping the width instead keeps the reCAPTCHA logo as the
   visible sliver (the attribution stays on screen, which Google requires) and
   opens the full badge on hover or when focus lands in it, so it is reachable
   by keyboard and readable on touch, where there is no hover at all. */
.grecaptcha-badge {
  left: clamp(1rem, 3vw, 2rem) !important;
  right: auto !important;
  bottom: clamp(1rem, 3vw, 2rem) !important;
  width: 70px !important;
  overflow: hidden !important;
  transition: width 0.3s var(--ease) !important;
}
.grecaptcha-badge:hover,
.grecaptcha-badge:focus-within { width: 256px !important; }
/* The global reduce rule is on `*`, so this more specific !important would beat
   it. Repeat it here rather than let the badge keep animating. */
@media (prefers-reduced-motion: reduce) {
  .grecaptcha-badge { transition-duration: 0.001ms !important; }
}

/* --------------------------------------------------------- Reveal on scroll */
/* Reveal = progressive enhancement. Content is visible by DEFAULT; JS "arms"
   the hidden state (.is-armed) only when it can also remove it (.is-in). So if
   JS or IntersectionObserver ever fails, no section ships blank. */
.reveal { transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); transition-delay: calc(var(--i, 0) * 70ms); }
.reveal.is-armed { opacity: 0; transform: translateY(22px); }
.reveal.is-in { opacity: 1; transform: none; }
/* Differentiated entrances — not one uniform fade for everything:
   hero sequences top-to-bottom, events slide in from the side, roster rows alternate (--i from JS). */
.hero__inner .reveal.is-armed { transform: translateY(28px) scale(0.99); }
.hero__inner .reveal:nth-child(1) { transition-delay: 0.05s; }
.hero__inner .reveal:nth-child(2) { transition-delay: 0.16s; }
.hero__inner .reveal:nth-child(3) { transition-delay: 0.30s; }
.hero__inner .reveal:nth-child(4) { transition-delay: 0.44s; }
@media (min-width: 720px) { .event.reveal.is-armed { transform: translateX(-20px); } }

/* --------------------------------------------------------- Mobile nav */
@media (max-width: 760px) {
  /* Toggle must sit ABOVE the panel so the X stays tappable to close. */
  .nav__toggle { display: flex; position: relative; z-index: 60; }
  .nav__menu {
    position: fixed; inset: 0 0 0 auto; width: min(78vw, 320px); z-index: 55;
    flex-direction: column; align-items: flex-start; justify-content: center; gap: 1.6rem;
    padding: 5rem var(--gutter) 2rem;
    background: rgba(255, 246, 236, 0.98); backdrop-filter: blur(14px);
    transform: translateX(100%); visibility: hidden;
    transition: transform 0.35s var(--ease), visibility 0s linear 0.35s;
    /* Warm-tinted, not rgba(0,0,0). */
    box-shadow: -20px 0 60px -30px rgba(42, 26, 34, 0.6);
  }
  .nav__menu.is-open {
    transform: translateX(0); visibility: visible;
    transition: transform 0.35s var(--ease), visibility 0s;
  }
  /* The slide-in panel is a light surface — links read as ink here, not the over-hero white. */
  .nav__menu a:not(.nav__cta) {
    font-size: 1.25rem; color: var(--ink);
    display: flex; align-items: center; min-height: 2.75rem;
  }
  /* ...and the focus ring owes the same flip, which it was not getting. The
     panel is a descendant of .nav, so while the bar is unstuck it inherited the
     over-hero --sand ring: #fff6ec on a #fbf2e8 panel is 1.04:1, i.e. no visible
     focus anywhere in the primary nav. --coral-text restores 4.98:1 here. */
  .nav:not(.is-stuck) .nav__menu :focus-visible { outline-color: var(--coral-text); }
}
