@import url('tokens.css');

/* Opts every same-origin navigation into the browser's own cross-document view transition
   (Chrome/Edge 126+; silently ignored, zero risk, everywhere else) — this is a page-to-page
   *multi-page* site, not an SPA, so every click is a full document load, and without this the
   browser's default behaviour is a hard cut to blank white the instant the old page unloads,
   held until the new page's first paint. That hard white flash is what "blinks before a page
   loads" — separate from (and upstream of) the nav/main fade-in timing fixed further below,
   which only ever addressed the *new* page's own content settling in, not the gap between the
   old page and the new one existing at all. With this on, supporting browsers cross-fade the old
   page's last frame into the new one instead of ever showing a blank frame in between. */
@view-transition {
  navigation: auto;
}

* {
  box-sizing: border-box;
}

::selection {
  background-color: var(--color-border);
  color: var(--color-text);
}

html {
  font-size: 100%;
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  line-height: 1.5;
  /* Matches the live Wix site's own Gill Sans MT rendering (confirmed via its computed styles),
     which explicitly sets this — without it, the embedded Gill Sans MT webfont renders
     noticeably heavier on WebKit/Chromium's default subpixel smoothing than the grayscale
     antialiasing this produces. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Standard "sticky footer" flex column, min-height:100vh — needed so .tracklist-hero's own
     min-height:100vh (see lyrics.css) doesn't end up stacked *underneath* the nav bar's own
     height instead of sharing the viewport with it. Without this, the page's total height was
     always (100vh + nav height), a permanent, pointless bit of scroll room on any screen tall
     enough that the tracklist content itself never needed to scroll — confirmed directly on a
     2560×1440 viewport (nav 57px + hero 100vh = 57px of scrollable overflow with nothing to
     actually scroll to). */
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

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

a {
  color: inherit;
  text-decoration: none;
}

h1, h2, h3 {
  font-family: var(--font-heading);
  font-weight: 400;
  line-height: 1.1;
  margin: 0 0 var(--space-3);
}

h1 {
  text-transform: uppercase;
  text-align: left;
  font-size: clamp(2.5rem, 6vw, 4.5rem);
  letter-spacing: -0.02em;
  color: #999997;
}

/* Shared load-in animation — everything on the site used to just blip into existence with no
   transition at all, which reads as choppy especially on a slower connection/device. Originally
   main played this immediately at first paint (it's present in the initial HTML, so its own
   `animation` property just autoplayed there and then) while the nav/footer partials only
   started once js/include.js's DOMContentLoaded handler actually got to them — two different
   starting lines, which looked like a blink (nav popping in late) followed by a separate,
   already-half-finished-looking fade on main. main's fade is now gated behind an `html.is-ready`
   class instead, added at the very top of that same DOMContentLoaded handler — on a warm/cached
   nav load (the common case after the first visit) that class add and the nav's own `.is-loaded`
   both land in the same synchronous tick, so the two fades start on the same frame. On a genuinely
   cold first-ever visit nav still necessarily waits on its own fetch — that's an inherent network
   wait, not something this animation choice caused or can remove. */
@keyframes page-fade-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Several page types keep content outside <main> entirely, so the plain [data-include]/main rule
   above never covers it: Gigs' header + year-tab strip, Stats' header + fixed back link, every
   Lyrics detail page's header + fixed back link, every album/EP hub page's whole
   `.tracklist-hero` (those pages have no <main> at all), and Home's floating disclaimer button (a
   sibling of main, meant to float over the page rather than sit inside its layout flow). Left
   uncovered, that content painted at first parse instead of waiting behind this same is-ready
   gate — for the text-bearing ones, that's what let the fallback system font show up
   front-and-center for a beat before swapping to Gill Sans MT (everywhere else, that swap
   finishes invisibly during the opacity:0 wait); for the disclaimer button, it's just about not
   popping in ahead of the rest of the page's own load-in fade. (Home's disclaimer card itself
   doesn't need adding here — it starts `hidden` and stays that way until explicitly opened,
   regardless of page-load timing.) */
[data-include],
main,
.gigs-page__header,
.gigs-timeline,
.stats-page__header,
.stats-page__back-fixed,
.lyrics-page__header,
.lyrics-page__back-fixed,
.tracklist-hero,
.home-disclaimer-btn {
  opacity: 0;
}

