/* ===========================
   CSS Custom Properties
   =========================== */
:root {
    --color-bg: #201d1d;
    --color-bar-bg: #000;
    --color-rule: rgba(255, 255, 255, 0.35);
    --rule: 1px solid var(--color-rule);
    --color-text: #fff;
    --color-overlay: rgba(0, 0, 0, 0.85);
    --font-display: 'Oswald', sans-serif;
    /* Newspaper type pair (Family, Release, Gigs only — never body/navbar/banner):
       a sturdy serif for prose, a typewriter face for headlines/roles/bylines. */
    --font-body: 'Libre Baskerville', 'Georgia', serif;
    --font-mono: 'Courier Prime', 'Courier New', monospace;
    --gradient-height: 100px;
    --nav-padding: 1.2rem 4rem;
    --icon-size: 24px;
    --icon-gap: 1.25rem;
    --logo-height: 64px;
    --topbar-height: 46px;
    --bottom-bar-height: 48px;
    --mobile-bar-height: 60px;
    --container: 1140px;
}

/* ===========================
   Reset & Base
   =========================== */
*,
*::before,
*::after {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html {
    height: 100%;
    scroll-behavior: smooth;
    scroll-padding-top: calc(var(--topbar-height) + 1rem);
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }
}

/* The loading overlay (below) visually covers the page but doesn't stop it
   being scrollable on its own - this class (toggled by the inline script in
   banner_block.html) locks the real page's scrollbar away while it's up. */
html.has-loading-overlay,
html.has-loading-overlay body {
    overflow: hidden;
}

/* Sticky-footer layout: `.copyright` (the only other in-flow child --
   everything else in the frame is position:fixed) gets margin-top:auto
   below, so it always sits at the true bottom of the viewport even when
   the page content doesn't fill it, instead of floating mid-page. */
body {
    min-height: 100%;
    display: flex;
    flex-direction: column;
    background: var(--color-bg);
    color: var(--color-text);
}

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

ul {
    list-style: none;
}

button {
    background: none;
    border: none;
    cursor: pointer;
    color: inherit;
}

/* ===========================
   Torn Paper
   A magazine-cutout treatment shared by the Family member photos and the
   Release cover art. Four nested layers, because CSS applies `filter`
   before `mask`: a drop-shadow set on a masked element is clipped away
   by that element's own silhouette, so every shadow here has to sit on
   an unmasked wrapper.

       .torn          unmasked, casts the shadow onto the page
       .torn__paper   masked with the outer contour -> paper backing
       .torn__lift    unmasked, casts the photo's shadow onto the paper
       .torn__photo   masked with the inner contour -> the image

   Layout (sizing, positioning) stays with the host component; only the
   material lives here. Each .torn--N supplies a pair of contours cut
   from one tear, so the rim between them varies in width the way real
   torn paper does. Add .torn--tilted to also take the variant's angle.
   =========================== */
.torn {
    /* Two tight contact shadows hugging the torn edge, so the cutout
       reads as paper resting on the page rather than a hole in it. Kept
       deliberately short-throw: a wide, far-offset shadow would float
       it well above the page instead. The page is #201d1d, not true
       black, so a dark shadow still registers against it. */
    filter:
        drop-shadow(0 1px 1px rgba(0, 0, 0, 0.65))
        drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
}

.torn--tilted {
    transform: rotate(var(--torn-rotate));
}

.torn__paper {
    background: #cec6b4;
    mask-image: var(--torn-shape);
    mask-repeat: no-repeat;
    mask-size: 100% 100%;
    -webkit-mask-image: var(--torn-shape);
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
}

/* The image sits a hair proud of its backing, so it casts its own small
   shadow onto the paper rim. */
.torn__lift {
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.45));
}

.torn__photo {
    mask-image: var(--torn-photo-shape);
    mask-repeat: no-repeat;
    mask-size: 100% 100%;
    -webkit-mask-image: var(--torn-photo-shape);
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
}

.torn--1 {
    --torn-shape: url("../img/torn/torn-1.49403260d62f.svg");
    --torn-photo-shape: url("../img/torn/torn-1-photo.1dc5216ac1ac.svg");
    --torn-rotate: -3deg;
}

.torn--2 {
    --torn-shape: url("../img/torn/torn-2.edf83d23d556.svg");
    --torn-photo-shape: url("../img/torn/torn-2-photo.360821d29d61.svg");
    --torn-rotate: 2deg;
}

.torn--3 {
    --torn-shape: url("../img/torn/torn-3.27c1b1634a13.svg");
    --torn-photo-shape: url("../img/torn/torn-3-photo.6d0136ed6db5.svg");
    --torn-rotate: -2deg;
}

.torn--4 {
    --torn-shape: url("../img/torn/torn-4.86a3d939bf34.svg");
    --torn-photo-shape: url("../img/torn/torn-4-photo.1a9b46677c56.svg");
    --torn-rotate: 3deg;
}

