

:root {
  --bg:          #080010;
  --surface:     #0f0020;
  --surface-2:   #160033;
  --text:        #ffffff;
  --pink:        #ff2d87;
  --cyan:        #00f0ff;
  --muted:       rgba(255,255,255,0.62);
  --line:        rgba(255,255,255,0.10);
  --line-strong: rgba(255,255,255,0.18);

  --font-display: 'Clash Display', 'Arial Black', sans-serif;
  --font-body:    'DM Sans', 'Inter', system-ui, sans-serif;

  --gutter: 20px;
  --tier:   1200px;
  --sec-y:  40px;
  --gap:    16px;

  --r:      16px;
  --r-sm:   10px;
  --r-pill: 9999px;

  --tap:    48px;
  --stub:   80px;   /* compact-row poster rail: 3:4 -> 80 x 107 */

  /* 10px + 10px padding + 48px --tap + 1px border. Constant at any root
     font size — every contributor is a fixed px value. preview.js keeps it
     honest via ResizeObserver if that ever stops being true.
     Consumed by .ed-poster's sticky offset ONLY. The mobile menu
     deliberately does not depend on it (see the menu block). */
  --nav-h: 69px;
}

@media (min-width: 900px)  { :root { --gutter: 32px; --sec-y: 64px; --gap: 24px; } }
@media (min-width: 1280px) { :root { --gutter: 64px; --sec-y: 72px; } }

*, *::before, *::after { box-sizing: border-box; }

html { font-size: 100%; -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.5;
  overflow-x: hidden;
  /* Kept, and matching production style.css:29. On <body> with <html>
     computing overflow:visible the used value propagates to the viewport,
     so documentElement.scrollWidth still reports the TRUE un-clipped
     extent — verified against a 150vw control (585 vs 390, clip on and
     off). It does not mask measurement. What it masks is the scrollbar and
     the user's ability to reach overflow, which is why any real overflow
     must be fixed rather than left to this. Never move this clip to a
     wrapper: measured, a wrapper clip clamps scrollWidth to clientWidth
     (creating real masking) AND breaks position:sticky on .nav. */
}

img, video { max-width: 100%; display: block; }
a { color: var(--cyan); }

:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; border-radius: 4px; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: 8px; top: 8px; z-index: 200;
  transform: translateY(calc(-100% - 16px));
  background: var(--pink); color: #05010f; font-weight: 700;
  padding: 12px 18px; border-radius: var(--r-sm);
}
.skip-link:focus { transform: none; }

.wrap { width: 100%; max-width: var(--tier); margin-inline: auto; padding-inline: var(--gutter); }

/* Long unbroken tokens (@handles, pasted URLs, b2b billings) must not blow
   out the layout. `anywhere`, NOT `break-word`: both break inside a word
   only as a last resort, but break-word's break opportunities are excluded
   from min-content intrinsic sizing, so in these auto-sized grid/flex
   tracks the track simply grew and the break never fired — measured, a
   40-char token at 320px gave scrollWidth 619 against clientWidth 320.
   Not `word-break: break-all`, which chops every ordinary filled line.
   Separately: ordinary titles stay off mid-word breaks because this
   preview does not per-character-split them the way production main.js
   does — that, not this property, is what does that work. */
.section-title, .event-name, .event-where, .night-name,
.ed-title, .ed-fact dd, .doorcard-name, .next-event .name {
  overflow-wrap: anywhere;
}

/* The flex/grid children that refuse to shrink. Without all three of
   these — anywhere, wrapper min-width:0, and card min-width:0 — 320px at
   200% root text overflows by 27px, and body{overflow-x:hidden} makes that
   overflow UNREACHABLE (wheel deltaX and focus() both leave scrollX at 0).
   That is silent content loss. Measured: min-width:0 alone 328px;
   anywhere alone 328px; all three 320px == clientWidth. */
.section-head-text { min-width: 0; }
.event-card, .featured-card, .night, .explore-card, .doorcard { min-width: 0; }

/* ============================================================
   PREVIEW CHROME
   ============================================================ */
.preview-banner {
  background: repeating-linear-gradient(135deg, #2a1c00 0 14px, #241800 14px 28px);
  border-bottom: 1px solid #7a5a00;
  color: #ffd400; font-size: 0.8125rem; padding: 8px var(--gutter); text-align: center;
}
.preview-banner strong { color: #fff; }
.preview-banner a { color: #ffd400; }
@media (max-width: 640px) {
  .preview-banner { font-size: 0.6875rem; line-height: 1.35; padding: 6px 12px; }
  .preview-banner .banner-detail { display: none; }
}

/* .mock-tag means exactly one thing: this action is intercepted and does
   nothing. A real, working link that is merely a review aid gets
   .preview-tag instead — conflating the two taught the reviewer that a
   live link was mocked. */
.preview-tag {
  display: inline-block; font-size: 0.6875rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--cyan); border: 1px solid var(--line-strong); border-radius: var(--r-pill);
  padding: 3px 10px; margin-left: 8px; vertical-align: middle;
}

.mock-tag {
  display: inline-block; font-size: 0.6875rem; letter-spacing: .08em; text-transform: uppercase;
  color: #ffd400; border: 1px dashed #7a5a00; border-radius: var(--r-pill);
  padding: 3px 10px; margin-left: 8px; vertical-align: middle;
}

/* Non-blocking, screen-reader-announced route notice. Replaces v1's
   blocking alert(). 58 of 93 anchors route through this. */
.preview-toast {
  position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
  z-index: 200; max-width: min(560px, calc(100vw - 2 * var(--gutter)));
  padding: 14px 18px;
  background: var(--surface-2); color: var(--text);
  border: 1px solid #7a5a00; border-left: 4px solid #ffd400;
  border-radius: var(--r-sm); box-shadow: 0 18px 40px rgba(0,0,0,.55);
  font-size: .875rem; line-height: 1.4; overflow-wrap: anywhere;
}
.preview-toast[hidden] { display: none; }
.preview-toast code { color: #ffd400; }

/* ============================================================
   §1 NAVIGATION + MOBILE MENU
   ============================================================ */
.nav {
  position: sticky; top: 0; z-index: 100;
  display: flex; align-items: center; gap: var(--gap);
  padding: 10px var(--gutter);
  background: rgba(8,0,16,.92);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}

.nav-logo {
  flex: none; display: inline-flex; align-items: center;
  min-height: var(--tap); line-height: 0; padding: 0 6px; margin-left: -6px;
}
.nav-logo img { width: 132px; height: auto; }
/* The mark carries class="logo-img" so production's style.css can size
   it too: its rules are `.nav-logo .logo-img` (3517) and
   `.footer-logo .logo-img` (3518), and it has none for a bare img.
   The 152 backfilled legacy pages link ONLY style.css - the chrome pass
   adds no rru.css link - so the shared chrome has to render correctly
   under both sheets, not just this one. */
.nav-logo .logo-img { width: 132px; height: auto; display: block; }
.footer-logo { margin: 0 0 12px; }
.footer-logo .logo-img { width: 150px; height: auto; display: block; }

.nav-links { display: none; margin: 0 auto 0 12px; padding: 0; list-style: none; gap: 4px; }
@media (min-width: 900px) { .nav-links { display: flex; } }

.nav-links a, .nav-more-btn {
  display: inline-flex; align-items: center; min-height: var(--tap); padding: 0 14px;
  font-size: 0.9375rem; font-weight: 500; color: var(--text); text-decoration: none;
  border-radius: var(--r-sm); background: none; border: 0; cursor: pointer; font-family: inherit;
}
.nav-links a:hover, .nav-more-btn:hover { background: rgba(255,255,255,.07); }

.nav-more { position: relative; }
.nav-more-btn::after { content: "\25BE"; margin-left: 6px; font-size: 0.75rem; opacity: .7; }
.nav-more-panel {
  position: absolute; right: 0; top: calc(100% + 6px); min-width: 200px; padding: 6px;
  background: var(--surface-2); border: 1px solid var(--line-strong);
  border-radius: var(--r); box-shadow: 0 18px 40px rgba(0,0,0,.55);
}
.nav-more-panel a {
  display: flex; align-items: center; min-height: 44px; padding: 0 12px;
  border-radius: var(--r-sm); color: var(--text); text-decoration: none; font-size: 0.9375rem;
}
.nav-more-panel a:hover { background: rgba(255,255,255,.08); }

.nav-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
@media (max-width: 899px) { .nav-actions .btn { display: none; } }

.nav-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap);
  background: none; border: 1px solid var(--line-strong);
  border-radius: var(--r-sm); color: var(--text); cursor: pointer;
}
@media (min-width: 900px) { .nav-toggle { display: none; } }
.nav-toggle .bar { display: block; width: 20px; height: 2px; background: currentColor; position: relative; }
.nav-toggle .bar::before, .nav-toggle .bar::after {
  content: ""; position: absolute; left: 0; width: 20px; height: 2px; background: currentColor;
}
.nav-toggle .bar::before { top: -6px; }
.nav-toggle .bar::after  { top: 6px; }

/* The drawer paints ABOVE the header (101 > 100). That is the whole fix.
   v1 sat at z-index 99 with a 76px top padding standing in for the header
   height — and 76px is right only for the production-equivalent render.
   With the preview banner present the sticky nav is pushed to 111.69px and
   covered 63.7% of the "Events" row; elementFromPoint returned nav.nav and
   Playwright's click() timed out. At root 32px it sliced through two rows.
   Painting above the header makes the panel immune to banner height, root
   font size, scroll position and any future nav change — no measurement,
   no magic constant, nothing to keep in sync. */
.mobile-menu[hidden] { display: none; }
.mobile-menu {
  position: fixed; inset: 0;
  z-index: 101;
  background: var(--bg);
  padding: 16px var(--gutter) 32px;
  overflow-y: auto; overscroll-behavior: contain;
}
/* Belt and braces: the drawer can never paint on desktop even if JS state
   is stale. The matchMedia listener in preview.js is the primary reset. */
@media (min-width: 900px) { .mobile-menu { display: none !important; } }

.mobile-menu ul { list-style: none; margin: 0; padding: 0; }
.mobile-menu a {
  display: flex; align-items: center; min-height: 56px;
  font-family: var(--font-display); font-size: 1.5rem; font-weight: 700;
  color: var(--text); text-decoration: none; border-bottom: 1px solid var(--line);
}
.mobile-menu .menu-group-label {
  margin: 22px 0 4px; font-size: 0.75rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted);
}
.mobile-menu .btn { margin-top: 22px; width: 100%; }