[data-include].is-loaded,
html.is-ready main,
html.is-ready .gigs-page__header,
html.is-ready .gigs-timeline,
html.is-ready .stats-page__header,
html.is-ready .stats-page__back-fixed,
html.is-ready .lyrics-page__header,
html.is-ready .lyrics-page__back-fixed,
html.is-ready .tracklist-hero,
html.is-ready .home-disclaimer-btn {
  /* opacity:1 as its own plain property, not just the animation's `to` state — the sitewide
     prefers-reduced-motion rule disables this animation with `!important`, and without a plain
     fallback here that would leave these permanently stuck at the rule above's opacity:0 forever
     for anyone with that preference on, instead of just skipping the animation. */
  opacity: 1;
  animation: page-fade-in 0.5s ease;
}

main {
  /* width:100% (not just relying on align-items:stretch's default) — needed specifically on pages
     whose content includes a CSS grid with repeat(auto-fill, minmax(...)) columns (the lyrics hub).
     Without a definite width here, the grid's auto-fill column count can't resolve against this
     flex item's real available space, so the browser falls back to sizing main by the grid's own
     max-content contribution (as many 280px columns as fit within the grid's max-width) instead of
     the viewport — main (and the grid inside it) end up wider than the screen and never reflow to
     fewer columns on resize. Confirmed via a direct measurement: main rendered at 1168px in a
     900px-wide viewport until this was added. */
  width: 100%;
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--space-4);
}

/* Reserves the nav's own rendered height on its still-empty wrapper div before js/include.js's
   fetch resolves and injects the real <nav> markup — every page uses the identical
   data-include="/partials/nav.html" attribute, so this one attribute selector covers the whole
   site with no per-page HTML change. Without it, the wrapper starts at 0 height (nothing else
   gives it any), so all the body content below it renders flush against the very top of the page
   and visibly jumps down by the nav's own height the instant the fetch completes — min-height
   (not height) so a narrower viewport where the nav wraps to two lines can still grow past this
   without clipping or overlapping the content below it. */
[data-include="/partials/nav.html"] {
  min-height: 104px;
}

/* 104px above matches the full two-row desktop nav's own real height — correct there, but too
   tall for the touch-only slimmed one-line nav (hidden socials, shrunk logo, further down this
   file), which only actually renders at ~79px. That's exactly what surfaced as an unexplained
   white gap between the nav and the page content on dark-background pages with no <main> (every
   album/EP Tracklist page) — confirmed directly: forcing this wrapper's min-height to 0 made the
   gap disappear entirely, and re-confirmed the *real*, non-zero value by measuring the touch nav's
   own actual rendered height live. Invisible on every other (white-background, has-<main>) page,
   which is why it was only ever reported here. */
@media (hover: none) {
  [data-include="/partials/nav.html"] {
    min-height: 80px;
  }
}

/* Sticky lives on the *wrapper* js/include.js injects nav's markup into, not on .site-nav
   itself — the real bug behind it "not sticking anywhere on the site" at all, in every browser,
   not just Safari (the previous round's flex-shrink/-webkit-sticky fix was chasing the wrong
   layer entirely). A stickily-positioned element can never travel further than its own
   containing block's own height — and .site-nav's containing block, if left sticky itself,
   would be this <div data-include="/partials/nav.html"> wrapper, which is barely taller than the
   nav bar it wraps (js/include.js just does mount.innerHTML = html; nothing gives it any extra
   height). It has nowhere to stick *to*: the wrapper itself scrolls out of view almost
   immediately, taking its "sticky" child with it, no matter how correct that child's own CSS is.
   Confirmed by contrast with a working example already on the site: Gigs' own sticky year-tab
   strip (css/gigs.css's .gigs-timeline) sticks correctly because it's a *direct* child of body
   (body is "display:flex;flex-direction:column;min-height:100vh", further up this file) —
   body's own height spans the entire page, giving it somewhere real to stick within. Targeting
   the wrapper by its exact data-include value (rather than every [data-include] — the footer
   partial uses the same attribute and has no reason to stick) puts nav in that same position:
   a flex item of body directly, sized by its own content but with body itself, not a
   content-height-only div, as what it sticks within. flex-shrink:0 stays a reasonable guard
   against a real, separate, well-documented WebKit flex-item-sizing quirk. */
[data-include="/partials/nav.html"] {
  position: sticky;
  position: -webkit-sticky;
  top: 0;
  z-index: 30;
  flex-shrink: 0;
}

.site-nav {
  background: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

/* Brand lockup: logo (2-image hover crossfade) + "BLOC PARTY." / "info" stacked wordmark,
   matching the live Wix site's own header exactly (confirmed via its computed styles/DOM —
   a 2-state image swap on the logo, and the wordmark's top line turning the page's accent red
   on hover, both over the same 0.4s ease already used sitewide for nav link hovers below). */
.site-nav__brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  text-decoration: none;
  color: inherit;
}