.torn--5 {
    --torn-shape: url("../img/torn/torn-5.3207fb9e8f1d.svg");
    --torn-photo-shape: url("../img/torn/torn-5-photo.446a9f040509.svg");
    --torn-rotate: -1.5deg;
}

.torn--6 {
    --torn-shape: url("../img/torn/torn-6.899a8f155451.svg");
    --torn-photo-shape: url("../img/torn/torn-6-photo.1ef43737ce05.svg");
    --torn-rotate: 1.5deg;
}

/* Square-cut variants. The masks stretch to whatever box they're given,
   so the portrait contours above would flatten and bite unevenly into
   square artwork -- release covers use these instead. */
.torn--sq-1 {
    --torn-shape: url("../img/torn/torn-sq-1.818e1be812f5.svg");
    --torn-photo-shape: url("../img/torn/torn-sq-1-photo.eb9ffb3a7d62.svg");
    --torn-rotate: -2deg;
}

.torn--sq-2 {
    --torn-shape: url("../img/torn/torn-sq-2.c9977ac669fd.svg");
    --torn-photo-shape: url("../img/torn/torn-sq-2-photo.025d230bbedc.svg");
    --torn-rotate: 2.5deg;
}

.torn--sq-3 {
    --torn-shape: url("../img/torn/torn-sq-3.ed2476323048.svg");
    --torn-photo-shape: url("../img/torn/torn-sq-3-photo.5f032dd8d0a1.svg");
    --torn-rotate: -1.5deg;
}

/* ===========================
   Banner Section
   =========================== */
.banner {
    position: relative;
    width: 100%;
    height: 100vh;
    height: 100dvh;
    border-bottom: var(--rule);
}

.banner__image {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: grayscale(1);
    /* object-position is set inline per image, from its Wagtail focal point */
}

/* Sequence (2-5 images, ADR-0005): stacked and cross-faded, no controls. A
   single-image Banner never gets these rules - display:block above already
   renders it full-bleed with no added cost. */
.banner--sequence .banner__image {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 1.5s ease;
}

.banner--sequence .banner__image.is-active {
    opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
    .banner--sequence .banner__image {
        transition: none;
    }
}

.banner__copy {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 2rem;
}

.banner__heading {
    font-family: var(--font-display);
    font-size: clamp(2rem, 6vw, 4.5rem);
    text-shadow: 0 2px 12px rgba(0, 0, 0, 0.6);
    text-transform: uppercase;
}

/* ===========================
   Loading Overlay
   Only ever exists in the DOM if JS cloned it out of the <template> in
   banner_block.html (see the inline script there) - so with JS disabled or
   broken, nothing ever shows. Hidden on window "load", not on the banner
   image alone, so it covers page-load in general, not just the banner.
   =========================== */
.loading-overlay {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.5rem;
    overflow: hidden;
    background: var(--color-bg);
    transition: opacity 0.5s ease;
}

.loading-overlay.is-hidden {
    opacity: 0;
    pointer-events: none;
}

.loading-overlay__logo {
    height: 96px;
    width: auto;
    animation: loading-overlay-zoom 0.9s ease-in-out infinite;
}

@keyframes loading-overlay-zoom {
    0%,
    100% {
        transform: scale(0.85);
    }
    50% {
        transform: scale(1.1);
    }
}

/* "Zoom through": the logo keeps scaling up while fading, rather than
   reversing - reads as flying into and past it, not just disappearing.
   Scale(80) takes a 96px logo well past 7680px, so it overshoots the
   viewport on any screen size instead of just growing to a
   bigger-but-still-bounded icon. A dedicated keyframe animation (not a
   transition off the idle animation) is used deliberately: swapping
   `animation: none` + a transition in the same class toggle is a known
   cross-browser trap where the transition can skip straight to its end
   value instead of playing - a fresh named animation always plays out
   its own keyframes in full. */
.loading-overlay.is-hidden .loading-overlay__logo {
    animation: loading-overlay-zoom-through 0.9s ease-in forwards;
}

@keyframes loading-overlay-zoom-through {
    0% {
        transform: scale(1);
        opacity: 1;
    }
    100% {
        transform: scale(100);
        opacity: 0;
    }
}

.loading-overlay__text {
    font-family: var(--font-display);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 0.9rem;
    opacity: 0.7;
    display: flex;
}

.loading-overlay__dots {
    display: inline-flex;
    width: 1.5em;
}

.loading-overlay__dot:nth-child(1) {
    opacity: 1;
}

.loading-overlay__dot:nth-child(2) {
    opacity: 0;
    animation: loading-overlay-dot-2 1.5s linear infinite;
}

.loading-overlay__dot:nth-child(3) {
    opacity: 0;
    animation: loading-overlay-dot-3 1.5s linear infinite;
}

@keyframes loading-overlay-dot-2 {
    0%,
    33.32% {
        opacity: 0;
    }
    33.34%,
    100% {
        opacity: 1;
    }
}