/* Required, not optional. .nav carries z-index:100 on a positioned
   element, so it forms a stacking context: once the drawer is at 101 the
   #nav-toggle inside .nav can never paint above it — a descendant cannot
   escape its own stacking-context root. Escape is keyboard-only. Without
   this button there is no pointer affordance to close the drawer. */
.menu-close-row { display: flex; margin-bottom: 8px; }
.menu-close {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap); margin-left: auto;
  background: none; border: 1px solid var(--line-strong);
  border-radius: var(--r-sm); color: var(--text); cursor: pointer;
  font-size: 1.25rem; line-height: 1;
}

/* ============================================================
   BUTTONS
   ============================================================ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap); padding: 0 22px;
  font-family: var(--font-body); font-size: 0.9375rem; font-weight: 700; letter-spacing: .02em;
  text-decoration: none; text-align: center;
  border-radius: var(--r-pill); border: 1.5px solid transparent;
  cursor: pointer; transition: transform .12s ease, background-color .12s ease;
}
.btn:active { transform: translateY(1px); }
@media (prefers-reduced-motion: reduce) { .btn { transition: none; } }

.btn-primary { background: var(--pink); color: #12000a; border-color: var(--pink); }
.btn-primary:hover { background: #ff4d9c; }
.btn-secondary {
  background: rgba(8,0,16,.72); color: var(--text);
  border-color: var(--line-strong); backdrop-filter: blur(6px);
}
.btn-secondary:hover { background: rgba(8,0,16,.92); border-color: rgba(255,255,255,.35); }
.btn-quiet { background: none; color: var(--cyan); border-color: transparent; padding-inline: 8px; }
.btn-quiet:hover { text-decoration: underline; }
.btn-block { width: 100%; }

/* ============================================================
   §3 HERO — variant A, centred over a full-bleed photograph.
   Used ONLY by index-centred.html now. Left byte-compatible so that
   page is a true comparison against the Doorlist hero.
   ============================================================ */
.hero { position: relative; isolation: isolate; }
.hero-media { position: absolute; inset: 0; z-index: -2; }
.hero-media img { width: 100%; height: 100%; object-fit: cover; }
.hero::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background:
    linear-gradient(180deg, rgba(8,0,16,.74) 0%, rgba(8,0,16,.54) 42%, rgba(8,0,16,.96) 100%),
    radial-gradient(120% 70% at 50% 20%, rgba(255,45,135,.16), transparent 60%);
}
.hero-inner { max-width: var(--tier); margin-inline: auto; padding: 44px var(--gutter) 32px; text-align: center; }

.wordmark { display: flex; align-items: flex-end; gap: 0; max-width: 100%; }
/* 9 letters, one row, natural widths 152+157+157+112+152+124+205+143+124
   = 1326 at height 154 -> aspect 8.61. Deriving height from viewport width
   is correct ONLY because this variant spans the viewport; the Doorlist
   variant sizes the row by its own aspect-ratio instead (see below). */
.wordmark img { height: min(96px, calc((100vw - 2 * var(--gutter)) / 8.61)); width: auto; }
.hero .wordmark { justify-content: center; margin-inline: auto; }

.hero-line { color: var(--muted); font-size: clamp(1rem, 2.2vw, 1.1875rem); }
.hero .hero-line { margin: 16px auto 0; max-width: 46ch; }

.next-event {
  display: grid; gap: 4px; padding: 16px 20px;
  background: rgba(15,0,32,.86); border: 1px solid var(--line-strong);
  border-radius: var(--r); backdrop-filter: blur(8px);
}
.hero .next-event { margin: 22px auto 0; max-width: 520px; justify-items: center; }
.next-event .label { font-size: 0.6875rem; letter-spacing: .16em; text-transform: uppercase; color: var(--cyan); }
.next-event .name { font-family: var(--font-display); font-weight: 700; font-size: clamp(1.25rem, 3.4vw, 1.75rem); line-height: 1.1; }
.next-event .facts { font-size: 0.9375rem; color: var(--muted); }

.hero-ctas { display: flex; flex-wrap: wrap; gap: 12px; }
.hero .hero-ctas { justify-content: center; margin-top: 18px; }
.hero-ctas .btn { flex: 1 1 auto; min-width: 190px; max-width: 340px; }

/* ============================================================
   §3 HERO — variant B, "Doorlist". index.html.
   The next show is told ONCE, in one object carrying the whole
   purchase decision. The photograph is a bounded desktop-only
   panel, never a band: at 1440x900 the v1 full-bleed band kept
   24.1% of a 1600x2400 portrait and cropped out both the face
   (19-35% of frame) and the decks (73-100%).
   ============================================================ */
.hero--doorlist { background: linear-gradient(180deg, rgba(255,45,135,.12), transparent 55%), var(--bg); }
.hero--doorlist .hero-media, .hero--doorlist::after { display: none; }

.hero--doorlist .hero-inner {
  max-width: var(--tier); margin-inline: auto; text-align: left;
  padding: clamp(20px, 4vw, 44px) var(--gutter) clamp(24px, 3.5vw, 40px);
  display: grid; gap: clamp(16px, 2.6vw, 22px);
}
/* Reset variant A's centring margin — leaving it made every Doorlist fold
   measurement ~20px optimistic. */
.hero--doorlist .hero-line { margin: 12px 0 0; max-width: 46ch; }

.hero--doorlist .wordmark { display: block; margin: 0; max-width: 100%; }
.hero--doorlist .wordmark-solo { width: 100%; max-width: 440px; height: auto; }
.hero--doorlist .wordmark-row  { display: none; }