.site-nav__logo {
  position: relative;
  width: 70px;
  height: 70px;
  flex-shrink: 0;
}

.site-nav__logo-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transition: opacity 0.35s ease;
}

.site-nav__logo-img--base {
  opacity: 1;
}

.site-nav__logo-img--hover {
  opacity: 0;
}

.site-nav__brand:hover .site-nav__logo-img--base,
.site-nav__brand:focus-visible .site-nav__logo-img--base {
  opacity: 0;
}

.site-nav__brand:hover .site-nav__logo-img--hover,
.site-nav__brand:focus-visible .site-nav__logo-img--hover {
  opacity: 1;
}

.site-nav__wordmark {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
}

.site-nav__wordmark-line--main {
  /* Only Gill Sans MT's regular (400) weight is embedded as a real font file — anything above
     that forces the browser to synthesize ("faux") bold, which reads as a heavier, uneven weight
     rather than a true bold cut of the typeface. Was 700. */
  font-weight: 400;
  font-size: 2.1rem;
  letter-spacing: 0.01em;
  color: #b0b0b0;
  /* Fast in, slow+delayed out — the colour snaps to red the instant you hover (delay/duration
     overridden below), but lingers there for a moment after the cursor leaves before slowly
     fading back, rather than symmetrically snapping back at the same speed it appeared. */
  transition: color 0.4s ease 0.1s;
}

.site-nav__wordmark-line--sub {
  font-size: 1.15rem;
  font-weight: 400;
  color: #000;
}

.site-nav__brand:hover .site-nav__wordmark-line--main,
.site-nav__brand:focus-visible .site-nav__wordmark-line--main {
  color: var(--color-accent);
  transition-duration: 0.2s;
  transition-delay: 0s;
}

/* Right-hand stack: social icons sitting directly above the pill nav, both right-aligned
   against the same edge the brand lockup mirrors on the left. */
.site-nav__menu {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
}

.site-nav__socials {
  display: flex;
  gap: var(--space-3);
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-nav__socials a {
  display: inline-flex;
  width: 18px;
  height: 18px;
  color: var(--color-text-muted);
  transition: color 0.4s ease 0.1s;
}

.site-nav__socials svg {
  width: 100%;
  height: 100%;
  display: block;
}

.site-nav__socials a:hover,
.site-nav__socials a:focus-visible {
  color: var(--color-accent);
  transition-duration: 0.2s;
  transition-delay: 0s;
}

/* Pill nav — a vanilla-CSS port of reactbits.dev's "Pill Nav" (a React+GSAP component; this
   site has no build step or framework, so the actual mechanic was ported by hand). Confirmed via
   its live DOM/computed styles rather than guesswork: each pill holds a circle that scales in
   from 0 to fully cover the pill on hover, timed with the label text rolling up and swapping to
   an inverted colour, all pure CSS transitions since it's a simple two-state hover (no continuous
   animation the way Drift Wall needed). The circle is a fixed 200px — deliberately oversized for
   every real label on this nav (Home/Gigs/Music/etc. are all short) and simply clipped by the
   pill's own overflow:hidden, which sidesteps needing JS to measure each pill's diagonal like the
   original does. */
.site-nav__menu {
  --pill-nav-h: 40px;
}

.pill-nav__list {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 3px;
  list-style: none;
  margin: 0;
  padding: 3px;
  background: var(--color-bg);
  border-radius: 9999px;
}

.pill-nav__list > li {
  display: flex;
}

.pill-nav__pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--pill-nav-h);
  padding: 0 var(--space-3);
  background: transparent;
  color: var(--color-text);
  border-radius: 9999px;
  /* Same reasoning as .site-nav__wordmark-line--main above — no real bold Gill Sans MT face
     exists, so a weight above 400 renders as synthesized faux-bold. Was 600. */
  font-weight: 400;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  white-space: nowrap;
  overflow: hidden;
  /* The pill's own rounded, clipped shape doesn't play well with the browser's default focus
     ring (it gets clipped unevenly by overflow:hidden, showing as a broken partial outline) —
     suppressed here since the hover/focus-visible colour-invert below already gives a clear,
     full-shape focus indicator on its own. */
  outline: none;
}

.pill-nav__circle {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 200px;
  height: 200px;
  border-radius: 50%;
  background: var(--color-accent);
  transform: translate(-50%, -50%) scale(0);
  transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
  z-index: 1;
}

.pill-nav__label-stack {
  position: relative;
  display: inline-block;
  line-height: 1;
  z-index: 2;
}