@keyframes loading-overlay-dot-3 {
    0%,
    66.65% {
        opacity: 0;
    }
    66.67%,
    100% {
        opacity: 1;
    }
}

@media (prefers-reduced-motion: reduce) {
    .loading-overlay__logo {
        animation: none;
    }

    .loading-overlay.is-hidden .loading-overlay__logo {
        animation: none;
        opacity: 0;
        transition: opacity 0.6s ease;
    }
}

/* ===========================
   Release Section
   =========================== */
.release {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 3rem;
    max-width: var(--container);
    margin: 0 auto;
    padding: 4rem;
    font-family: var(--font-body);
    font-size: 1.05rem;
    line-height: 1.7;
}

/* The torn layers are plain block boxes here (unlike the Family gallery,
   which stacks crossfading images and so positions them absolutely) --
   the cover art itself is in flow and gives all four layers its size. */
.release__cover {
    flex: 0 1 320px;
    min-width: 0;
    max-width: 480px;
    width: 100%;
}

.release__cover-art {
    display: block;
    width: 100%;
    height: auto;
}

.release__copy {
    display: flex;
    flex: 1 1 320px;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
    min-width: 0;
    max-width: 40rem;
    text-align: center;
}

.release__heading {
    font-family: var(--font-display);
    font-size: clamp(1.75rem, 4vw, 3rem);
    text-transform: uppercase;
}

.release__legend {
    font-size: 1.1rem;
    opacity: 0.85;
}

.release__platform-links {
    display: flex;
    align-items: center;
    gap: 1.5rem;
    margin-top: 1rem;
}

.release__platform-link {
    display: inline-flex;
    transition: opacity 0.2s ease;
}

.release__platform-link:hover {
    opacity: 0.7;
}

/* Logos rendered large, per the ticket: the real brand marks at a consistent size. */
.platform-link__logo {
    width: 48px;
    height: 48px;
}

/* ===========================
   Family Section
   =========================== */
/* Family goes light - paper, not the site's dark chrome (Banner/topbar/
   bottom-bar/mobile-nav stay black; Release and Gigs stay dark too, since
   they're data display rather than the storytelling this contrast is
   for). The noise is a tiled, desaturated feTurbulence SVG compressed into
   a near-white band (see the feComponentTransfer below) and multiplied
   over the ivory base, so it reads as visible paper fiber/grain without
   tipping into static. Overriding --color-rule here (rather than editing
   every rule/divider individually) is deliberate: every var(--rule)
   consumer already inside .family - the Article's headline border, its
   two-column divider - picks up this ivory-appropriate value for free,
   through the same custom-property cascade that already threads --rule
   through the rest of the page. */
.family {
    /* Full-bleed - the paper runs edge to edge; .family__inner below is
       what actually gets capped to the page's usual reading column. */
    background-color: #f2ede1;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch' result='noise'/%3E%3CfeColorMatrix in='noise' type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.45' intercept='0.6'/%3E%3CfeFuncG type='linear' slope='0.45' intercept='0.6'/%3E%3CfeFuncB type='linear' slope='0.45' intercept='0.6'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    background-blend-mode: multiply;
    color: #2a2420;
    /* Solid, not translucent: a low-alpha tint (the site's usual --rule)
       reads as a fairly light gray once composited over ivory rather than
       the dark backdrop it's tuned for - the same backdrop-dependent
       math that bit the Phrase above. A real column rule is a solid line,
       not a faint one, anyway. */
    --rule: 1px solid #1c191774;
}

.family__inner {
    display: flex;
    flex-direction: column;
    gap: 3rem;
    max-width: var(--container);
    margin: 0 auto;
    padding: 4rem;
    font-family: var(--font-body);
    font-size: 0.95rem;
    line-height: 1.7;
}

.family__heading {
    font-family: var(--font-display);
    font-size: clamp(1.75rem, 4vw, 3rem);
    text-align: center;
    text-transform: uppercase;
}

/* ---- Article: a headline + prose set in newspaper columns ---- */

.article {
    width: 100%;
}

.article__headline {
    font-family: var(--font-mono);
    font-size: clamp(1rem, 2.2vw, 1.3rem);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: 1rem;
    padding-bottom: 0.5rem;
    border-bottom: var(--rule);
}

.article__prose {
    opacity: 0.9;
}

.article__prose p + p {
    margin-top: 1em;
}

/* Draftail's "bold" feature writes <b>, not <strong> (its contentstate
   converter maps BOLD -> "b"), so the highlight has to match both. */
.article__prose strong,
.article__prose b {
    background: rgba(178, 34, 34, 0.3);
    padding: 0.05em 0.3em;
    border-radius: 0.2em;
}

/* True newspaper flow: text is one continuous river poured into both
   columns, so a paragraph may break across the boundary and the columns
   stay flush at the bottom. A one-column Article runs the same full
   width as a two-column one - a real newspaper's opening lede is set
   exactly this wide, just as a single wide column. */