.doorcard {
  display: grid; gap: 12px;
  max-width: 560px;   /* base, NOT inside the >=900 block: without it the
                         600-899px band gave an 859px card and a 603px pill */
  padding: clamp(16px, 4vw, 22px);
  background: linear-gradient(180deg, rgba(255,45,135,.10), rgba(15,0,32,.92) 42%), var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--r);
}
.doorcard-eyebrow {
  margin: 0; display: flex; align-items: center; gap: 8px;
  font-size: .6875rem; letter-spacing: .16em; text-transform: uppercase; color: var(--cyan);
}
.doorcard-eyebrow .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--cyan); flex: none; }
.doorcard-when {
  margin: 0; font-size: clamp(1rem, 3vw, 1.1875rem);
  font-weight: 700; letter-spacing: .03em; text-transform: uppercase; color: var(--cyan);
}
.doorcard-name {
  margin: 0; font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.5rem, 5.2vw, 2.375rem); line-height: 1.05;
}
.doorcard-name a { color: inherit; text-decoration: none; }
.doorcard-name a:hover { text-decoration: underline; text-underline-offset: 3px; }
.doorcard-buy { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 16px; margin-top: 2px; }
.doorcard-price { margin: 0; font-size: 1.125rem; font-weight: 700; }
/* No white-space:nowrap on the price. With the real "Price to be
   announced" state at 375 + 200% text it needed 379px in a 301px box —
   scrollWidth 433 vs clientWidth 375, sheared off by the body clip. */
.doorcard-buy .btn-primary { flex: 1 1 200px; min-width: 0; }
.doorcard-meta { margin: 0; font-size: .9375rem; color: var(--muted); }

.hero-figure { display: none; }   /* phones: omitted, and never fetched */

@media (min-width: 900px) {
  .hero--doorlist .hero-inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, .68fr);
    /* The masthead spans BOTH columns. Confining it to column 1 collapsed
       the wordmark to 72px at 1024 and 86px at 1440 — a 31% brand-asset
       shrink with a discontinuous snap at the breakpoint. */
    grid-template-areas: "intro intro" "door figure";
    column-gap: clamp(28px, 4vw, 56px); align-items: start;
  }
  .hero--doorlist .hero-intro { grid-area: intro; }
  .hero--doorlist .doorcard   { grid-area: door; justify-self: start; width: 100%; }

  .hero--doorlist .hero-figure {
    grid-area: figure; align-self: stretch; display: block;
    position: relative; overflow: hidden;
    border: 1px solid var(--line-strong); border-radius: var(--r);
    background: #0a0018;
    min-height: 300px;
    max-height: 520px;   /* a CEILING as well as a floor: without it a long
                            event name drove the panel to 623px at 1440 and
                            733px at 900, and the crop turned HORIZONTAL */
  }
  /* Load-bearing: absolutely positioning the picture removes the image's
     intrinsic height from grid row sizing, so the panel FOLLOWS the text
     column instead of leading it. */
  .hero--doorlist .hero-figure picture,
  .hero--doorlist .hero-figure img { position: absolute; inset: 0; width: 100%; height: 100%; }
  .hero--doorlist .hero-figure img { object-fit: cover; object-position: 50% var(--focal, 24%); }
  .hero--doorlist .hero-figure::after {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(200deg, rgba(255,45,135,.16), transparent 46%, rgba(8,0,16,.44));
  }

  .hero--doorlist .wordmark-solo { display: none; }
  .hero--doorlist .wordmark-row {
    display: flex; align-items: stretch;
    aspect-ratio: 1326 / 154;                  /* the row owns the ratio */
    width: 100%; max-width: min(100%, 827px);  /* 96 x 8.61 = variant-A cap */
  }
  .hero--doorlist .wordmark-row img { height: 100%; width: auto; }
}

/* ============================================================
   SECTIONS
   ============================================================ */
.section { padding-block: var(--sec-y); }
.section + .section { border-top: 1px solid var(--line); }

/* The hero -> first-section seam. v1 left exactly 160px of blank between
   the hero CTA and the "Upcoming" title at 1440x900 — all of it internal
   padding (56px hero-bottom + 104px --sec-y), 0px between the sections.
   Scoped to .hero--doorlist + so index-centred.html stays a true
   comparison rather than silently inheriting the tightening. */
.hero--doorlist + .section { padding-top: clamp(28px, 4.5vw, 52px); }
.hero--doorlist + .section .section-head { margin-bottom: 16px; }

.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--gap); flex-wrap: wrap; margin-bottom: 20px;
}
.section-title {
  margin: 0; font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.75rem, 4.4vw, 3rem); line-height: 1.08; letter-spacing: -0.01em;
}
.section-sub { margin: 6px 0 0; color: var(--muted); font-size: 0.9375rem; }

/* ============================================================
   §4 EVENTS
   ============================================================ */
/* The card's declared gap only renders as declared once the UA <p> margins
   are reset — .event-when and .event-where are <p>, .event-card is a
   column flex container so margins do not collapse, and the pair added
   58.00px of unintended height per card. */
.event-card p, .featured-card p, .doorcard p { margin: 0; }

.featured-card {
  display: grid; gap: var(--gap);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r); padding: var(--gap); margin-bottom: var(--gap);
}
@media (min-width: 900px) {
  .featured-card { grid-template-columns: minmax(0,300px) minmax(0,1fr); align-items: center; padding: 22px; gap: 32px; }
}

.event-when { font-size: 0.875rem; font-weight: 600; letter-spacing: .04em; color: var(--cyan); text-transform: uppercase; }
.event-name {
  margin: 2px 0 0; font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.1875rem, 2.4vw, 1.5rem); line-height: 1.12;
}
.featured-card .event-name { font-size: clamp(1.5rem, 3.6vw, 2.25rem); }
.event-name a { color: inherit; text-decoration: none; }
.event-name a:hover { text-decoration: underline; text-underline-offset: 3px; }

.event-where { font-size: 0.9375rem; color: var(--muted); margin: 6px 0 0; }
.event-where.is-tbd { font-style: italic; }
.event-price { font-size: 1rem; font-weight: 700; }
.event-price.is-unknown { color: var(--muted); font-weight: 500; }

.event-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 14px; }
.event-meta-row { display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: baseline; margin-top: 10px; }

.poster {
  position: relative; aspect-ratio: 3 / 4; background: #0a0018;
  border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden;
}
/* contain, never cover: posters carry their own lettering and cropping
   destroys it. The compact row overrides this at 80px wide, where that
   lettering is unreadable anyway. */
.poster img { width: 100%; height: 100%; object-fit: contain; }
.poster-empty {
  display: grid; place-content: center; gap: 6px; height: 100%; padding: 20px; text-align: center;
  color: var(--muted); font-size: 0.8125rem; letter-spacing: .1em; text-transform: uppercase;
}
.poster-empty span { font-family: var(--font-display); font-size: 1.375rem; color: rgba(255,255,255,.30); letter-spacing: 0; text-transform: none; }


/* ============================================================
   §4a RUNTIME-INJECTED DOM  (written by /js/main.js, not by any generator)
   ============================================================
   None of these classes appear in any file this port generates - main.js
   creates them at runtime - and the port stylesheet defined none of them, so
   on a ported page every countdown, badge, takeover and rail would have
   rendered as unstyled inline text. A generator diff could never show this;
   it only appears when the real CSS and the real JS are loaded together.

   Sources are production's style.css, restated in v2 tokens:
   .event-countdown 666, .countdown-segment 672/680, .countdown-num 689,
   .countdown-label 699, .countdown-passed 705, .urgency-badge 1382,
   .urgency-tonight 1395, .urgency-weekend 1402, .urgency-live 1408,
   .live-dot 1415, .rail-progress 5147, .rail-nav 5167, .rail-btn 5174,
   .nav.scrolled 143. */

/* -- countdown (main.js:335 initCountdowns) -------------------------------- */
.event-countdown { display: flex; gap: 8px; margin: 0 0 20px; flex-wrap: wrap; }
.countdown-segment {
  background: rgba(0,240,255,.08); border: 1px solid rgba(0,240,255,.2);
  border-radius: var(--r-sm); padding: 8px 12px; text-align: center; min-width: 64px;
}
.countdown-segment.urgent {
  background: rgba(255,45,135,.10); border-color: rgba(255,45,135,.30);
  animation: rruUrgentPulse 1s ease-in-out infinite;
}
.countdown-num {
  display: block; font-family: var(--font-display); font-size: 1.75rem;
  letter-spacing: .05em; color: var(--cyan); line-height: 1;
}
.countdown-label {
  display: block; font-family: var(--font-display); font-size: 0.6875rem;
  letter-spacing: .15em; color: var(--muted);
}
/* The passed state is a <p> main.js swaps in wholesale; without a rule it
   inherited the flex row and read as a stray fragment. */