.pill-nav__label {
  position: relative;
  z-index: 2;
  display: block;
  transform: translateY(0);
  transition: transform 0.35s ease;
}

.pill-nav__label--hover {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 3;
  color: var(--color-bg);
  opacity: 0;
  transform: translateY(var(--pill-nav-h));
  transition: transform 0.35s ease, opacity 0.25s ease;
}

.pill-nav__pill:hover .pill-nav__circle,
.pill-nav__pill:focus-visible .pill-nav__circle {
  transform: translate(-50%, -50%) scale(1);
}

.pill-nav__pill:hover .pill-nav__label,
.pill-nav__pill:focus-visible .pill-nav__label {
  transform: translateY(calc(-1 * var(--pill-nav-h)));
}

.pill-nav__pill:hover .pill-nav__label--hover,
.pill-nav__pill:focus-visible .pill-nav__label--hover {
  transform: translateY(0);
  opacity: 1;
}

/* Current page is its own permanent look — solid dark fill, independent of the hover-circle
   mechanic above — not a temporary "flash" state like hover/focus, so it doesn't animate in via
   the circle at all; it's simply always this colour on the page you're already on. Hovering the
   current pill still triggers the same red flash as any other pill (z-index puts the circle above
   this background), it just settles back to black instead of back to invisible. */
.pill-nav__pill[aria-current="page"] {
  background: var(--color-text);
  color: var(--color-bg);
}

/* Toggle button — replaces the pill bar under the breakpoint below. Mirrors the pill's own
   shape/colour (same dark rounded pill) so swapping between the two doesn't jolt the layout,
   with its own "Menu"/"Close" text roll and a plus-to-cross icon echoing the pill labels' own
   roll animation instead of introducing a visually unrelated hamburger icon. */
.nav-toggle {
  display: none;
  align-items: center;
  gap: var(--space-2);
  height: var(--pill-nav-h);
  padding: 0 var(--space-3);
  margin: 3px;
  background: var(--color-text);
  color: var(--color-bg);
  border: none;
  border-radius: 9999px;
  font-family: var(--font-body);
  /* Matches .pill-nav__pill's own fix — same face, same lack of a real bold cut. Was 600. */
  font-weight: 400;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  cursor: pointer;
}

.nav-toggle__text {
  position: relative;
  display: inline-block;
  height: 1em;
  overflow: hidden;
  line-height: 1;
}

.nav-toggle__text-line {
  display: block;
  transition: transform 0.3s ease;
}