.article--columns-two .article__prose,
.article--columns-one .article__prose {
    text-align: justify;
    hyphens: auto;
}

.article--columns-two .article__prose {
    columns: 20rem 2;
    column-gap: 2.5rem;
    column-rule: var(--rule);
}

/* One drop cap per Article, on its opening paragraph only - not every
   paragraph, the way a real newspaper piece opens. `background: transparent`
   guards against ::first-letter ever picking up the bold-highlight rule
   above when a paragraph happens to open on highlighted text. */
.article__prose > p:first-of-type::first-letter {
    float: left;
    font-family: var(--font-body);
    font-weight: 700;
    font-size: 3.6em;
    line-height: 0.82;
    margin: 0.05em 0.08em 0 0;
    background: transparent;
}

/* ---- Lineup: one or more Band Members, carousel only when there are 2+ ---- */

.lineup__carousel {
    display: flex;
    align-items: center;
    gap: 1.5rem;
}

.lineup__members {
    flex: 1;
    min-width: 0;
}

.lineup__member {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 2rem;
}

.lineup__member[hidden] {
    display: none;
}

.lineup__gallery {
    position: relative;
    width: 100%;
    max-width: 320px;
    aspect-ratio: 3 / 4;
    flex-shrink: 0;
}

.lineup__gallery-paper,
.lineup__gallery-photo-lift,
.lineup__gallery-photo {
    position: absolute;
    inset: 0;
}

.lineup__gallery-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0;
    transition: opacity 1s ease;
}

.lineup__gallery-image.is-active {
    opacity: 1;
}

.lineup__copy {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    max-width: 32rem;
}

.lineup__name {
    font-family: var(--font-body);
    font-size: clamp(1.4rem, 3vw, 2rem);
}

.lineup__role {
    font-family: var(--font-mono);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    opacity: 0.6;
    font-size: 0.9rem;
}

.lineup__bio {
    opacity: 0.85;
    line-height: 1.7;
}

.lineup__nav {
    font-family: var(--font-display);
    font-size: 2rem;
    line-height: 1;
    opacity: 0.6;
    transition: opacity 0.2s ease;
    flex-shrink: 0;
}

.lineup__nav:hover {
    opacity: 1;
}

.lineup__dots {
    display: flex;
    justify-content: center;
    gap: 0.6rem;
}

.lineup__dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    /* Ink-based, not the site's usual white dot - a Lineup only ever
       renders inside the (now light) Family. */
    background: rgba(42, 36, 32, 0.25);
    transition: background 0.2s ease;
    padding: 0;
}

.lineup__dot.is-active {
    background: #2a2420;
}

@media (prefers-reduced-motion: reduce) {
    .lineup__gallery-image {
        transition: none;
    }
}

/* ===========================
   Gigs Section
   =========================== */
.gigs {
    display: flex;
    flex-direction: column;
    gap: 2rem;
    max-width: var(--container);
    margin: 0 auto;
    padding: 4rem;
    font-family: var(--font-body);
    font-size: 1.05rem;
    line-height: 1.7;
}

.gigs__heading {
    font-family: var(--font-display);
    font-size: clamp(1.75rem, 4vw, 3rem);
    text-transform: uppercase;
}

/* Table-like alignment: `.gigs__item` is `display: contents` so its
   fields become direct grid children of `.gigs__list`, lining up in
   columns across rows. Every field is always rendered (even empty) so
   the column count per row never shifts. Collapses to a stacked card
   per gig below 768px — see the mobile media query. */
.gigs__list {
    display: grid;
    grid-template-columns: 8rem 1fr 1fr 1fr auto;
    column-gap: 1.5rem;
}

.gigs__list--past {
    grid-template-columns: 5rem 8rem 1fr 1fr 1fr;
    opacity: 0.5;
}

.gigs__item {
    display: contents;
}

.gigs__date,
.gigs__event-name,
.gigs__venue,
.gigs__location,
.gigs__ticket-cta-cell,
.gigs__past-label {
    display: flex;
    align-items: center;
    padding: 0.75rem 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}

.gigs__list > .gigs__item:last-child > * {
    border-bottom: none;
}

.gigs__date {
    font-family: var(--font-display);
}

.gigs__event-name {
    font-family: var(--font-display);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.gigs__venue {
    font-weight: 700;
}

.gigs__location {
    opacity: 0.6;
}

.gigs__ticket-cta-cell {
    justify-content: flex-end;
}

.gigs__ticket-cta {
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 0.85rem;
    border: 1px solid rgba(255, 255, 255, 0.3);
    padding: 0.5rem 1.2rem;
    transition: border-color 0.2s ease, background 0.2s ease;
}

.gigs__ticket-cta:hover {
    border-color: rgba(255, 255, 255, 0.6);
    background: rgba(255, 255, 255, 0.05);
}

.gigs__empty {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
}

.gigs__empty-message {
    opacity: 0.85;
    max-width: 32rem;
}

.gigs__past-label {
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 0.75rem;
}

/* ===========================
   Topbar (Desktop) - fixed at every scroll depth
   =========================== */
.topbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--topbar-height);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 4rem;
    z-index: 300;
    transition: background-color 0.3s ease, border-color 0.3s ease;
    border-bottom: 1px solid transparent;
}