.countdown-passed {
  margin: 0; font-family: var(--font-display); letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); font-size: 0.875rem;
}
.countdown-passed.countdown-live { color: var(--cyan); }

/* -- urgency badges (main.js:1129 initUrgencyBadges) ----------------------- */
/* These are TIME FACTS, not invented scarcity: HAPPENING NOW / TONIGHT / THIS
   WEEKEND are computed from the event's own start time. The fabricated
   viewer counts and SELLING FAST badge were removed from production on
   2026-09-07 and are deliberately not styled here or anywhere. */
.urgency-badge {
  position: absolute; top: 16px; left: 16px; z-index: 2;
  font-family: var(--font-display); font-size: 0.8125rem; letter-spacing: .12em;
  text-transform: uppercase; padding: 6px 12px; border-radius: var(--r-pill);
}
.urgency-tonight { background: rgba(255,45,135,.9); color: #fff; box-shadow: 0 0 20px rgba(255,45,135,.5); }
.urgency-weekend { background: rgba(0,240,255,.85); color: #000; box-shadow: 0 0 16px rgba(0,240,255,.4); }
.urgency-live    { background: rgba(204,255,0,.9);  color: #000; box-shadow: 0 0 20px rgba(204,255,0,.5); }
.live-dot {
  display: inline-block; width: 8px; height: 8px; background: #ff0000;
  border-radius: 50%; margin-right: 6px; vertical-align: middle;
}
/* An urgency badge and a past badge both anchor to .event-card-image at almost
   the same offset. A card can never legitimately be both - a night that has
   ended is not happening now - but if one ever were, the past badge must lose,
   because "You missed it" over a live show is the wrong answer. */
.event-card-past .urgency-badge { display: none; }
/* ON /events/ THE CARD CARRIES ITS OWN BADGE (lib/rru_event_pages.py _badge,
   proposal E): the when-line's label, on the venue-calendar rule, so main.js
   skips the card and the page shows one label, not two computed differently.
   Where the card has no label the badge is an empty placeholder, hidden. */
.urgency-badge[hidden] { display: none; }
/* NOT IN THE COMPACT ROW. Below 700px the poster is the 80px stub (--stub),
   and a ~108px badge anchored 16px inside it spilled over the date line and
   the heading (BROWSER-CHECKS.md, visual finding 1). The when-line carries
   the same label at this width, so the badge is not drawn here. */
@media (max-width: 699px) {
  .events-grid .event-card .urgency-badge { display: none; }
}

/* -- hero takeover (main.js:1169 initHeroTakeover) ------------------------- */
/* main.js appends this into .hero unconditionally once a show is inside the
   12h window. Without position:absolute it is not an overlay at all - it drops
   into normal flow and pushes the hero's own content down the page. */
.hero-takeover {
  position: absolute; inset: 0; z-index: 5; display: grid; place-content: center;
  gap: 12px; text-align: center; padding: var(--gutter);
  background: rgba(8,0,16,.72); backdrop-filter: blur(4px);
}
.hero.hero-imminent { position: relative; }

/* -- drag rail (main.js:2428 "FILL THE WIDTH") ---------------------------- */
/* main.js:2474 selects `#past-events .events-grid-past` - which the restored
   PAST-EVENTS region now emits - and at 2503 injects a .rail-nav into the
   preceding .section-header. So this port's own markup is what summons these
   controls; unstyled they appeared as a bare "DRAG / SCROLL" and two naked
   arrow buttons above the recent-nights grid. */
.rail-nav { display: flex; align-items: center; gap: 14px; padding-bottom: 16px; }
.rail-nav-label {
  font-family: var(--font-display); font-size: 0.8125rem;
  letter-spacing: .24em; color: var(--muted); text-transform: uppercase;
}
.rail-btn {
  width: 44px; height: 44px; border-radius: var(--r-pill);
  border: 1px solid var(--line-strong); background: transparent;
  color: var(--muted); font-size: 1.125rem; line-height: 1; cursor: pointer;
}
.rail-btn:hover, .rail-btn.is-active {
  border-color: var(--pink); color: var(--pink);
  box-shadow: 0 0 20px rgba(255,45,135,.4);
}
.rail-progress { margin: 24px 0 0; height: 2px; background: var(--line); position: relative; }
.rail-progress-fill { position: absolute; left: 0; top: 0; height: 2px; width: 20%; background: var(--pink); }

/* -- the nav scroll state (main.js:674) ------------------------------------ */
/* main.js toggles .scrolled on every scroll tick past 60px. With no rule the
   one piece of state initNav maintains was inert. */
.nav.scrolled {
  background: rgba(8,0,16,.85); backdrop-filter: blur(20px);
  border-bottom: 1px solid rgba(255,45,135,.15);
}

@keyframes rruUrgentPulse {
  0%, 100% { opacity: 1; } 50% { opacity: .72; }
}
@keyframes urgencyPulse {
  0%, 100% { transform: scale(1); } 50% { transform: scale(1.04); }
}
/* Both pulses are decoration on a factual badge, so the fact survives the
   preference and only the motion stops. */
@media (prefers-reduced-motion: reduce) {
  .countdown-segment.urgent, .urgency-tonight, .urgency-live { animation: none; }
}

/* THE EXIT-INTENT POPUP - the one piece of runtime DOM this section missed.
   main.js builds `.exit-overlay > .exit-popup` on EVERY page it runs on, and
   all 21 `.exit-*` rules live in style.css, which the generated pages do not
   link. So on /events/* the popup rendered as raw markup: the heading flush at
   x=0, default browser text inputs, a bare close glyph. It sat at the foot of
   two of the full-page captures submitted for review and the harness passed
   35/35 around it, because nothing asserted on a component nobody had thought
   to look for. Same failure this whole section exists to prevent - the port
   owns the styling for JS-injected DOM on the pages it generates.

   Restated in the port's own tokens rather than copied, so it follows this
   sheet's palette. The overlay stays below the drawer (10000) and the runtime
   widgets, and hidden until main.js adds `.active`. */
.exit-overlay {
  position: fixed; inset: 0; z-index: 500;
  background: rgba(0,0,0,.85); backdrop-filter: blur(8px);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none; visibility: hidden;
  transition: opacity .3s, visibility .3s;
}
.exit-overlay.active { opacity: 1; pointer-events: auto; visibility: visible; }
.exit-popup {
  position: relative; width: 90%; max-width: 440px;
  background: var(--surface); border: 1px solid rgba(255,45,135,.3);
  border-radius: 20px; padding: 40px 32px; text-align: center;
  box-shadow: 0 0 60px rgba(255,45,135,.15);
}
.exit-close {
  position: absolute; top: 12px; right: 16px;
  min-width: var(--tap); min-height: var(--tap);
  font-size: 28px; line-height: 1; color: var(--muted);
  background: none; border: 0; cursor: pointer;
}
.exit-close:hover { color: var(--text); }
.exit-emoji { font-size: 48px; display: block; margin-bottom: 16px; }
.exit-title {
  font-family: var(--font-display); font-size: clamp(24px, 5vw, 32px);
  font-weight: 800; text-transform: uppercase; margin: 0 0 12px;
}
.exit-text {
  color: var(--muted); font-size: 15px; line-height: 1.6; margin: 0 0 24px;
}
.exit-form { display: flex; gap: 8px; }
.exit-input {
  flex: 1; min-width: 0; padding: 14px 18px;
  background: rgba(8,0,16,.6); border: 1px solid rgba(0,240,255,.2);
  border-radius: 60px; color: var(--text); font-family: var(--font-body);
  font-size: 15px; outline: none; transition: border-color .3s;
}
.exit-input:focus { border-color: var(--accent-2, #00f0ff); }
.exit-input::placeholder { color: var(--muted); }
.exit-submit {
  padding: 14px 24px; background: var(--accent, #ff2d87); color: #12000a;
  font-family: var(--font-body); font-size: 14px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  border: 0; border-radius: 60px; cursor: pointer; white-space: nowrap;
}
.exit-submit:hover { background: #ff4da0; }
.exit-dismiss {
  margin-top: 16px; font-size: 13px; color: var(--muted);
  opacity: .5; cursor: pointer; background: none; border: 0;
}
.exit-dismiss:hover { opacity: 1; }
.exit-error { color: #ff6b6b; font-size: 13px; margin-top: 10px; }
/* The card animates in; the preference stops the motion, not the popup. */
@media (prefers-reduced-motion: no-preference) {
  .exit-overlay.active .exit-popup {
    animation: rruPopupIn .4s cubic-bezier(.16,1,.3,1);
  }
}
@keyframes rruPopupIn {
  from { transform: scale(.9) translateY(20px); opacity: 0; }
  to   { transform: scale(1) translateY(0); opacity: 1; }
}
@media (max-width: 560px) {
  .exit-form { flex-direction: column; }
  .exit-submit { width: 100%; }
}

/* ============================================================
   §4b RECENT NIGHTS  (the PAST-EVENTS region)
   ============================================================
   The region's markers wrap a header, a grid and image-led cards - not the
   bare <article> list an earlier renderer emitted. These are the class names
   the published page already uses; production's style.css has no rule for a
   single one of them, so the section has been rendering unstyled there. The
   card is deliberately NOT the upcoming card: a night that has happened
   leads with its poster and offers the Fun Club, not a ticket.
   .section-header is production's spelling; .section-head is this sheet's.
   Both exist on the site, so both are styled rather than one being renamed
   under a live page. */
.section-header {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--gap); flex-wrap: wrap; margin-bottom: 20px;
}
.section-header .section-sub { flex: 1 1 100%; }

.events-grid-past { --gap-past: 14px; gap: var(--gap-past); }

.event-card-past {
  padding: 0; overflow: hidden; gap: 0;
  /* Dimmer than an upcoming card, on purpose: this is the archive, and the
     grid above it is the thing to act on. */
  background: var(--surface);
  border-color: rgba(255,255,255,0.07);
}
.event-card-past:hover { border-color: var(--line-strong); }

/* The upcoming card's poster wrapper carries this class too, because
   main.js appends the urgency badge into it - so it needs the
   positioning context without inheriting the past card's 3:2 crop. */
.poster.event-card-image { position: relative; aspect-ratio: auto; overflow: visible; }
.event-card-past .event-card-image {
  position: relative; aspect-ratio: 3 / 2; overflow: hidden;
  background: var(--surface-2);
}
/* SCOPED TO PAST CARDS. Now that the upcoming card's poster wrapper also
   carries .event-card-image (main.js appends the urgency badge into it), an
   unscoped rule here would crop and dim every upcoming poster too - undoing
   the accepted "contain, never cover" decision on the whole grid. */
.event-card-past .event-card-image img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  /* cover, not contain, unlike .poster: this is a 3:2 thumbnail of a night
     that is over, and a letterboxed poster in a short box reads as broken. */
  filter: saturate(0.75) brightness(0.78);
  transition: filter .35s ease, transform .35s ease;
}
.event-card-past:hover .event-card-image img {
  filter: saturate(1) brightness(0.92); transform: scale(1.03);
}
.event-card-past .event-card-image .poster-empty { position: absolute; inset: 0; }

.past-badge {
  position: absolute; left: 10px; top: 10px; z-index: 1;
  padding: 5px 10px; border-radius: var(--r-pill);
  background: rgba(8,0,16,0.78); border: 1px solid var(--line-strong);
  color: var(--text); font-size: 0.6875rem; letter-spacing: .12em;
  text-transform: uppercase; backdrop-filter: blur(6px);
}

.event-card-body {
  display: flex; flex-direction: column; gap: 8px; padding: var(--gap);
  /* flex:1 with the auto margin below pins every card's CTA to the same
     baseline. Without it a two-line title or a full postal address pushes one
     card's link 60px lower than its neighbours', which reads as three
     different components rather than one repeated card. */
  flex: 1;
}
.event-card-past .event-name { font-size: clamp(1.0625rem, 2.2vw, 1.25rem); }

.event-meta-display { display: flex; flex-direction: column; gap: 2px; }
.event-date-display { color: var(--cyan); font-size: 0.8125rem; letter-spacing: .08em; text-transform: uppercase; }
.event-venue-display {
  color: var(--muted); font-size: 0.8125rem;
  /* Venue strings are full postal addresses, so they must be allowed to break
     anywhere rather than pushing the grid column wider than its track. */
  overflow-wrap: anywhere;
}

.past-cta-text { margin: 4px 0 0; color: var(--muted); font-size: 0.8125rem; }
.btn-past { align-self: flex-start; padding-inline: 0; margin-top: auto; }

/* The empty state keeps the section and its container, so the page does not
   reflow when the strip empties, and says something true instead of showing
   "You missed it" above nothing. */
.events-grid-past .events-empty { grid-column: 1 / -1; }

.events-grid { display: grid; gap: var(--gap); grid-template-columns: 1fr; container-type: inline-size; container-name: evgrid; }
@media (min-width: 700px)  { .events-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (min-width: 1100px) { .events-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } }

.event-card {
  position: relative; display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r); padding: var(--gap); gap: 12px;
}
/* Stretched link: the whole card/row is the details target (320x184
   instead of a 254x22 inline box), while the action row is lifted above it
   so "Get tickets" stays independently tappable. The overlay costs 0px —
   it is painted over content that exists anyway. */
.event-card .event-name a::after { content: ""; position: absolute; inset: 0; border-radius: var(--r); }
.event-card .event-actions { position: relative; z-index: 1; }

/* --- compact rows below 700px --------------------------------------
   v1: 350 x 714.09 per card, pitch 730.09 = 94.2% of the 775px usable
   viewport; max 1 name+date pair visible at any scroll position. Rows
   bring the grid 2196 -> 778px and fit all three non-featured shows in
   one 844px viewport with 66px to spare. Zero markup change — the six
   children are already in the order a row needs. */
@media (max-width: 699px) {
  .events-grid { gap: 10px; }

  .events-grid .event-card {
    display: grid;
    grid-template-columns: var(--stub) minmax(0, 1fr);
    column-gap: 12px; row-gap: 6px;
    align-items: start;
    padding: 14px; border-radius: var(--r-sm);
    grid-template-areas: "stub when" "stub name" "stub where" "stub price" "act act";
  }
  .events-grid .event-card > .poster         { grid-area: stub; aspect-ratio: 3/4; border-radius: 6px; }
  .events-grid .event-card > .poster img     { object-fit: cover; }
  .events-grid .event-card > .event-when     { grid-area: when;  font-size: .8125rem; }
  .events-grid .event-card > .event-name     { grid-area: name;  font-size: 1.0625rem; line-height: 1.15; }
  .events-grid .event-card > .event-where    { grid-area: where; }
  .events-grid .event-card > .event-meta-row { grid-area: price; }
  .events-grid .event-card > .event-actions  { grid-area: act; margin-top: 0; padding-top: 0; }
  .events-grid .event-card > .event-actions .btn-primary { flex: 1 1 100%; min-width: 0; }
  .events-grid .event-card > .event-actions .btn-quiet   { display: none; }

  /* An 80px grey box says nothing, and .poster-empty's wording overflowed
     the frame by 30px at 200% text. Drop the rail instead of patching it. */
  .events-grid .event-card:has(.poster-empty) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "when" "name" "where" "price" "act";
  }
  .events-grid .event-card:has(.poster-empty) > .poster { display: none; }

  /* The lead show keeps its full-size poster — Codex asked for the large
     featured artwork to survive, and demoting every poster to an 80px stub
     would remove artwork from the site entirely for the three shows whose
     detail pages do not exist locally. */
  .events-grid .event-card--lead {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "stub" "when" "name" "where" "price" "act";
    padding: var(--gap); border-radius: var(--r);
  }
  .events-grid .event-card--lead > .poster { aspect-ratio: 3/4; border-radius: var(--r-sm); }
  .events-grid .event-card--lead > .poster img { object-fit: contain; }
  .events-grid .event-card--lead > .event-name { font-size: 1.25rem; }
}

/* The events index reuses the homepage grid and its cards link the poster, so
   the poster is a block there. STATED BEFORE THE CONTAINER QUERY ON PURPOSE:
   this and the `display: none` below are the same selector at the same
   specificity (0,3,0), so whichever comes last wins outright. It used to sit
   ~230 lines further down, which silently defeated the large-text fallback -
   at 200% text the index rows kept their posters and the first card measured
   4198px tall instead of collapsing to a text-only row. */
.events-grid .event-card > .poster { display: block; }

/* At large text the 80px rail plus its gap crushes the title column and an
   ordinary word breaks mid-word ("FREAKUENCY" measured 274.4px against a
   207px column at 38px type). Container-query em resolves against the
   container's own font size, so this fires exactly when the text has
   outgrown the row and never on a merely narrow viewport. */
@container evgrid (max-width: 12em) {
  .events-grid .event-card,
  .events-grid .event-card--lead {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "when" "name" "where" "price" "act";
  }
  .events-grid .event-card > .poster { display: none; }
}

.events-empty {
  text-align: center; padding: 40px 20px;
  background: var(--surface); border: 1px dashed var(--line-strong); border-radius: var(--r);
}
.events-empty p { color: var(--muted); margin: 0 0 16px; }
.events-empty .btn { margin: 4px; }

/* ============================================================
   RECENT NIGHTS
   ============================================================ */
.nights-grid { display: grid; gap: var(--gap); grid-template-columns: 1fr; }
@media (min-width: 700px) { .nights-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } }
.night {
  position: relative; display: block; text-decoration: none; color: inherit;
  border-radius: var(--r); overflow: hidden; background: var(--surface); border: 1px solid var(--line);
}
.night-img { aspect-ratio: 4 / 3; background: #0a0018; }
.night-img img { width: 100%; height: 100%; object-fit: cover; }
.night-body { padding: 14px 16px 18px; }
.night-when { font-size: 0.75rem; letter-spacing: .12em; text-transform: uppercase; color: var(--cyan); margin: 0; }
.night-name { margin: 4px 0 0; font-family: var(--font-display); font-size: 1.25rem; font-weight: 700; }
.night-note { margin: 6px 0 0; font-size: 0.875rem; color: var(--muted); }
.night:hover .night-name { text-decoration: underline; text-underline-offset: 3px; }

/* ============================================================
   FUN CLUB / EXPLORE / FAQ / FOOTER
   ============================================================ */
.funclub { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); padding: 26px var(--gutter); }
@media (min-width: 900px) { .funclub { padding: 36px; } }
.funclub-benefit { margin: 0 0 18px; color: var(--muted); max-width: 52ch; }
.funclub-form { display: grid; gap: 12px; max-width: 560px; }
@media (min-width: 700px) { .funclub-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } }
.funclub label { display: block; font-size: 0.8125rem; color: var(--muted); margin-bottom: 6px; }
.funclub input {
  width: 100%; min-height: var(--tap); padding: 0 14px;
  background: #0a0018; color: var(--text);
  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  font: inherit; font-size: 1rem;
}
.funclub input::placeholder { color: rgba(255,255,255,.38); }
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
.form-status { margin: 0; font-size: 0.875rem; min-height: 1.2em; }
.form-status.ok  { color: var(--cyan); }
.form-status.err { color: #ff8080; }

.explore-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(210px,1fr)); }
.explore-card {
  display: block; min-height: var(--tap); padding: 16px; text-decoration: none; color: inherit;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
}
.explore-card:hover { border-color: var(--line-strong); }
.explore-card h3 { margin: 0 0 4px; font-family: var(--font-display); font-size: 1.125rem; font-weight: 700; }
.explore-card p  { margin: 0; font-size: 0.875rem; color: var(--muted); }

.faq-list { display: grid; gap: 10px; max-width: 760px; }
.faq-item { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); }
.faq-item summary {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  min-height: var(--tap); padding: 12px 18px; font-weight: 600; cursor: pointer; list-style: none;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after { content: "+"; color: var(--cyan); font-size: 1.25rem; }
.faq-item[open] summary::after { content: "–"; }
.faq-item p { margin: 0; padding: 0 18px 18px; color: var(--muted); }

.footer { border-top: 1px solid var(--line); padding-block: 36px; }
.footer-nav { display: flex; flex-wrap: wrap; gap: 4px 8px; margin: 16px 0; padding: 0; list-style: none; }
.footer-nav a {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 10px;
  color: var(--muted); text-decoration: none; font-size: 0.9375rem; border-radius: var(--r-sm);
}
.footer-nav a:hover { color: var(--text); background: rgba(255,255,255,.06); }
.footer-legal { color: var(--muted); font-size: 0.875rem; margin: 0; }

/* ============================================================
   §2 EVENT DETAIL — six semantic siblings placed with grid areas.
   No `order` anywhere: DOM order IS the mobile reading order.
   v1 used order:1/order:2 on two wrappers, which can only put the
   poster before or after the ENTIRE main column — measured, the
   poster landed 1070px after the ticket button, dead last, at every
   viewport below 900px.
   ============================================================ */
.ed { padding-block: var(--sec-y); }
.ed-back { display: inline-flex; align-items: center; min-height: 44px; gap: 8px; color: var(--muted); text-decoration: none; font-size: 0.875rem; }
.ed-back:hover { color: var(--text); }

.ed-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  row-gap: 20px;
  grid-template-areas: "head" "actions" "poster" "facts" "about" "tail";
}
.ed-head      { grid-area: head; }
.ed-cta-row   { grid-area: actions; display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.ed-poster    { grid-area: poster; margin: 0; }
.ed-facts-sec { grid-area: facts; }
.ed-about     { grid-area: about; }
.ed-tail      { grid-area: tail; }

/* Scoped: setting this on the base rule leaked past 900px and shrank the
   desktop poster 507 -> 453px. */
@media (max-width: 899.98px) { .ed-poster { max-width: 340px; } }

@media (min-width: 900px) {
  .ed-grid {
    grid-template-columns: minmax(0, 380px) minmax(0, 1fr);
    column-gap: 48px; row-gap: 20px;   /* NOT a flat gap:48px — that was the
                                          gap between two wrappers; the
                                          inter-block rhythm was 20px */
    align-items: start;
    /* The poster must appear in EVERY row: sticky travel is bounded by the
       grid AREA, not the column. "poster head" alone un-sticks it after
       ~120px instead of ~500px. */
    grid-template-areas: "poster head" "poster actions" "poster facts" "poster about" "poster tail";
  }
  .ed-poster { position: sticky; top: calc(var(--nav-h) + 16px); max-width: none; }
}

.ed-title {
  margin: 8px 0 0; font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.875rem, 5.2vw, 3.5rem); line-height: 1.04; letter-spacing: -0.015em;
}
.ed-when { font-size: clamp(1rem, 2.4vw, 1.25rem); color: var(--cyan); font-weight: 600; margin: 10px 0 0; }
.ed-cta-row .btn-primary { min-width: 220px; }