.nav-toggle__text-line:nth-child(2) {
  position: absolute;
  top: 100%;
  left: 0;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__text-line {
  transform: translateY(-100%);
}

.nav-toggle__icon {
  position: relative;
  width: 14px;
  height: 14px;
}

.nav-toggle__icon-line {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 14px;
  height: 2px;
  background: currentColor;
  transform: translate(-50%, -50%);
  transition: transform 0.3s ease;
}

.nav-toggle__icon-line--v {
  transform: translate(-50%, -50%) rotate(90deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__icon-line {
  transform: translate(-50%, -50%) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__icon-line--v {
  transform: translate(-50%, -50%) rotate(-45deg);
}

/* Off-canvas panel the toggle opens — a vanilla-CSS take on reactbits.dev's "Staggered Menu"
   (its real mechanic: a panel sliding in from the edge, with its list items staggering in one
   after another rather than all at once, ported here via a per-item transition-delay driven by
   the --i custom property set inline on each <li> in partials/nav.html, instead of the
   original's GSAP timeline). visibility (not display) is what actually hides it at rest — the
   panel needs to keep participating in transitions while closed, and pointer-events:none is what
   actually keeps it from intercepting clicks then. */
.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 200;
  visibility: hidden;
  pointer-events: none;
}

.mobile-menu[aria-hidden="false"] {
  visibility: visible;
  pointer-events: auto;
}

.mobile-menu__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  opacity: 0;
  transition: opacity 0.35s ease;
}

.mobile-menu[aria-hidden="false"] .mobile-menu__backdrop {
  opacity: 1;
}

.mobile-menu__panel {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: min(360px, 100%);
  background: var(--color-bg);
  color: var(--color-text);
  padding: var(--space-5) var(--space-4) var(--space-4);
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
  box-shadow: -8px 0 32px rgba(0, 0, 0, 0.15);
}

.mobile-menu[aria-hidden="false"] .mobile-menu__panel {
  transform: translateX(0);
}

.mobile-menu__close {
  align-self: flex-end;
  background: none;
  border: none;
  font-size: 2rem;
  line-height: 1;
  color: var(--color-text);
  cursor: pointer;
  padding: var(--space-2);
}

.mobile-menu__list {
  list-style: none;
  margin: var(--space-3) 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  overflow-y: auto;
}

.mobile-menu__list a {
  display: block;
  font-family: var(--font-heading);
  font-size: 1.6rem;
  text-transform: uppercase;
  color: var(--color-text);
  padding: var(--space-1) 0;
  opacity: 0;
  transform: translateX(24px);
  transition: opacity 0.35s ease, transform 0.35s ease, color 0.4s ease 0.1s;
}

.mobile-menu[aria-hidden="false"] .mobile-menu__list a {
  opacity: 1;
  transform: translateX(0);
  transition-delay: calc(var(--i) * 0.06s);
}

.mobile-menu__list a:hover,
.mobile-menu__list a[aria-current="page"] {
  color: var(--color-accent);
  transition: color 0.2s ease;
}

.mobile-menu__socials {
  display: flex;
  gap: var(--space-4);
  list-style: none;
  margin: auto 0 0;
  padding: var(--space-4) 0 0;
  border-top: 1px solid var(--color-border);
}

.mobile-menu__socials a {
  display: inline-flex;
  width: 22px;
  height: 22px;
  color: var(--color-text-muted);
  transition: color 0.4s ease 0.1s;
}

.mobile-menu__socials svg {
  width: 100%;
  height: 100%;
  display: block;
}

.mobile-menu__socials a:hover {
  color: var(--color-accent);
  transition-duration: 0.2s;
  transition-delay: 0s;
}

@media (max-width: 900px) {
  .pill-nav {
    display: none;
  }

  .nav-toggle {
    display: inline-flex;
  }
}

/* width:100% makes this reserve the full row once .site-nav wraps the brand and menu onto
   separate lines at this width — without it, .site-nav's own justify-content:space-between has
   nothing left to space a single wrapped item away from, so it would just sit at its own
   shrink-wrapped width. align-items intentionally isn't touched here (stays the default
   flex-end, set where .site-nav__menu is first declared) — an earlier flex-start override here
   pulled the socials row and the Menu+ toggle to the left at this width, misaligned from how
   they sit (right-aligned) at every wider breakpoint; confirmed removing it was the actual fix,
   not something that also needed a replacement value. */
@media (max-width: 640px) {
  .site-nav__menu {
    width: 100%;
  }
}

/* Touch-only: the pill nav is already replaced by the Menu+ toggle below 900px (see that
   breakpoint above), so the bar at rest is just the brand lockup plus that one button — hiding
   the socials row and shrinking the brand gets both of those onto a single line instead of two
   (sticky itself now applies sitewide, above, not just here — a slim one-line bar just makes
   more sense to pin in place than the full two-row desktop version would). Applies sitewide
   (this partial is shared by every page), not just Home. Deliberately placed *after* the
   max-width:640px rule above, not just after it in the source for tidiness — a real bug, not
   just organization: both rules target .site-nav__menu at equal specificity, so on any real
   phone (almost always under 640px wide) the later one in the file wins regardless of which
   condition "should" logically take precedence; originally placed earlier in this file, its own
   width:auto here was silently overridden right back to width:100% by that rule, which is
   exactly why the brand and the Menu+ button stayed on two lines on a real device despite this
   rule visibly being in the stylesheet. */
@media (hover: none) {
  .site-nav__socials {
    display: none;
  }

  .site-nav__logo {
    width: 40px;
    height: 40px;
  }

  .site-nav__wordmark-line--main {
    font-size: 1.1rem;
  }

  .site-nav__wordmark-line--sub {
    font-size: 0.75rem;
  }

  .site-nav__menu {
    width: auto;
  }
}

.site-footer {
  padding: var(--space-4);
  text-align: center;
  color: var(--color-text-muted);
  font-size: 0.85rem;
}

.placeholder {
  padding: var(--space-6) var(--space-4);
  text-align: center;
  color: var(--color-text-muted);
}

/* Shared by js/tilted-card.js wherever it's used (Music timeline artwork, Bio grid tiles) — the
   perspective has to live on an ancestor of whatever element actually rotates for rotateX/rotateY
   to read as real 3D tilt rather than a flat skew, and position:relative is what lets an optional
   tooltip position itself against the card's own box rather than the page. */
.tilted-card {
  position: relative;
  perspective: 600px;
}

/* Shared by js/split-flap-text.js wherever it's used — a vanilla port of reactbits.dev's "Split
   Flap Text" (confirmed the real CSS/markup directly from its own component source). Font
   deliberately overridden to the site's own Gill Sans MT rather than the original's monospace
   stack, per request — the tile grid itself (fixed 0.78em-wide tiles, centred glyphs) was designed
   around a monospace font where every character has identical advance width, so a proportional
   font like Gill Sans MT will show slightly more breathing room around narrow letters (an "I"
   won't fill its tile the way an "M" does); nothing breaks, it just reads a little less
   machine-precise than the original's true-monospace look. font-weight dropped from the original's
   760 to 400 for the same reason base.css's own h1 uses 400 — only a 400 (and a 300 "Light") Gill
   Sans MT face is actually loaded, so anything heavier would be the browser's own synthetic
   (faux-bold) rendering, not a real bold face. Tile/text colours also swapped from the original's
   dark near-black board for a light one matching this site's own palette (`--color-border` /
   `--color-text`) instead of dropping a stark dark box onto an otherwise all-white page. */
.split-flap-text {
  display: inline-flex;
  align-items: center;
  gap: var(--split-flap-gap, 6px);
  color: var(--split-flap-text-color, var(--color-text));
  font-family: var(--font-heading);
  font-size: var(--split-flap-font-size, 52px);
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.035em;
  white-space: pre;
  user-select: none;
  font-variant-numeric: tabular-nums;
}

.split-flap-text__tile {
  position: relative;
  width: 0.78em;
  height: 1.08em;
  overflow: hidden;
  border-radius: var(--split-flap-radius, 8px);
  background:
    radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.5), transparent 44%),
    linear-gradient(
      180deg,
      color-mix(in srgb, var(--split-flap-tile-color, var(--color-border)) 82%, white),
      var(--split-flap-tile-color, var(--color-border))
    );
  box-shadow:
    0 0.035em 0.08em rgba(255, 255, 255, 0.4) inset,
    0 -0.05em 0.1em rgba(0, 0, 0, 0.12) inset,
    0 0.16em 0.38em rgba(0, 0, 0, 0.1);
  perspective: 520px;
  transform-style: preserve-3d;
  isolation: isolate;
}