/* Gradient scrim over the banner; replaced by a solid bar + rule once scrolled
   past the banner (frame.js toggles .is-scrolled via one IntersectionObserver) */
.topbar::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: var(--gradient-height);
    background: linear-gradient(to bottom, rgba(0, 0, 0, 0.98), rgba(0, 0, 0, 0.6), transparent);
    pointer-events: none;
    z-index: -1;
    opacity: 1;
    transition: opacity 0.3s ease;
}

.topbar.is-scrolled {
    background-color: var(--color-bar-bg);
    border-bottom-color: var(--color-rule);
}

.topbar.is-scrolled::before {
    opacity: 0;
}

.menu-items {
    display: flex;
    gap: 2rem;
    align-items: center;
}

.menu-items a {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 1.15rem;
    text-transform: uppercase;
    letter-spacing: 0.15em;
    opacity: 0.6;
    transition: opacity 0.2s ease;
}

.menu-items a:hover {
    opacity: 1;
}

.nav-logo img {
    height: var(--logo-height);
    width: auto;
    filter: grayscale(1);
    transition: filter 0.3s ease;
}

.nav-logo:hover img {
    filter: grayscale(0);
}

.topbar__left {
    display: flex;
    gap: 1.25rem;
    align-items: center;
}

/* Straddles the bar's bottom edge: half the logo sits inside the bar, half
   floats over the page below it. No shrink-on-scroll. */
.topbar__logo {
    position: absolute;
    left: 50%;
    top: 100%;
    transform: translate(-50%, -48%);
}

.topbar__right {
    display: flex;
    align-items: center;
}

.icon-link {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--icon-size);
    height: var(--icon-size);
    opacity: 0.6;
    transition: opacity 0.2s ease;
    padding: 0;
}

/* Desktop topbar shrink (its icons stay smaller than the mobile bar's) */
.topbar .icon-link {
    width: 20px;
    height: 20px;
}

.icon-link:hover {
    opacity: 1;
}

.icon-link svg {
    width: 100%;
    height: 100%;
}

.topbar__cta {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    opacity: 0.7;
    transition: opacity 0.2s ease;
}

.topbar__cta:hover {
    opacity: 1;
}

.topbar__cta svg {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
}

.topbar__cta-label {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
}

.topbar__divider {
    opacity: 0.35;
    font-weight: 300;
}

.locale-switcher {
    display: flex;
    gap: 0.5rem;
}

.locale-switcher__pill {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.35);
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.7rem;
    letter-spacing: 0.02em;
    opacity: 0.6;
    transition: opacity 0.2s ease, border-color 0.2s ease;
}

.locale-switcher__pill:hover,
.locale-switcher__pill[aria-current="page"] {
    opacity: 1;
    border-color: rgba(255, 255, 255, 0.7);
}

/* In-flow, right after the page content - no background/border of its own,
   just extra bottom padding so it clears the fixed bottom bar.
   margin-top: auto (body is a flex column) pins it to the true bottom of
   the viewport on short pages, and to right after the content on long
   ones -- same as a plain in-flow element once content overflows. */
.copyright {
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.8rem;
    font-weight: 300;
    letter-spacing: 0.03em;
    opacity: 0.25;
    margin: auto 0 0;
    padding: 2rem 1rem calc(var(--bottom-bar-height) + 2rem);
    text-align: center;
    white-space: nowrap;
}

/* ===========================
   Bottom Bar (Desktop + Mobile) - fixed, social links only
   =========================== */
.bottom-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: var(--bottom-bar-height);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--icon-gap);
    z-index: 300;
    transition: background-color 0.3s ease, border-color 0.3s ease;
    border-top: 1px solid transparent;
}

.bottom-bar::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: var(--gradient-height);
    background: linear-gradient(to top, rgba(0, 0, 0, 0.98), rgba(0, 0, 0, 0.6), transparent);
    pointer-events: none;
    z-index: -1;
    opacity: 1;
    transition: opacity 0.3s ease;
}

.bottom-bar.is-scrolled,
.bottom-bar.menu-open {
    background-color: var(--color-bar-bg);
    border-top-color: var(--color-rule);
}

.bottom-bar.is-scrolled::before,
.bottom-bar.menu-open::before {
    opacity: 0;
}

.bottom-bar a {
    display: flex;
    align-items: center;
    justify-content: center;
    transition: opacity 0.2s ease;
}

.bottom-bar a:hover {
    opacity: 0.6;
}