.ed-facts { display: grid; gap: 0; margin: 0; border-top: 1px solid var(--line); }
.ed-fact { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 16px; padding: 13px 0; border-bottom: 1px solid var(--line); }
@media (max-width: 480px) { .ed-fact { grid-template-columns: 1fr; gap: 2px; } }
.ed-fact dt { font-size: 0.75rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.ed-fact dd { margin: 0; }
.ed-fact dd a { color: var(--cyan); display: inline-flex; align-items: center; min-height: 44px; }
.ed-venue-addr { color: var(--muted); font-size: 0.9375rem; }

.ed-about h2 { font-family: var(--font-display); font-size: 1.375rem; margin: 0 0 10px; }
.ed-about p { margin: 0 0 12px; color: var(--muted); max-width: 68ch; }

.ed-tail { padding-top: 24px; border-top: 1px solid var(--line); }

/* ============================================================
   MOTION
   ============================================================ */
@media (prefers-reduced-motion: no-preference) {
  .rise { animation: rise .5s cubic-bezier(.22,.8,.3,1) both; }
  .rise-2 { animation-delay: .07s; }
  .rise-3 { animation-delay: .14s; }
  @keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
}

/* ============================================================
   CONTENT-STATE PAGE (states.html)
   ============================================================ */
.state-label {
  display: inline-block; margin-bottom: 8px; font-size: 0.6875rem; letter-spacing: .1em;
  text-transform: uppercase; color: #ffd400; border: 1px dashed #7a5a00;
  border-radius: var(--r-pill); padding: 3px 10px;
}
.state-note { color: var(--muted); font-size: 0.875rem; margin: 8px 0 0; }
.map-wrap { overflow-x: auto; }
.map-table { width: 100%; border-collapse: collapse; font-size: 0.9375rem; }
.map-table th, .map-table td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
.map-table th { color: var(--muted); font-weight: 600; font-size: 0.8125rem; letter-spacing: .06em; text-transform: uppercase; }
.map-table code { color: var(--cyan); }


/* ---------------------------------------------------------------------------
   Added by the port for the event index and detail pages. Everything here uses
   the tokens already defined above, so these pages inherit the same palette,
   spacing and type scale as the homepage rather than starting a second system.
   --------------------------------------------------------------------------- */

.page-head { padding: clamp(2rem, 6vw, 4rem) 0 1rem; }
.page-head h1 { font-family: var(--font-display, inherit); font-size: clamp(2rem, 7vw, 3.25rem); margin: 0 0 .25rem; }

/* A show that has started but not ended. It stays in the grid - the calendar
   filter is "not yet ended" - so it needs to read as current, not upcoming. */
.is-live {
  display: inline-block; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; font-size: .75em; color: var(--accent, #ff2e93);
}
.event-card--live { outline: 2px solid var(--accent, #ff2e93); outline-offset: 2px; }

/* Event detail: three semantic sections in the agreed order - head (title,
   date, primary action), poster, then facts with a repeat action. On mobile
   that is simply document order; on desktop the same three become named grid
   areas, so nothing is reordered visually away from how it reads. */
.event-detail {
  display: grid; gap: clamp(1rem, 4vw, 2rem);
  /* Real gutters. The first version ran text to the screen edge. */
  padding: clamp(1.25rem, 5vw, 2rem) var(--gutter, clamp(1rem, 5vw, 2rem))
           clamp(2rem, 8vw, 5rem);
  /* --tier, NOT a private measure. `--wrap` is declared nowhere in any of the
     three sheets - this was its only mention - so the 72rem fallback always
     applied and the detail page sat at 1152px while the events index, from the
     same generator, sat at --tier's 1200px. Measured on the captures, that put
     their content edges 24px apart (208px vs 184px at 1440). The review asked
     for the established width token rather than an unrelated new width. */
  max-width: var(--tier); margin-inline: auto;
  grid-template-areas: "head" "poster" "facts";
}
.ed-head { grid-area: head; min-width: 0; }
.ed-poster { grid-area: poster; min-width: 0; }
.ed-facts { grid-area: facts; min-width: 0; }

/* Uncropped: the whole flyer, whatever its aspect ratio. */
.ed-poster img { width: 100%; height: auto; display: block; border-radius: var(--r-md, 14px); }
.ed-poster .poster-empty { aspect-ratio: 2/3; display: grid; place-content: center; border-radius: var(--r-md, 14px); }

.ed-head .event-name { font-size: clamp(1.75rem, 6vw, 3rem); margin: .25rem 0 .5rem; overflow-wrap: anywhere; }
.event-lineup { color: var(--muted, #b9b3c7); margin: 0 0 .5rem; }
.event-blurb { margin: 0 0 1.25rem; max-width: 60ch; }
.event-back { margin-top: 1.5rem; }
.cal-unavailable { color: var(--muted, #b9b3c7); font-size: .875rem; margin: .5rem 0 0; }
.event-actions { display: flex; flex-wrap: wrap; gap: .625rem; margin-top: 1rem; }
.event-actions--repeat { margin-top: 0; }

@media (min-width: 860px) {
  .event-detail {
    grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
    grid-template-areas: "poster head" "poster facts";
    align-items: start; column-gap: clamp(2rem, 4vw, 3.5rem);
  }
}

/* (`.events-grid .event-card > .poster { display: block }` used to live here.
   It has moved above the @container evgrid block, which needs to be able to
   hide the poster at large text; same selector, same specificity, so source
   order decides.) */
.events-grid .event-card .event-name a { color: inherit; text-decoration: none; }
.events-grid .event-card .event-name a:hover { text-decoration: underline; }


/* ---------------------------------------------------------------------------
   Chrome behaviour contracts. main.js drives the mobile drawer by toggling the
   class `open` (main.js:670+) and has no handler at all for the More panel, so
   both are expressed here rather than depending on JavaScript the production
   runtime does not contain.
   --------------------------------------------------------------------------- */

/* Drawer: hidden until main.js adds .open. Using the `hidden` attribute instead
   left the toggle flipping aria-expanded while the drawer stayed shut. */
.mobile-menu { display: none; }
.mobile-menu.open { display: block; }

/* More: a CSS disclosure, so the five destinations behind it are reachable by
   mouse AND keyboard with no script. :focus-within covers tab navigation. */
.nav-more { position: relative; }
.nav-more-panel { display: none; position: absolute; right: 0; top: 100%; z-index: 40;
  min-width: 12rem; padding: .5rem; border-radius: var(--r-sm, 10px);
  background: var(--surface, #17101f); box-shadow: 0 10px 30px rgba(0,0,0,.45); }
/* TWO MECHANISMS, NEVER BOTH AT ONCE.
   Without JavaScript the panel opens on hover and on :focus-within, so the
   five destinations behind More stay reachable - but nothing can then keep
   aria-expanded accurate, which is why the markup ships without it.
   js/rru-chrome.js sets .rru-js-nav on <html> the moment it initialises,
   which switches these rules off and hands the panel to the button's own
   state. Scoping this way means the announced state and what is visible can
   never disagree: whichever mechanism is live is the only one live. */
:root:not(.rru-js-nav) .nav-more:hover .nav-more-panel,
:root:not(.rru-js-nav) .nav-more:focus-within .nav-more-panel { display: block; }
.rru-js-nav .nav-more-panel.is-open { display: block; }

/* The drawer's scroll lock pins <body>; this keeps the page from showing a
   scrollbar for content it is no longer scrolling. */
body.rru-drawer-open { overflow: hidden; }
.nav-more-panel a { display: block; padding: .5rem .75rem; white-space: nowrap; }

/* ============================================================
   POSTER FIT ON THE HOMEPAGE GRID - a specificity correction
   ============================================================
   MEASURED, not guessed. production style.css crops these posters with two
   ID-scoped rules of specificity (1,4,1):

     :5050  #events .events-grid > .event-card:first-child
              .event-card-image img { object-fit: cover }
     :5090  #events .events-grid > .event-card:not(:first-child)
              .event-card-image img { object-fit: cover }

   They did not match the port's cards until this pass, because the v2 card
   wrapped its poster in `.poster` alone. It now also carries
   `.event-card-image`, which main.js requires to append the urgency tier badge
   - so those two rules started matching and cropped artwork the selected
   design keeps whole. In a browser: every upcoming poster object-fit "cover".

   Matched one class higher, (1,5,1), rather than with !important - which would
   also win arguments it should not. "Contain, never cover" is the accepted
   rule for event artwork: posters carry their own lettering and cropping
   destroys it. The one exception is the compact mobile row, where the poster
   is an 80px stub and its lettering is unreadable at that size anyway. */
#events .events-grid > .event-card > .poster.event-card-image img {
  object-fit: contain;
}
@media (max-width: 699px) {
  /* The stub keeps `cover`: at 78x104 the artwork is an identifier, not a
     readable poster, and letterboxing it wastes half the rail. The LEAD card
     is excluded - it keeps its full artwork, uncropped. */
  #events .events-grid > .event-card:not(.event-card--lead) > .poster.event-card-image img {
    object-fit: cover;
  }
}

/* production SIZES that wrapper too, not only its object-fit:

     style.css:5046  #events ... > .event-card:first-child .event-card-image
                       { min-height: 600px; height: 100% }
     style.css:5089  #events ... > .event-card:not(:first-child)
                       .event-card-image { height: 250px }

   Both (1,4,0). Once the v2 card carried `.event-card-image` for the urgency
   badge hook, they took over its geometry: the 80px compact stub rendered
   80x250 - a tall thin strip - and the lead card inherited a 600px floor.
   Measured in a browser at 390px: posterBox [80, 250] with aspect-ratio 3/4
   declared and ignored, because an explicit height beats a ratio.

   Equal specificity, later sheet, so the port's own ratios decide again. */
#events .events-grid > .event-card > .poster.event-card-image {
  height: auto; min-height: 0;
}

/* ============================================================
   THE HERO'S OUTER LAYOUT - overriding the legacy contract
   ============================================================
   The Doorlist grid was introduced INSIDE a section whose outer contract was
   never reset. production style.css:

     .hero { min-height: 100vh; display: flex; align-items: center;
             justify-content: center; overflow: hidden }

   So the composition sat centred inside a full viewport-height box: the whole
   first screen went to the hero at both sizes, a large gap opened above the
   wordmark on mobile, and Upcoming never entered the viewport. The inner grid
   cannot fix that - it is the outer box that is 100vh.

   `.hero.hero--doorlist` is (0,2,0) against `.hero` (0,1,0), so this wins on
   specificity rather than on load order alone. Content-driven height, no
   centring, and overflow restored so the BPM control is not clipped. */
.hero.hero--doorlist {
  min-height: 0;
  display: block;
  align-items: initial;
  justify-content: initial;
  overflow: visible;

  /* CLEARANCE FOR A FIXED HEADER. On the legacy homepage `.nav` is
     `position: fixed` (style.css:131), so it is out of flow and paints over
     whatever begins at y=0. `min-height:100vh` plus the centring used to hide
     that - the composition floated mid-screen, far below the bar. Removing it
     put the lockup at the top of the document and the header landed on top of
     it: measured at 1440x900, the wordmark ran 44->140 under an 88px bar, and
     24->91 under an 80px bar at 390x844.

     The reserved space is the approved bar's height: 10px + a 48px control +
     10px + the 1px border. It is stated here rather than in the chrome sheet
     because it is a property of THIS composition, and because the port pages
     that link rru.css have a sticky header which needs no clearance and no
     `.hero--doorlist`. verify_items_2_3.py asserts this against the bar's
     measured height instead of trusting the arithmetic. */
  padding-block: calc(69px + env(safe-area-inset-top)) 0;
}

/* Spacing: the accepted reference breathes above the lockup and leaves a real
   gap before Upcoming, without spending a screen on it. The bottom step is the
   smaller of the two - what follows the hero is not bare page, it is the live
   ticker band, which already supplies a visual rest. */
.hero.hero--doorlist .hero-inner {
  padding-block: clamp(24px, 3.2vw, 44px) clamp(18px, 2.4vw, 30px);
}

/* THE SEAM BETWEEN THE HERO AND UPCOMING.
   Removing `min-height: 100vh` was necessary but not sufficient: with the hero
   ending at y=708, Upcoming still opened at y=1015, 115px below the fold. The
   307px in between belongs to two elements the port does not own - production's
   live ticker band (112px) and `#events`' own `.pad-headline` padding (124px
   top) - so no amount of hero tightening could reach it.

   The seam rule already in SECTIONS could not do this job twice over. It is
   written `.hero--doorlist + .section`, and the ticker band sits between the
   two, so the adjacency never matched; and even matched, `.section` is a class
   production owns, so build_regions_css.py correctly refuses to copy the rule
   into rru-regions.css, which is the sheet the applied homepage actually loads.

   Addressing the region by its APPLIER-OWNED id solves both: `~ #events`
   crosses the ticker without depending on what sits there, `#events` is unique
   so the general sibling combinator cannot over-reach, the only class named is
   one the port owns, and (1,1,0) beats `.pad-headline`'s (0,1,0) on
   specificity rather than on load order. The bottom padding is left alone -
   that seam is between two production sections and is not ours.

   Only the padding is set here. The companion `.section-head` rule in SECTIONS
   already covers the port's own pages, which spell the block `.section-head`
   (see the note at .section-header below); applied pages use production's
   `.section-header` and its interior is not the port's to restyle. */
/* ============================================================
   THE TRANSITION FROM THE HERO INTO UPCOMING
   ============================================================
   Every rule below is scoped `.hero--doorlist ~ …`, so it applies only to a
   page that carries the Doorlist hero - the homepage. `.h-mega`, the ticker
   component and the 152 legacy pages are untouched; nothing here is a global
   restyle. build_regions_css.py copies these across on that scope rather than
   on its usual owned-class test (see the note in that script).

   Tightening the hero could never have finished this on its own: measured, the
   307px between the hero and Upcoming was production's two-row ticker band
   (112px) plus the `.pad-headline` padding, and a further ~71px inside the
   section header belonged to an eyebrow and a display-size heading. */

/* 1. THE TICKER BAND. Suppressed on this page only. `display: none` rather
      than height/visibility so it reserves no blank box. The element, the
      TICKER-EVENTS marker pair and the text the applier writes into it all
      remain in the document, so the region contract still validates and any
      other surface using `.ticker-band` is unaffected. */
.hero--doorlist ~ #rru-live-ticker { display: none; }

/* 2. THE EYEBROW. "NEXT UP · 4 SHOWS LIVE" labelled an upcoming count as
      shows currently live, and duplicated the heading beneath it. Hidden, not
      deleted: `[data-events-live]` stays in the DOM so anything updating it
      keeps working. */
.hero--doorlist ~ #events .eyebrow { display: none; }

/* 3. THE HEADING. `.h-mega` is production's display treatment - 134px tall at
      1440. The approved section heading is the compact one, restated here at
      the reference's own values and scoped to this page so `.h-mega` keeps its
      meaning everywhere else. */
.hero--doorlist ~ #events .section-title {
  margin: 0; font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.75rem, 4.4vw, 3rem); line-height: 1.08;
  letter-spacing: -0.01em;
  /* The reference reads "Upcoming". Production upper-cases its section titles,
     and setting only size/weight left that inherited, so the compact heading
     still shouted UPCOMING. */
  text-transform: none;
}
.hero--doorlist ~ #events .section-header { margin-bottom: 0; }

/* 4. THE COUNT AND THE ALL-EVENTS LINK, placed with the header. The applier
      writes both inside `.events-countline`, which is still a child of
      `.events-grid` because the marker pair may not move. Spanning every
      column and ordering it ahead of the cards puts it under the heading and
      out of the card flow, so it cannot occupy a slot. */
.hero--doorlist ~ #events .events-grid > .events-countline {
  grid-column: 1 / -1; order: -1;
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--gap); flex-wrap: wrap; margin: 0;
}
/* The count reads as body copy, as it does in the reference. Production's
   `.section-sub` (style.css:561) is an 18px kicker in `--font-accent` at
   0.1em tracking - and that face has no lowercase, so "4 upcoming shows"
   rendered as 4 UPCOMING SHOWS with `text-transform` computing to `none`.
   The face was doing it, not a transform, which is why only looking caught it. */
.hero--doorlist ~ #events .events-countline > p {
  margin: 0; font-family: var(--font-body); font-size: 0.9375rem;
  letter-spacing: normal; text-transform: none; color: var(--muted);
}

/* 5. SPACING. The reference's own step, not a compensation for the removed
      bands - no extra margin, no min-height, no duplicate heading. */
.hero--doorlist ~ #events { padding-top: clamp(28px, 4.5vw, 52px); }
@media (max-width: 899px) {
  .hero--doorlist ~ #events { padding-top: 26px; }
}

/* THE SPOTIFY CONTROL, kept and repositioned. At `top: 100px; right: 40px` it
   was anchored to a 100vh box; in a content-height hero it landed in the
   ticket information's airspace on desktop and reserved an empty band under
   the card on mobile. It sits with the lockup instead - still in the hero,
   still `#bpm-badge`, all three main.js bindings intact. */
.hero.hero--doorlist .bpm-badge {
  /* Offset from the hero's PADDING edge, not its border edge. The hero's box
     now starts at y=0 and reserves the fixed bar's height as padding, so a
     bare `top: 16px` put the badge 23px underneath the bar at 1440x900 -
     found by check_bar_overlap.py, which is exactly the collision it is for.
     Same 69px + inset as the hero clearance, plus the badge's own 12px. */
  position: absolute; top: calc(69px + env(safe-area-inset-top) + 12px);
  right: var(--gutter); bottom: auto; left: auto;
}
@media (max-width: 899px) {
  .hero.hero--doorlist .bpm-badge {
    position: static; margin: 4px var(--gutter) 0 auto;
  }
}

/* THE TAGLINE IS VISIBLE WITHOUT JAVASCRIPT.
   production's `.hero-line { opacity: 0 }` waits for revealHero() to add
   `.revealed`. Inheriting that made the sentence invisible whenever the script
   did not run - which was listed as an accepted limitation and should not have
   been, because this candidate owns the element. Visible by default; the
   reveal still animates when the class arrives. */
.hero.hero--doorlist .hero-line { opacity: 1; transform: none; }
.hero.hero--doorlist .hero-line.revealed {
  opacity: 1; transform: none;
  transition: opacity .8s cubic-bezier(.16,1,.3,1);
}