.split-flap-text__tile::before {
  content: '';
  position: absolute;
  z-index: 8;
  top: calc(50% - 0.5px);
  left: 0;
  width: 100%;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(255, 255, 255, 0.5) 18%,
    rgba(0, 0, 0, 0.2) 50%,
    rgba(255, 255, 255, 0.4) 82%,
    transparent
  );
  box-shadow:
    0 -1px 0 rgba(255, 255, 255, 0.25),
    0 1px 0 rgba(0, 0, 0, 0.15);
  pointer-events: none;
}

.split-flap-text__tile::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 9;
  border: 1px solid rgba(0, 0, 0, 0.06);
  border-radius: inherit;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3) inset;
  pointer-events: none;
}

.split-flap-text__half,
.split-flap-text__flap {
  position: absolute;
  left: 0;
  width: 100%;
  height: 50%;
  overflow: hidden;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.5), transparent 34%), var(--split-flap-tile-color, var(--color-border));
  backface-visibility: hidden;
}

.split-flap-text__half--top,
.split-flap-text__flap--front {
  top: 0;
}

.split-flap-text__half--bottom,
.split-flap-text__flap--back {
  bottom: 0;
}

.split-flap-text__half--bottom,
.split-flap-text__flap--back {
  background:
    linear-gradient(0deg, rgba(255, 255, 255, 0.4), transparent 38%),
    color-mix(in srgb, var(--split-flap-tile-color, var(--color-border)) 92%, black);
}

.split-flap-text__char {
  position: absolute;
  left: 0;
  width: 100%;
  height: 200%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--split-flap-text-color, var(--color-text));
  text-shadow: 0 0.025em 0 rgba(255, 255, 255, 0.4);
}

.split-flap-text__half--top .split-flap-text__char,
.split-flap-text__flap--front .split-flap-text__char {
  top: 0;
}

.split-flap-text__half--bottom .split-flap-text__char,
.split-flap-text__flap--back .split-flap-text__char {
  bottom: 0;
}

.split-flap-text__flap {
  z-index: 6;
  will-change: transform, filter;
  transform-style: preserve-3d;
}

.split-flap-text__flap--front {
  transform-origin: center bottom;
  animation: split-flap-front var(--split-flap-flip-duration, 0.12s) cubic-bezier(0.23, 1, 0.32, 1) both;
}

.split-flap-text__flap--back {
  transform-origin: center top;
  transform: rotateX(90deg);
  animation: split-flap-back var(--split-flap-flip-duration, 0.12s) cubic-bezier(0.23, 1, 0.32, 1) both;
}