.bottom-bar svg {
    width: 20px;
    height: 20px;
}

/* ===========================
   Mobile Navigation Bar
   =========================== */
.mobile-nav-bar {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--mobile-bar-height);
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    padding: 0 1.25rem;
    z-index: 300;
    transition: background-color 0.3s ease, border-color 0.3s ease;
    border-bottom: 1px solid transparent;
}

/* Gradient scrim over the banner; replaced by a solid bar + rule once scrolled
   past the banner */
.mobile-nav-bar::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: linear-gradient(to bottom, rgba(0, 0, 0, 0.9), transparent);
    pointer-events: none;
    z-index: -1;
    opacity: 1;
    transition: opacity 0.3s ease;
}

.mobile-nav-bar.is-scrolled,
.mobile-nav-bar.menu-open {
    background-color: var(--color-bar-bg);
    border-bottom-color: var(--color-rule);
}

.mobile-nav-bar.is-scrolled::before,
.mobile-nav-bar.menu-open::before {
    opacity: 0;
}

.mobile-nav-bar__left {
    display: flex;
    gap: 1rem;
    align-items: center;
    justify-self: start;
}

.mobile-logo {
    justify-self: center;
}

.mobile-logo img {
    height: 40px;
    width: auto;
}

/* ===========================
   Hamburger Button
   =========================== */
.hamburger-button {
    display: flex;
    flex-direction: column;
    justify-content: center;
    justify-self: end;
    gap: 5px;
    width: 28px;
    height: 28px;
    padding: 0;
}

.hamburger-button span {
    display: block;
    width: 100%;
    height: 2px;
    background: var(--color-text);
    transition: transform 0.3s ease, opacity 0.3s ease;
    transform-origin: center;
}

.hamburger-button.active span:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
}

.hamburger-button.active span:nth-child(2) {
    opacity: 0;
}

.hamburger-button.active span:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
}

.mobile-home-link {
    justify-self: end;
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.85rem;
    text-transform: lowercase;
    letter-spacing: 0.05em;
    color: var(--color-text);
}

/* ===========================
   Mobile Menu Overlay
   =========================== */
.mobile-menu-overlay {
    display: none;
    position: fixed;
    /* Starts below the mobile bar and stops above the bottom bar rather than
       covering them, so the hamburger (now shown as an X) and the bottom
       bar's social links stay visible/usable instead of being ghosted behind
       this backdrop. */
    top: var(--mobile-bar-height);
    left: 0;
    right: 0;
    bottom: var(--bottom-bar-height);
    background: var(--color-overlay);
    z-index: 500;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

.mobile-menu-overlay.active {
    display: flex;
}

.overlay-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3rem;
}

.overlay-menu-items {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.5rem;
}

.overlay-menu-items a {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 2rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    transition: opacity 0.2s ease;
}

.overlay-menu-items a:hover {
    opacity: 0.6;
}

.locale-switcher--overlay .locale-switcher__pill {
    width: 2.2rem;
    height: 2.2rem;
    font-size: 0.85rem;
}

/* ===========================
   Scrolling Phrases (Desktop) - left edge only (CONTEXT.md's Phrase)
   =========================== */
.scrolling-phrases {
    position: fixed;
    z-index: 400;
    overflow: hidden;
    pointer-events: none;
    top: 0;
    bottom: 0;
    left: 0;
    width: 44px;
    /* Fades out near the top/bottom bars without knowing their exact pixel
       heights, so the strip never has to be coordinated with the frame. */
    mask-image: linear-gradient(transparent, black 30%, black 70%, transparent);
    -webkit-mask-image: linear-gradient(transparent, black 30%, black 70%, transparent);
}

.scrolling-phrases__track {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    white-space: nowrap;
    will-change: transform;
}

.scrolling-phrases__item {
    font-family: var(--font-display);
    font-size: 1rem;
    text-transform: uppercase;
    letter-spacing: 0.15em;
    /* A mid-tone, not white, is the deliberate choice here: difference-blend
       is |source - backdrop|, so a source near one extreme (white) is
       nearly IDENTICAL to a light backdrop (diff ~ 0) and can only be made
       visible there by cranking opacity so high it overshoots to solid
       black, while also being maximally loud against the dark chrome. A
       source near the midpoint between the dark chrome (~#201d1d) and the
       Family's ivory paper (~#f2ede1) gives the same diff magnitude against
       both - the fixed result then reads as light relative to the dark
       chrome and dark relative to the light paper, which is the inversion
       we're after, without any opacity tuning. */
    color: #8a857e;
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    /* Computed continuously per pixel against whatever's actually behind
       the strip at that point, rather than a single toggle for the whole
       strip (which can't be right the moment a dark/light Section boundary
       sits mid-viewport). Accepted trade-off: over the Banner's actual
       photo colors, a difference blend can look tinted/off rather than a
       clean gray - not a bug, just what this technique costs against a
       photo instead of a flat color. */
    mix-blend-mode: difference;
}