@keyframes split-flap-front {
  0% {
    transform: rotateX(0deg);
    filter: brightness(1.06);
  }

  100% {
    transform: rotateX(-90deg);
    filter: brightness(0.85);
  }
}

@keyframes split-flap-back {
  0%,
  45% {
    transform: rotateX(90deg);
    filter: brightness(0.85);
  }

  100% {
    transform: rotateX(0deg);
    filter: brightness(1);
  }
}

/* The board's own tile count is fixed at the longest word among however many are passed in (every
   shorter word pads out to that same width with blank tiles, matching a real departure board) — on
   Gigs that's "CONCIERTOS" at 10 tiles. At the site's normal h1 size that's wide enough to overflow
   a phone viewport well before any of the sitewide nav/layout breakpoints kick in (confirmed
   directly at 375px), so this needs its own, smaller floor below the h1 clamp()'s own 2.5rem
   minimum — the JS sets --split-flap-font-size inline, but that only feeds the *variable*; a later
   plain (no !important needed) font-size rule for the same element still wins normally by cascade
   order once this query is active. */
@media (max-width: 640px) {
  .split-flap-text {
    font-size: 1.75rem;
    gap: 4px;
  }
}

/* A vanilla port of reactbits.dev's "Shiny Text" — pure CSS, no JS at all needed: a linear
   gradient with a bright band at its midpoint, clipped to the text itself (background-clip:text +
   text-fill-color:transparent), with the gradient's own background-position swept back and forth
   by a keyframe animation so the bright band travels across the letters. Colours swapped from the
   original's near-white base/white shine (built for a dark demo background) to this site's own
   muted h1 grey as the base and the accent red as the "shine" — on this site's white page, a
   literal white shine sweeping across text would nearly vanish into the background at its peak
   instead of reading as a highlight; the accent red stays legible against white at every point in
   the sweep and doubles as a glint that matches the sitewide red-accent language used everywhere
   else. Timing (a 2s sweep + 3s hold, not the original's non-stop 2s loop) is a deliberate
   slow-down too — a marketing demo can get away with a shine sweeping by every 2 seconds forever,
   but a page's own static heading, sat there while someone's actually reading the page below it,
   reads calmer with a longer pause between glints. */
.shiny-text {
  display: inline-block;
  background-image: linear-gradient(
    120deg,
    var(--shiny-text-base, var(--color-text-muted)) 0%,
    var(--shiny-text-base, var(--color-text-muted)) 35%,
    var(--shiny-text-shine, var(--color-accent)) 50%,
    var(--shiny-text-base, var(--color-text-muted)) 65%,
    var(--shiny-text-base, var(--color-text-muted)) 100%
  );
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: shiny-text-sweep 5s linear infinite;
}

@keyframes shiny-text-sweep {
  0% {
    background-position: 150% center;
  }

  40% {
    background-position: -50% center;
  }

  100% {
    background-position: -50% center;
  }
}

/* Shared by js/border-glow.js wherever it's used — a vanilla port of reactbits.dev's "Border
   Glow": a pointer-tracked colour that traces whichever edge the cursor is nearest, via two custom
   properties (--edge-proximity, --cursor-angle, updated on pointermove) driving a masked colour
   border, a soft inner fill, and a spotlight-shaped outer bloom that intentionally extends past
   the card's own box (that's what the negative `inset` on .edge-light is for). Three deliberate
   departures from the original, all for the same reason — this specific instance wraps a Tilted
   Card (js/tilted-card.js) on the Music page's detail-panel artwork, which needs to coexist with
   it: (1) .border-glow-inner's overflow is `visible`, not the original's `auto` — Tilted Card's
   own hover scale/tilt needs to be free to bulge slightly past its own edges (learned the hard way
   on the Bio grid, see js/tilted-card.js), and `auto` would clip that bulge right at this
   wrapper's boundary; (2) the outer bloom's `mix-blend-mode` is dropped from the original's
   `plus-lighter` to normal blending — `plus-lighter` *adds* light, which reads as a glow against
   the original's dark demo background but does almost nothing against this site's white page (adding
   light to white stays white); (3) colours/background swapped to the site's own white/red instead
   of the demo's dark card + purple-pink-blue mesh, and glow/border radii scaled down to suit a
   240px artwork thumbnail rather than the demo's large rounded panel. */