/* ===========================
   Corner Link Buttons
   =========================== */
.corner-link {
    font-family: 'Courier New', Courier, monospace;
    font-weight: 300;
    font-size: 1.2rem;
    text-transform: lowercase;
    letter-spacing: 0.03em;
    opacity: 0.35;
    transition: opacity 0.2s ease;
    text-align: left;
    padding: 0;
}

.corner-link:hover {
    opacity: 0.8;
}

/* ===========================
   Form Modal Overlay
   =========================== */
.form-modal {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 600;
    align-items: center;
    justify-content: center;
}

.form-modal.active {
    display: flex;
}

.form-modal__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.94);
}

.form-modal__content {
    position: relative;
    width: 100%;
    max-width: 460px;
    padding: 3rem 2.5rem;
    max-height: 100vh;
    overflow-y: auto;
}

/* Fixed to the viewport corner (not the narrow centered content card) so it
   lands in a comfortable, consistent spot at every screen size - same idea
   as the hamburger button's corner position. */
.form-modal__close {
    position: fixed;
    top: 1.5rem;
    right: 1.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    font-size: 2.75rem;
    opacity: 0.4;
    transition: opacity 0.2s ease;
    padding: 0;
    line-height: 1;
}

.form-modal__close:hover {
    opacity: 0.8;
}

.form-modal__title {
    font-family: 'Courier New', Courier, monospace;
    font-weight: 300;
    font-size: 1.4rem;
    text-transform: lowercase;
    letter-spacing: 0.05em;
    margin-bottom: 0.5rem;
}

.form-modal__subtitle {
    font-family: 'Courier New', Courier, monospace;
    font-weight: 300;
    font-size: 0.85rem;
    opacity: 0.4;
    margin-bottom: 1.5rem;
}

.form-modal__unavailable {
    font-family: 'Courier New', Courier, monospace;
    font-weight: 300;
    font-size: 0.9rem;
    opacity: 0.7;
    line-height: 1.5;
}

.form-modal__form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.form-field {
    display: flex;
    flex-direction: column;
}

.form-field input,
.form-field select,
.form-field textarea {
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.9rem;
    color: #fff;
    background: rgba(255, 255, 255, 0.1);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 0;
    padding: 0.75rem 0.8rem;
    outline: none;
    transition: border-color 0.2s ease, background 0.2s ease;
    width: 100%;
}

.form-field input:focus,
.form-field select:focus,
.form-field textarea:focus {
    border-color: rgba(255, 255, 255, 0.35);
    background: rgba(255, 255, 255, 0.08);
}

.form-field input::placeholder,
.form-field textarea::placeholder {
    color: rgba(255, 255, 255, 0.3);
}

.form-field select {
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='rgba(255,255,255,0.4)' d='M6 8L0 0h12z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.8rem center;
    padding-right: 2.5rem;
}

.form-field select option {
    background: #111;
    color: #fff;
}

.form-field textarea {
    resize: vertical;
    min-height: 100px;
}

.form-field.has-error input,
.form-field.has-error select,
.form-field.has-error textarea {
    border-color: #cc4444;
}

.form-field__error {
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.75rem;
    color: #cc4444;
    margin-top: 0.3rem;
    min-height: 0;
}

.form-modal__submit {
    display: inline-flex;
    align-items: center;
    gap: 0.5em;
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    padding: 0.75rem 1.5rem;
    border: 1px solid rgba(255, 255, 255, 0.2);
    background: transparent;
    color: #fff;
    cursor: pointer;
    transition: border-color 0.2s ease, background 0.2s ease;
    margin-top: 0.5rem;
}

.form-modal__submit:hover {
    border-color: rgba(255, 255, 255, 0.5);
    background: rgba(255, 255, 255, 0.05);
}

.form-modal__submit:disabled {
    opacity: 0.4;
    cursor: default;
}

.form-modal__submit-spinner {
    display: none;
    width: 0.9em;
    height: 0.9em;
    flex: none;
    border: 2px solid rgba(255, 255, 255, 0.3);
    border-top-color: #fff;
    border-radius: 50%;
    animation: form-modal-spin 0.6s linear infinite;
}

.form-modal__submit--sending .form-modal__submit-spinner {
    display: inline-block;
}

@keyframes form-modal-spin {
    to {
        transform: rotate(360deg);
    }
}

@media (prefers-reduced-motion: reduce) {
    .form-modal__submit-spinner {
        animation: none;
    }
}

.form-modal__status {
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.8rem;
    min-height: 1.2em;
}

.form-modal__status--success {
    color: #66cc66;
}

.form-modal__status--error {
    color: #cc4444;
}

.form-modal__privacy {
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.7rem;
    opacity: 0.3;
    margin-top: 1.5rem;
}