.border-glow-card {
  --edge-proximity: 0;
  --cursor-angle: 45deg;
  --edge-sensitivity: 30;
  --color-sensitivity: calc(var(--edge-sensitivity) + 20);
  --border-radius: 6px;
  --glow-padding: 20px;
  --cone-spread: 25;

  position: relative;
  border-radius: var(--border-radius);
  isolation: isolate;
  transform: translate3d(0, 0, 0.01px);
  display: grid;
  border: 1px solid rgba(0, 0, 0, 0.08);
  background: var(--card-bg, #ffffff);
  overflow: visible;
}

.border-glow-card::before,
.border-glow-card::after,
.border-glow-card > .edge-light {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  transition: opacity 0.25s ease-out;
  z-index: -1;
}

.border-glow-card:not(:hover):not(.sweep-active)::before,
.border-glow-card:not(:hover):not(.sweep-active)::after,
.border-glow-card:not(:hover):not(.sweep-active) > .edge-light {
  opacity: 0;
  transition: opacity 0.75s ease-in-out;
}

.border-glow-card::before {
  border: 1px solid transparent;
  background:
    linear-gradient(var(--card-bg, #ffffff) 0 100%) padding-box,
    linear-gradient(rgb(255 255 255 / 0%) 0% 100%) border-box,
    var(--gradient-one) border-box,
    var(--gradient-two) border-box,
    var(--gradient-three) border-box,
    var(--gradient-four) border-box,
    var(--gradient-five) border-box,
    var(--gradient-six) border-box,
    var(--gradient-seven) border-box,
    var(--gradient-base) border-box;
  opacity: calc((var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));
  mask-image: conic-gradient(
    from var(--cursor-angle) at center,
    black calc(var(--cone-spread) * 1%),
    transparent calc((var(--cone-spread) + 15) * 1%),
    transparent calc((100 - var(--cone-spread) - 15) * 1%),
    black calc((100 - var(--cone-spread)) * 1%)
  );
}

.border-glow-card::after {
  border: 1px solid transparent;
  background:
    var(--gradient-one) padding-box,
    var(--gradient-two) padding-box,
    var(--gradient-three) padding-box,
    var(--gradient-four) padding-box,
    var(--gradient-five) padding-box,
    var(--gradient-six) padding-box,
    var(--gradient-seven) padding-box,
    var(--gradient-base) padding-box;
  mask-image:
    linear-gradient(to bottom, black, black),
    radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%),
    radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%),
    radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%),
    radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%),
    radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%),
    conic-gradient(from var(--cursor-angle) at center, transparent 5%, black 15%, black 85%, transparent 95%);
  mask-composite: subtract, add, add, add, add, add;
  opacity: calc(var(--fill-opacity, 0.4) * (var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));
  mix-blend-mode: soft-light;
}

.border-glow-card > .edge-light {
  inset: calc(var(--glow-padding) * -1);
  pointer-events: none;
  z-index: 1;
  mask-image: conic-gradient(
    from var(--cursor-angle) at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%
  );
  opacity: calc((var(--edge-proximity) - var(--edge-sensitivity)) / (100 - var(--edge-sensitivity)));
}

.border-glow-card > .edge-light::before {
  content: '';
  position: absolute;
  inset: var(--glow-padding);
  border-radius: inherit;
  box-shadow:
    inset 0 0 0 1px var(--glow-color, hsl(4deg 99% 51% / 100%)),
    inset 0 0 1px 0 var(--glow-color-60, hsl(4deg 99% 51% / 60%)),
    inset 0 0 3px 0 var(--glow-color-50, hsl(4deg 99% 51% / 50%)),
    inset 0 0 6px 0 var(--glow-color-40, hsl(4deg 99% 51% / 40%)),
    inset 0 0 15px 0 var(--glow-color-30, hsl(4deg 99% 51% / 30%)),
    inset 0 0 25px 2px var(--glow-color-20, hsl(4deg 99% 51% / 20%)),
    inset 0 0 50px 2px var(--glow-color-10, hsl(4deg 99% 51% / 10%)),
    0 0 1px 0 var(--glow-color-60, hsl(4deg 99% 51% / 60%)),
    0 0 3px 0 var(--glow-color-50, hsl(4deg 99% 51% / 50%)),
    0 0 6px 0 var(--glow-color-40, hsl(4deg 99% 51% / 40%)),
    0 0 15px 0 var(--glow-color-30, hsl(4deg 99% 51% / 30%)),
    0 0 25px 2px var(--glow-color-20, hsl(4deg 99% 51% / 20%)),
    0 0 50px 2px var(--glow-color-10, hsl(4deg 99% 51% / 10%));
}

.border-glow-inner {
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: visible;
  z-index: 1;
}

@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
    animation: none !important;
  }
}