/* reCAPTCHA v2 Invisible: badge hidden per Google's terms, replaced by the
   disclosure text below (required whenever the badge isn't shown). */
.grecaptcha-badge {
    visibility: hidden;
}

.form-modal__recaptcha-disclosure {
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.6rem;
    opacity: 0.3;
    margin-top: 0.3rem;
}

.form-modal__recaptcha-disclosure a {
    color: inherit;
}

/* ===========================
   Success Toast Notification
   =========================== */
.success-toast {
    position: fixed;
    bottom: 2rem;
    left: 50%;
    transform: translateX(-50%) translateY(100px);
    background: rgba(46, 204, 113, 0.95);
    color: white;
    padding: 1rem 2rem;
    border-radius: 8px;
    font-size: 1rem;
    font-family: 'Courier New', Courier, monospace;
    opacity: 0;
    transition: all 0.3s ease;
    z-index: 10000;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.success-toast.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

/* ===========================
   Maintenance Mode Banner (staff only - see CONTEXT.md's Maintenance mode)
   =========================== */
.maintenance-banner {
    position: fixed;
    left: 0;
    right: 0;
    bottom: var(--bottom-bar-height);
    z-index: 350;
    background: #c0392b;
    color: #fff;
    text-align: center;
    padding: 0.6rem 1rem;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
}

/* ===========================
   Share Page (Share Link landing - /share/<token>/, issue #100/#101)
   =========================== */

/* Prototype (issue #101 follow-up): the band's own home page banner,
   barely visible, fixed behind the file list. */
.share-page__watermark {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    opacity: 0.03;
    z-index: 0;
    pointer-events: none;
}

.share-page {
    position: relative;
    z-index: 1;
    max-width: 560px;
    margin: 0 auto;
    padding: calc(var(--topbar-height) + 4rem) 1.5rem 4rem;
    text-align: center;
}

.share-page__title {
    font-family: var(--font-display);
    font-size: 1.8rem;
    margin-bottom: 0.5rem;
}

.share-page__subtitle {
    font-family: 'Courier New', Courier, monospace;
    opacity: 0.7;
    margin-bottom: 2rem;
}

.share-page__list {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    text-align: left;
}

.share-page__item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1rem 1.25rem;
    border: 1px solid rgba(255, 255, 255, 0.2);
}

.share-page__item-title {
    font-family: 'Courier New', Courier, monospace;
    word-break: break-word;
}

.share-page__download-btn,
.share-page__download-all {
    font-family: 'Courier New', Courier, monospace;
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    padding: 0.6rem 1.1rem;
    border: 1px solid rgba(255, 255, 255, 0.4);
    background: transparent;
    color: #fff;
    cursor: pointer;
    white-space: nowrap;
    transition: border-color 0.2s ease, background 0.2s ease;
}

.share-page__download-btn:hover,
.share-page__download-all:hover {
    border-color: #fff;
    background: rgba(255, 255, 255, 0.08);
}

.share-page__download-all {
    display: inline-block;
    margin-top: 2rem;
}

/* ===========================
   Responsive: Topbar CTA labels drop below 1024px, icons only from there
   down to the 900px mobile-bar handoff
   =========================== */
@media (max-width: 1024px) {
    .topbar__cta-label {
        display: none;
    }
}

/* ===========================
   Responsive: hamburger handoff (<= 900px)
   Switches from the topbar to the hamburger menu earlier than the rest of
   the mobile styles below it, so the three menu items don't end up
   squeezed against the icons/logo/locale switcher in a narrowing topbar.
   =========================== */
@media (max-width: 900px) {
    html {
        scroll-padding-top: calc(var(--mobile-bar-height) + 1rem);
    }

    .topbar {
        display: none !important;
    }

    .mobile-nav-bar {
        display: grid !important;
    }
}

/* ===========================
   Responsive: Mobile (<= 768px)
   =========================== */
@media (max-width: 768px) {
    .scrolling-phrases {
        display: none;
    }

    .form-modal__content {
        padding: 2rem 1.5rem;
    }

    /* Gigs collapse from a grid "table" to one stacked card per gig —
       `.gigs__item` stops being `display: contents` so its fields group
       together instead of lining up in columns. */
    .gigs__list {
        display: flex;
        flex-direction: column;
        gap: 1.5rem;
    }

    .gigs__item {
        display: flex;
        flex-direction: column;
        gap: 0.35rem;
        padding-bottom: 1.5rem;
        border-bottom: 1px solid rgba(255, 255, 255, 0.12);
    }

    .gigs__list > .gigs__item:last-child {
        border-bottom: none;
    }

    .gigs__date,
    .gigs__event-name,
    .gigs__venue,
    .gigs__location,
    .gigs__ticket-cta-cell,
    .gigs__past-label {
        padding: 0;
        border-bottom: none;
    }

    .gigs__ticket-cta-cell {
        justify-content: flex-start;
    }

    .gigs__ticket-cta-cell:empty {
        display: none;
    }
}