html {
  scrollbar-width: none;
}

html::-webkit-scrollbar,
body::-webkit-scrollbar {
  display: none;
}

:root {
  --green: #16ac1f;
}

* { box-sizing: border-box; }

html {
  scroll-snap-type: y mandatory;
  /* The arrow is a jump link. Without this it teleports, and the whole
     point of the arrow is to show that there is somewhere to go. */
  scroll-behavior: smooth;
}

main {
  position: relative;
}

body {
  margin: 0;
  background: #000;
}

.screen {
  position: relative;
  height: 100vh;
  overflow: hidden;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* Aniko, full height on the right. The shot is lit against black, so a screen
   blend keys it out exactly, no matte edge to clean up. */
.aniko {
  position: absolute;
  inset: 0 0 0 auto;
  margin: 0;
  height: 100%;
  mix-blend-mode: screen;
  pointer-events: none;
}

.aniko img {
  display: block;
  height: 100%;
  width: auto;
  /* The JPEG's "black" is about #0d0d0d, and screening that over #000 leaves
     exactly that grey. Crushing the low end drops everything under ~#1a to
     true zero, so the plate disappears into the page. */
  filter: contrast(1.25);
  /* fade the frame edges so the crop never reads as a rectangle */
  -webkit-mask-image:
    linear-gradient(to right, transparent 0, #000 22%),
    linear-gradient(to bottom, transparent 0, #000 6%, #000 94%, transparent 100%);
  -webkit-mask-composite: source-in;
          mask-image:
    linear-gradient(to right, transparent 0, #000 22%),
    linear-gradient(to bottom, transparent 0, #000 6%, #000 94%, transparent 100%);
          mask-composite: intersect;
}

/* The lockup is built from two crops rather than one. The e of "live" and the
   asterisk overlap horizontally in the source, so no single rectangle keeps
   the e and drops the asterisk; they do separate on a horizontal line at
   y=182, so the art is cut into a band above it and a band below. Each band
   is a plain single-layer luminance mask, which is what the earlier
   mask-composite notch failed to be: that left a translucent black rectangle
   over the whole box, invisible on the black page and obvious on the green.
   --fill paints the letters, so the same art works in either colourway. */
.lockup {
  position: relative;
  aspect-ratio: 477 / 197;
}

.lockup i {
  position: absolute;
  left: 0;
  display: block;
  background: var(--fill, var(--green));
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-source-type: luminance;
          mask-mode: luminance;
}

.lt {
  top: 0;
  width: 96.855%;
  height: 61.929%;
  -webkit-mask-image: url(assets/logo-top.jpg);
          mask-image: url(assets/logo-top.jpg);
}

.lb {
  top: 61.929%;
  width: 100%;
  height: 38.071%;
  -webkit-mask-image: url(assets/logo-bot.jpg);
          mask-image: url(assets/logo-bot.jpg);
}

.logo {
  width: clamp(300px, 42vw, 620px);
}

/* Logo over the partner mark, top left of the hero. Stacking them rather than
   placing each one absolutely means the 14A sits under the lockup at any size
   without anyone doing the height arithmetic. */
.masthead {
  position: absolute;
  left: 24px;
  top: 40px;
}

.presents {
  margin: 34px 0 0;
  color: var(--green);
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .22em;
  font-size: clamp(12px, 1.6vw, 18px);
}

/* Zero-size anchors. The script reads their centres and flies the asterisk
   from one to the other, so the path stays right at any viewport and any
   type size without a hardcoded coordinate anywhere. */
.slot {
  position: absolute;
  width: 0;
  height: 0;
}

/* where the asterisk sat in the original lockup art */
#slot-a { left: 105.2%; top: 45.7%; }

/* the dot the ı has lost, sitting over its stem */
#slot-b { left: 46%; top: -.03em; }

/* Two layers in one box. The asterisk rides the first leg, then crossfades
   into a solid pill on the second so it can land as the button's background.
   Both are painted from --star, which the script drives. */
.star {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 4;
  pointer-events: none;
  will-change: transform;
}

.star i {
  position: absolute;
  inset: 0;
  display: block;
  background: var(--star, var(--green));
}

.star .art {
  -webkit-mask: url(assets/star.jpg) center / contain no-repeat;
  -webkit-mask-source-type: luminance;
          mask: url(assets/star.jpg) center / contain no-repeat luminance;
}

.star .fill {
  border-radius: 999px;
  opacity: 0;
}


/* The 14A art came in a yellower green. Masking it and painting the fill with
   a flat colour rather than the file puts it on the same green as everything
   else. */
.partner {
  margin-top: 26px;
  width: clamp(88px, 11vw, 150px);
  aspect-ratio: 1;
  background: var(--green);
  -webkit-mask: url(assets/14a.jpg) center / contain no-repeat;
  -webkit-mask-source-type: luminance;
          mask: url(assets/14a.jpg) center / contain no-repeat luminance;
}

/* The billing screen inverts: black type on the green. */
.bill {
  display: grid;
  grid-template-rows: 1fr;
  padding: 40px 24px;
  background: var(--green);
  color: #000;
  text-align: center;
}

.bill p, .bill dl, .bill dd, .bill dt { margin: 0; }

.set {
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
}

/* Billing, the connector and the video read as one column, so the line can run
   straight out of the underline and into the frame's outline. */
.stage {
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  translate: 0 -5%;
}

.lead {
  width: 2px;
  height: clamp(20px, 4vh, 46px);
  background: #000;
}

.video {
  /* sized off the viewport height, not its width: a portrait frame is the one
     thing on this screen that can push the column past 100vh */
  height: clamp(260px, 46vh, 520px);
  width: auto;
  aspect-ratio: 3 / 4;
  border: 2px solid #000;
  padding: 10px;
}

/* object-fit does nothing to an iframe, so the crop is done by hand: the
   player keeps its own 16:9, is grown until it covers the portrait box, and
   is centred so the overflow falls off both sides evenly. */
.crop {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.crop iframe {
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  height: 100%;
  width: auto;
  aspect-ratio: 16 / 9;
  border: 0;
  display: block;
}

.act {
  font-family: "Helvetica Now Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700;
  font-size: clamp(66px, 13vw, 180px);
  line-height: .78;
  letter-spacing: -.05em;
}

.dotless { position: relative; }

.set {
  margin-top: .5em;
  margin-bottom: .55em;
  font-size: clamp(18px, 3.4vw, 46px);
  letter-spacing: .01em;
  line-height: 1;
  text-decoration: underline;
  text-underline-offset: .3em;
  text-decoration-thickness: 2px;
}

/* Buy tickets, the last screen the arrow points at. */
.tickets {
  display: grid;
  place-items: center;
  text-align: center;
}

.ticketing { color: var(--green); }
.ticketing p { margin: 0; }

.when {
  font-family: "Helvetica Now Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700;
  font-size: clamp(72px, 15vw, 220px);
  line-height: .8;
  letter-spacing: -.045em;
}

.where {
  margin-top: .5em;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: -.01em;
  font-size: clamp(16px, 3vw, 34px);
}

.cta {
  display: inline-block;
  margin-top: 2.2em;
  padding: .85em 2.4em;
  border-radius: 999px;
  background: transparent;
  color: #000;
  position: relative;
  z-index: 5;
  text-decoration: none;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .02em;
  font-size: clamp(14px, 2vw, 22px);
}

/* The pointer. No container: just the word and the arrow under it, and only
   the arrow moves so the label stays readable. */
.jump {
  position: absolute;
  left: 50%;
  bottom: 76px;
  z-index: 3;
  translate: -50% 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .7em;
  color: var(--green);
  text-decoration: underline;
  text-underline-offset: .28em;
  text-decoration-thickness: 1.5px;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: 13px;
  white-space: nowrap;
}

.jump svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  animation: bob 2.2s ease-in-out infinite;
}

@keyframes bob {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 6px; }
}

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

/* A line down the right edge that draws itself from the top of the second
   screen to the bottom of the last one, down the left edge. The colour is anchored to the track,
   not to the drawn length, so the run stays black over the green screen and
   green over the black one however far it has filled. */
.flow {
  position: absolute;
  top: 100vh;
  right: 0;
  width: 2px;
  /* height and gradient are measured in the script */
  height: 300vh;
  z-index: 1;
  pointer-events: none;
}

.flow-line {
  display: block;
  width: 100%;
  height: 100%;
  clip-path: inset(0 0 100% 0);
  background: var(--green);
}

/* Once the asterisk is home the rail turns into a current. The bands
   are a repeating gradient scrolled down the line for good: nothing is
   measured, nothing ends, and because the pattern tiles at its own
   length it never seams. Two speeds over one another - a long slow
   swell and a short bright run - so it reads as fluid rather than as a
   barber's pole. */
.flow.flowing .flow-line {
  background-image:
    repeating-linear-gradient(
      to bottom,
      rgba(255, 255, 255, 0) 0,
      rgba(255, 255, 255, .55) 26px,
      rgba(255, 255, 255, 0) 74px),
    repeating-linear-gradient(
      to bottom,
      rgba(0, 0, 0, .55) 0,
      rgba(0, 0, 0, 0) 120px,
      rgba(0, 0, 0, .55) 260px);
  background-size: 100% 74px, 100% 260px;
  animation: flow-run 1.9s linear infinite,
             flow-swell 7.5s linear infinite;
}

@keyframes flow-run {
  to { background-position-y: 74px, 0; }
}

@keyframes flow-swell {
  to { background-position-y: 0, 260px; }
}

/* The head stops being a marker and starts being the tip of the
   current: it breathes on the same clock as the fast band. */
.flow.flowing + .flow-head {
  animation: head-breathe 1.9s ease-in-out infinite;
}

@keyframes head-breathe {
  0%, 100% { scale: 1; opacity: .9; }
  50%      { scale: 1.35; opacity: .55; }
}

@media (prefers-reduced-motion: reduce) {
  .flow.flowing .flow-line { animation: none; }
  .flow.flowing + .flow-head { animation: none; }
}

.flow-head {
  position: absolute;
  top: 100vh;
  right: -8px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #000;
  opacity: 0;
  z-index: 1;
  pointer-events: none;
}

@media (max-width: 600px) {
  .flow { width: 2px; }
  .flow-head { right: -6px; width: 14px; }
}

/* Sponsors and the footer share the last screen: credits held in the middle,
   the footer sitting on the floor of it. */
.closing {
  display: grid;
  grid-template-rows: 1fr auto;
  align-items: center;
  padding: 40px 24px 32px;
  color: var(--green);
  text-align: center;
}

.sponsors {
  display: grid;
  justify-items: center;
}

.label {
  margin: 0;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .22em;
  font-size: clamp(11px, 1.4vw, 15px);
  opacity: .6;
}

.supported { margin-top: clamp(40px, 7vh, 84px); }

/* The real marks, lifted off the flyer's sponsor bar. They carry their own
   brand colours so they cannot be masked and recoloured the way the 14A was;
   they keep the flyer's white bar instead. */
.brands {
  display: block;
  width: clamp(280px, 52vw, 690px);
  height: auto;
  margin-top: 20px;
  padding: 14px 22px;
  border-radius: 4px;
  background: #fff;
}


footer {
  display: grid;
  justify-items: center;
  gap: 18px;
  border-top: 2px solid rgba(255, 255, 255, .12);
  padding-top: 24px;
}

.foot-mark {
  width: clamp(150px, 19vw, 260px);
  margin-bottom: clamp(22px, 3.5vh, 40px);
}

footer nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(14px, 2.4vw, 34px);
}

footer a {
  color: var(--green);
  text-decoration: underline;
  text-underline-offset: .28em;
  text-decoration-thickness: 1.5px;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: clamp(12px, 1.6vw, 16px);
}

.copy {
  margin: 0;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .18em;
  font-size: clamp(10px, 1.2vw, 13px);
  opacity: .45;
}



/* The line sits above the screens' backgrounds but under their content, so it
   runs behind the type instead of over it. A plain z-index below the sections
   would not do: the green screen paints an opaque background that would hide
   the line completely. */
.aniko,
.masthead { z-index: 2; }

.stage,
.sponsors,
footer {
  position: relative;
  z-index: 2;
}

/* Above the star, not just above the line. This block is a stacking context,
   so the button's own z-index only counts inside it: leave this at 2 and the
   pill paints over its own label. */
.ticketing {
  position: relative;
  z-index: 6;
}


/* ============================================================
   Immersion pass: grain, reveals, tickers, countdown, tiers.
   Everything below is additive. The rule the whole page keeps:
   black and one green, condensed caps for anything that is not
   a headline, and nothing moves that the scroll did not move.
   ============================================================ */

/* A single fixed grain plate over the whole page. feTurbulence is
   rendered once into a data URI and then drifted by transform, so
   the browser is compositing one layer rather than repainting noise
   every frame. Overlay keeps it out of the true blacks. */
.grain {
  position: fixed;
  inset: -60px;
  z-index: 8;
  pointer-events: none;
  opacity: .16;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/></filter><rect width='180' height='180' filter='url(%23n)'/></svg>");
  animation: grain 700ms steps(1) infinite;
}

@keyframes grain {
  0%   { transform: translate(0, 0); }
  20%  { transform: translate(-14px, 9px); }
  40%  { transform: translate(11px, -12px); }
  60%  { transform: translate(-9px, -7px); }
  80%  { transform: translate(13px, 8px); }
  100% { transform: translate(0, 0); }
}

/* Vignette on the hero only, so Aniko's plate falls off into the page
   instead of stopping at the viewport edge. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: radial-gradient(120% 85% at 62% 45%, transparent 38%, rgba(0,0,0,.75) 100%);
}

/* Reveals. Each screen gets .in when it comes into view and never
   loses it, so nothing re-animates on the way back up. --d staggers
   the children off one trigger rather than four observers. */
.rise, .grow {
  opacity: 0;
  transition:
    opacity .7s cubic-bezier(.2,.7,.2,1) var(--d, 0s),
    transform .9s cubic-bezier(.2,.7,.2,1) var(--d, 0s),
    clip-path .9s cubic-bezier(.2,.7,.2,1) var(--d, 0s);
}

.rise { transform: translateY(18px); }
.in .rise { opacity: 1; transform: none; }

.grow { transform: scaleY(0); transform-origin: top; }
.in .grow { opacity: 1; transform: none; }

/* The act name is the one thing that cannot be translated: the asterisk
   flies to a slot inside it while this screen is still off-view, and a
   transform would drag the target with it. So it wipes instead. */
.act {
  opacity: 0;
  clip-path: inset(0 0 108% 0);
  transition:
    opacity .5s ease var(--d, 0s),
    clip-path 1s cubic-bezier(.2,.7,.2,1) var(--d, 0s);
}

.in .act { opacity: 1; clip-path: inset(-20% -20% -20% -20%); }

@media (prefers-reduced-motion: reduce) {
  .rise, .grow, .act { opacity: 1; transform: none; clip-path: none; transition: none; }
  .grain { animation: none; }
  .ticker-run { animation: none; }
}

/* Corner slugs. Flight-info type: small, wide-tracked, sitting on the
   margin so the middle of every screen stays empty. */
.corner {
  position: absolute;
  top: 44px;
  z-index: 3;
  margin: 0;
  color: var(--green);
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .2em;
  font-size: clamp(10px, 1.2vw, 13px);
  line-height: 1.7;
}

.corner.tr { right: 24px; text-align: right; }
.corner.tl { left: 24px; }
.corner.bk { color: #000; opacity: .55; }

/* Tickers. Two identical runs side by side and a translate of exactly
   -50% of the pair, which is what makes the loop seamless without
   measuring anything. */
.ticker {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  overflow: hidden;
  padding: 12px 0;
  border-top: 1px solid rgba(22, 172, 31, .28);
  white-space: nowrap;
  pointer-events: none;
}

.ticker.bk { border-top-color: rgba(0, 0, 0, .3); }

.ticker-run {
  display: inline-flex;
  animation: slide 26s linear infinite;
}

.ticker-run.rev { animation-direction: reverse; }

.ticker span {
  color: var(--green);
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .26em;
  font-size: clamp(10px, 1.3vw, 14px);
  padding-right: .26em;
}

.ticker.bk span { color: #000; }
.ticker i { font-style: normal; opacity: .5; }

@keyframes slide {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* the hero's arrow has to clear the ticker */
.hero .jump { bottom: 92px; }

/* A tally light on the clip, so the frame reads as a feed rather than a
   still. It sits inside the frame's padding, over the crop. */
.video { position: relative; }

.rec {
  position: absolute;
  left: 18px;
  bottom: 18px;
  display: flex;
  align-items: center;
  gap: .55em;
  color: #fff;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .22em;
  font-size: 10px;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .6);
}

.rec::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fff;
  animation: tally 1.6s ease-in-out infinite;
}

@keyframes tally {
  0%, 100% { opacity: 1; }
  50%      { opacity: .15; }
}

/* Tickets: a green bloom behind the stack that breathes, which is the
   only light on an otherwise black screen. */
.glow {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(120vw, 900px);
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(22, 172, 31, .22), transparent 70%);
  pointer-events: none;
  animation: breathe 7s ease-in-out infinite;
}

@keyframes breathe {
  0%, 100% { transform: scale(1);    opacity: .75; }
  50%      { transform: scale(1.12); opacity: 1; }
}

.kicker {
  margin: 0 0 1.1em;
  color: var(--green);
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .3em;
  font-size: clamp(10px, 1.3vw, 14px);
  opacity: .65;
}

/* Countdown. Fixed-width tabular figures: the seconds tick every second
   and the button underneath is where the asterisk lands, so the row is
   not allowed to change width. */
.count {
  display: flex;
  justify-content: center;
  gap: clamp(10px, 2.2vw, 28px);
  margin-top: clamp(22px, 3.4vh, 40px);
}

.unit {
  display: grid;
  justify-items: center;
  gap: .5em;
  min-width: clamp(52px, 8vw, 92px);
  padding: .7em .4em .6em;
  border: 1px solid rgba(22, 172, 31, .3);
  border-radius: 3px;
  background: rgba(22, 172, 31, .05);
}

.unit b {
  font-family: "Helvetica Now Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-size: clamp(22px, 3.4vw, 42px);
  line-height: 1;
  letter-spacing: -.02em;
}

.unit span {
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .18em;
  font-size: clamp(9px, 1.1vw, 12px);
  opacity: .55;
}

/* Tiers. A price list, not cards: rules between rows, the live one is
   the only thing on the screen carrying a solid fill. */
.tiers {
  list-style: none;
  margin: clamp(24px, 3.6vh, 44px) 0 0;
  padding: 0;
  width: min(92vw, 520px);
  text-align: left;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
}

.tiers li {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: baseline;
  gap: clamp(10px, 2vw, 22px);
  padding: .85em .2em;
  border-top: 1px solid rgba(22, 172, 31, .22);
  letter-spacing: .04em;
  font-size: clamp(13px, 1.9vw, 19px);
}

.tiers li:last-child { border-bottom: 1px solid rgba(22, 172, 31, .22); }

.tiers span { font-variant-numeric: tabular-nums; }

.tiers i {
  justify-self: end;
  min-width: 6.4em;
  padding: .3em .7em;
  border: 1px solid rgba(22, 172, 31, .35);
  border-radius: 999px;
  text-align: center;
  font-style: normal;
  letter-spacing: .16em;
  font-size: clamp(9px, 1.1vw, 11px);
  opacity: .6;
}

.tiers .live i {
  background: var(--green);
  border-color: var(--green);
  color: #000;
  opacity: 1;
}

.tiers li:not(.live) b,
.tiers li:not(.live) span { opacity: .45; }

/* The button gets a ring so it still reads as a button before the
   asterisk has arrived to fill it, and a hover that lifts rather than
   recolours: the fill belongs to the animation. */
.cta {
  outline: 1px solid rgba(22, 172, 31, .45);
  outline-offset: 0;
  transition: transform .35s cubic-bezier(.2,.7,.2,1), outline-offset .35s ease;
}

.cta:hover { transform: translateY(-2px); outline-offset: 5px; }

.jump, footer a { transition: opacity .3s ease; }
.jump:hover, footer a:hover { opacity: .6; }

/* Sponsor bar: the white plate is the loudest thing in the page, so it
   sits back until it is looked at. */
.brands {
  opacity: .78;
  transition: opacity .4s ease, transform .5s cubic-bezier(.2,.7,.2,1);
}

.brands:hover { opacity: 1; transform: translateY(-3px); }

@media (max-width: 600px) {
  .corner { top: 34px; font-size: 10px; }
  .tiers i { min-width: 0; }
  .tiers li { grid-template-columns: 1fr auto; row-gap: .2em; }
  .tiers i { grid-column: 2; }
}

/* The grain and the flying asterisk both sit above the sections. The
   grain has to stay on top of the asterisk so the pill picks up the
   same texture as everything else. */
.star { z-index: 4; }

/* The hero vignette paints at z-index 2, same as the masthead, and comes
   later in the DOM, so the type has to be lifted clear of it. */
.hero .masthead { z-index: 3; }

/* The ticket stack is the tallest column in the page: date, countdown,
   three tiers and the button. On a short landscape window the date has to
   give way first, since it is the one element sized off viewport width. */
@media (min-width: 601px) and (max-height: 900px) {
  .when { font-size: clamp(56px, 9vh, 120px); }
  .count { margin-top: 2.4vh; }
  .unit { padding: .5em .4em .45em; }
  .unit b { font-size: clamp(20px, 3vh, 32px); }
  .tiers { margin-top: 2.6vh; }
  .tiers li { padding: .6em .2em; }
  .cta { margin-top: 1.6em; }
}


/* ============================================================
   Billing screen, second pass. The old layout centred a fixed
   46vh column in a 100vh box, so a third of the screen was dead
   green above the name and another third below the frame. It is
   now four bands stacked to the full height, with the frame taking
   whatever is left over instead of being sized in advance.
   ============================================================ */

.bill {
  grid-template-rows: auto 1fr auto;
  gap: clamp(14px, 2.4vh, 26px);
  padding: 34px 24px calc(46px + clamp(14px, 2.4vh, 26px));
  align-content: stretch;
}

/* Header band: the ledger line across the top. Three slots, the
   middle one dropping out on narrow screens where it would only
   repeat what the previous screen already said. */
.band {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  padding-bottom: 12px;
  border-bottom: 2px solid #000;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .2em;
  font-size: clamp(10px, 1.2vw, 13px);
}

.band .mid { opacity: .5; }

/* The column no longer centres itself in dead space: it fills the
   1fr row, and min-height:0 is what lets the frame inside it shrink
   below its content on a short window instead of overflowing. */
.stage {
  align-self: stretch;
  min-height: 0;
  justify-content: center;
  gap: 0;
  translate: none;
}

/* The frame is the flexible one. It takes the leftover height and the
   video reads its width back off the 3:4, so the same rule holds a
   tall portrait on a phone and a small one on a short laptop. */
.frame {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: grid;
  place-items: center;
}

.video {
  height: 100%;
  width: auto;
  max-width: 100%;
  aspect-ratio: 3 / 4;
}

/* Footer band: the set's facts, which is what the bottom third was
   missing. Four columns on a wide screen, two on a phone, so the row
   never wraps into a ragged third line. */
.specs {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px clamp(12px, 2.4vw, 32px);
  margin: 0;
  padding-top: 12px;
  border-top: 2px solid #000;
  text-align: left;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
}

.specs dt {
  letter-spacing: .2em;
  font-size: clamp(9px, 1.1vw, 12px);
  opacity: .5;
}

.specs dd {
  margin: .45em 0 0;
  letter-spacing: .01em;
  font-size: clamp(14px, 1.9vw, 22px);
}

@media (max-width: 600px) {
  .band .mid { display: none; }
  .specs { grid-template-columns: repeat(2, 1fr); }
}

/* the tally light rides the frame's inner edge, not the old padding */
.video .rec { left: 14px; bottom: 14px; }


/* ============================================================
   Billing screen on a wide window. The 3:4 crop is a phone shape:
   on a landscape screen it leaves two columns of dead green either
   side of a thumbnail. Wide windows get the clip in its own 16:9
   instead, which is also the ratio it was shot in, so the hand crop
   inside stops throwing away the edges of the frame.
   ============================================================ */

@media (min-width: 901px) {
  /* The frame is sized off the height left over, not off the width,
     so the column can never grow past the screen. max-width is only
     a backstop for a very wide, very short window. */
  .video {
    aspect-ratio: 16 / 9;
    height: 100%;
    width: auto;
    max-width: 100%;
  }

  /* The name gives up some size to pay for the frame: at 11vw it was
     eating the height the clip needs to be worth showing. */
  .act { font-size: clamp(72px, 7.4vw, 132px); }
  .set { font-size: clamp(20px, 2.2vw, 36px); }

  /* A landscape frame is wide enough that the connector reads better
     short and centred than as a long drop. */
  .lead { height: clamp(18px, 3vh, 34px); }
}

/* Tablets keep the portrait crop: the screen is still taller than it
   is wide, so 16:9 would waste more than it gains. */
@media (min-width: 601px) and (max-width: 900px) {
  .act { font-size: clamp(80px, 10vw, 130px); }
}


/* ============================================================
   Sound control and the scribbles.
   ============================================================ */

/* The clip cannot come up with sound on its own: browsers hold audio
   behind a real gesture. So the page carries a control that says what
   the state is rather than pretending it is already playing. */
.sound {
  position: fixed;
  right: 20px;
  bottom: 68px;
  z-index: 7;
  display: inline-flex;
  align-items: center;
  gap: .7em;
  padding: .7em 1.1em;
  border: 1px solid rgba(0, 0, 0, .35);
  border-radius: 999px;
  background: transparent;
  color: #000;
  cursor: pointer;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .18em;
  font-size: 11px;
  opacity: 0;
  transform: translateY(10px);
  pointer-events: none;
  transition: opacity .45s ease, transform .45s cubic-bezier(.2,.7,.2,1),
              background .3s ease, color .3s ease;
}

.sound.show { opacity: 1; transform: none; pointer-events: auto; }
.sound.on { background: #000; color: var(--green); border-color: #000; }

/* Four bars: flat while muted, dancing once the sound is actually up.
   The animation is paused rather than removed so they hold their
   staggered heights instead of snapping to level. */
.bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 12px;
}

.bars i {
  width: 2px;
  height: 3px;
  background: currentColor;
  animation: eq .9s ease-in-out infinite alternate;
  animation-play-state: paused;
}

.bars i:nth-child(2) { animation-duration: .62s; }
.bars i:nth-child(3) { animation-duration: 1.1s; }
.bars i:nth-child(4) { animation-duration: .78s; }

.sound.on .bars i { animation-play-state: running; }

@keyframes eq {
  from { height: 3px; }
  to   { height: 12px; }
}

/* ---- Scribbles ----------------------------------------------------
   Marker marks in the margins: the thing a poster gets when someone
   has drawn on it. They sit under the type and over the backgrounds,
   never in the middle of a screen, and each one draws itself in when
   its screen arrives. Sized in vw so they stay proportionate rather
   than turning into stickers on a big monitor. */
.scribble {
  position: absolute;
  z-index: 1;
  pointer-events: none;
  overflow: visible;
  stroke: var(--green);
  stroke-width: 3.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
  transition: opacity .8s ease var(--sd, .3s);
}

.scribble.bk { stroke: #000; opacity: 0; }

.in .scribble { opacity: .5; }
.in .scribble.bk { opacity: .32; }

/* The draw-on. 900 is longer than any path here, so one dash value
   covers every shape without measuring each one. */
.scribble path {
  stroke-dasharray: 900;
  stroke-dashoffset: 900;
}

.in .scribble path {
  animation: draw 1.5s cubic-bezier(.3,.8,.3,1) forwards;
  animation-delay: var(--sd, .3s);
}

.scribble path:nth-child(2) { animation-delay: calc(var(--sd, .3s) + .45s); }

@keyframes draw {
  to { stroke-dashoffset: 0; }
}

/* A slow drift on top of the draw, so they never sit perfectly still.
   It is on the element and the draw is on the paths, which keeps the
   two animations from fighting over the same property. */
.scribble {
  animation: sway 11s ease-in-out infinite;
}

@keyframes sway {
  0%, 100% { rotate: 0deg;   translate: 0 0; }
  50%      { rotate: 2.5deg; translate: 4px -6px; }
}

/* Placement. Each one is pinned to a margin the layout was not using. */
.s-swirl  { left: -3vw;  bottom: 16vh; width: clamp(120px, 15vw, 230px); --sd: .8s; }
.s-burst  { right: 7vw;  top: 26vh;    width: clamp(48px, 5vw, 84px);   --sd: 1s; }

.s-loop   { left: 2vw;   bottom: 20vh; width: clamp(110px, 13vw, 210px); --sd: .7s; }
.s-zig    { right: 3vw;  top: 30vh;    width: clamp(90px, 11vw, 170px);  --sd: .9s; }
.s-ring   { right: 6vw;  bottom: 24vh; width: clamp(70px, 8vw, 130px);   --sd: 1.1s; }

.s-scrawl { left: 3vw;   top: 16vh;    width: clamp(120px, 15vw, 240px); --sd: .5s; }
.s-star   { right: 8vw;  top: 22vh;    width: clamp(44px, 5vw, 80px);    --sd: .8s; }
.s-wave   { left: 50%;   bottom: 12vh; width: clamp(160px, 22vw, 340px);
            translate: -50% 0; opacity: 0; --sd: 1s; }

.s-tail   { right: 2vw;  top: 12vh;    width: clamp(110px, 13vw, 200px); --sd: .5s; }
.s-cross  { left: 5vw;   top: 20vh;    width: clamp(40px, 4.5vw, 70px);  --sd: .8s; }

/* The wave is centred with a translate, which sway would overwrite, so
   it gets the drift taken off it and keeps its position. */
.s-wave { animation: none; }
.in .s-wave { opacity: .4; }

@media (max-width: 600px) {
  /* On a phone the margins are gone. Keep the two that still have a
     corner to live in and drop the rest rather than crowding the type. */
  .s-burst, .s-ring, .s-star, .s-cross, .s-zig { display: none; }
  .scribble { stroke-width: 4.4; }
  .s-swirl { left: -8vw; bottom: 22vh; width: 130px; }
  .s-loop  { left: -6vw; bottom: 26vh; width: 120px; }
  .sound { right: 14px; bottom: 60px; }
}

@media (prefers-reduced-motion: reduce) {
  .scribble { animation: none; }
  .in .scribble path { animation: none; stroke-dashoffset: 0; }
  .bars i { animation: none; }
}

/* The hero's vignette paints at z-index 2 over everything below it, so
   its two marks have to sit above the vignette or they get faded out at
   exactly the edges they were placed in. */
.hero .scribble { z-index: 3; }

/* Aniko has a pulse. Two beats and a long rest, not a sine wave: an
   even swell reads as a slow zoom, where the double kick reads as a
   heart. The amplitude is deliberately small, around 4% at the peak,
   because the plate is full height and anything more turns into the
   frame sliding about behind the type.

   It goes on the img rather than the figure because the figure's
   transform belongs to the pointer parallax, and the two would
   overwrite each other. */
/* The plate is scaled from script now, off the low end of the track,
   so the canned heartbeat comes off. The origin stays: the pulse
   should come from her face, not from the middle of the crop. */
.aniko img {
  transform-origin: 62% 45%;
}

@keyframes pulse {
  0%   { transform: scale(1); }
  7%   { transform: scale(1.038); }  /* lub */
  14%  { transform: scale(1.012); }
  21%  { transform: scale(1.046); }  /* dub */
  32%  { transform: scale(1.004); }
  100% { transform: scale(1); }      /* rest */
}

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


/* ============================================================
   Second wave of scribbles. Same rules as the first: margins only,
   under the type, drawn on their screen's reveal. The set is mixed
   on purpose - a couple of gestural strokes, a couple of hard marks
   like the plus and the burst, so it reads as someone's hand rather
   than a pack of icons.
   ============================================================ */

/* the third stroke in a mark waits its turn like the second does */
.scribble path:nth-child(3) { animation-delay: calc(var(--sd, .3s) + .8s); }
.scribble path:nth-child(4) { animation-delay: calc(var(--sd, .3s) + 1.1s); }

/* Break up the drift. All of them swaying on one 11s clock reads as
   the page wobbling; staggered clocks read as separate marks. */
.scribble:nth-of-type(even) { animation-duration: 14s; animation-direction: reverse; }
.scribble:nth-of-type(3n)   { animation-duration: 9s; }

/* ---- Hero ---- */
.s-arrow  { right: 26vw; top: 9vh;    width: clamp(90px, 11vw, 175px); --sd: 1.2s; }
.s-spiral { right: 3vw;  bottom: 27vh; width: clamp(70px, 8vw, 130px); --sd: 1.4s; }
.s-dots   { left: 5vw;   top: 60vh;   width: clamp(70px, 8vw, 120px);  --sd: 1.6s; }

/* ---- Billing ---- */
.s-bracket { left: 3vw;  top: 28vh;    width: clamp(26px, 3vw, 48px);   --sd: 1.2s; }
.s-heart   { right: 7vw; top: 15vh;    width: clamp(46px, 5.5vw, 88px); --sd: 1.4s; }
.s-swash   { left: 4vw;  bottom: 21vh; width: clamp(110px, 14vw, 220px); --sd: 1.5s; }
.s-plus    { right: 15vw; bottom: 29vh; width: clamp(34px, 4vw, 62px);  --sd: 1.7s; }

/* ---- Tickets ---- */
/* The ring goes round the date rather than sitting beside it, which is
   the one place a mark is allowed off the margin: it is annotating
   something, not decorating a corner. */
.s-circle { left: 50%; top: 27vh; width: clamp(230px, 34vw, 440px);
            translate: -50% 0; animation: none; --sd: 1.3s; }
.in .s-circle { opacity: .38; }

.s-point  { left: 15vw;  bottom: 24vh; width: clamp(56px, 7vw, 110px);  --sd: 1.5s; }
.s-spark  { right: 17vw; top: 20vh;    width: clamp(38px, 4.5vw, 68px); --sd: 1.7s; }
.s-box    { right: 5vw;  bottom: 28vh; width: clamp(90px, 11vw, 175px); --sd: 1.9s; }

/* ---- Closing ---- */
.s-swoosh { left: 3vw;   top: 8vh;     width: clamp(130px, 16vw, 260px); --sd: 1.1s; }
.s-bang   { right: 11vw; top: 24vh;    width: clamp(20px, 2.4vw, 38px);  --sd: 1.3s; }
.s-eye    { left: 7vw;   bottom: 24vh; width: clamp(80px, 10vw, 150px);  --sd: 1.5s; }

/* On a phone there are no margins to draw in. Each screen keeps two or
   three marks that have a corner of their own; the rest would land on
   the type. */
@media (max-width: 600px) {
  .s-arrow, .s-dots, .s-heart, .s-plus, .s-spark, .s-box, .s-bang { display: none; }

  .s-spiral  { right: -4vw; bottom: 30vh; width: 84px; }
  .s-bracket { left: -1vw;  top: 30vh;    width: 30px; }
  .s-swash   { left: 2vw;   bottom: 17vh; width: 130px; }
  .s-circle  { top: 30vh;   width: 88vw; }
  .s-point   { left: 6vw;   bottom: 21vh; width: 64px; }
  .s-swoosh  { left: -2vw;  top: 6vh;     width: 150px; }
  .s-eye     { left: 4vw;   bottom: 20vh; width: 90px; }
}


/* ============================================================
   Entrances. Everything that is a piece of art rather than a piece
   of type now arrives instead of being there: the plate, both
   lockups, the 14A mark, the frame, the sponsor bar and the
   asterisk. Type keeps the .rise it already had - the two run off
   the same .in trigger so a screen comes in as one move.
   ============================================================ */

/* Curtain. The page is black anyway, so this is not a loader: it is
   half a second of held black so the hero's first frame is the plate
   arriving rather than the plate already sitting there. */
.curtain {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: #000;
  pointer-events: none;
  animation: lift .7s ease .25s both;
}

@keyframes lift {
  to { opacity: 0; visibility: hidden; }
}

/* The plate pushes in. It is masked at the edges already, so a scale
   from 1.06 reads as the frame settling rather than as a zoom, and the
   heartbeat picks up from where this lands. */
.aniko {
  animation: plate 1.6s cubic-bezier(.16, 1, .3, 1) .35s both;
}

@keyframes plate {
  from { opacity: 0; scale: 1.07; }
  to   { opacity: 1; scale: 1; }
}

/* The lockup is two crops stacked, which gives it its own entrance for
   free: wipe the top band across, then the bottom, and it draws on the
   way a sign gets painted. */
.lockup i {
  clip-path: inset(0 100% 0 0);
}

.in .lockup .lt { animation: wipe .85s cubic-bezier(.2,.8,.2,1) .5s both; }
.in .lockup .lb { animation: wipe .85s cubic-bezier(.2,.8,.2,1) .68s both; }

@keyframes wipe {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 -2% 0 0); }
}

/* The 14A is a stamp, so it lands like one: down onto the page and
   slightly over size, not faded up. */
.partner {
  animation: stamp .6s cubic-bezier(.2, 1.4, .4, 1) .95s both;
}

@keyframes stamp {
  from { opacity: 0; scale: 1.35; }
  60%  { opacity: 1; }
  to   { opacity: 1; scale: 1; }
}

/* The closing screen's marks are the same art, so they take the same
   entrances - just off that screen's own .in, later down the page. */
.closing .partner { animation-delay: .55s; }

/* The asterisk is positioned by script on every frame, so its transform
   is spoken for. scale and opacity are separate properties and compose
   with it, which is what lets it have an entrance at all. */
.star {
  animation: pop .7s cubic-bezier(.2, 1.5, .4, 1) 1.1s both;
}

@keyframes pop {
  from { opacity: 0; scale: 0; }
  to   { opacity: 1; scale: 1; }
}

/* The frame comes up from under its own bottom edge, so the clip looks
   like it is being raised into the layout rather than fading in. */
.in .frame {
  animation: raise 1s cubic-bezier(.16, 1, .3, 1) .55s both;
}

@keyframes raise {
  from { opacity: 0; clip-path: inset(0 0 100% 0); translate: 0 18px; }
  to   { opacity: 1; clip-path: inset(0 0 -4% 0); translate: 0 0; }
}

/* The sponsor bar is a white plate on a black page: it earns a slower,
   plainer arrival than everything else so it does not flash. */
.in .brands {
  /* backwards, not both: a forwards fill would pin the opacity at .78
     for good and the hover on the bar would stop working. */
  animation: bar 1.1s cubic-bezier(.16, 1, .3, 1) .5s backwards;
}

@keyframes bar {
  from { opacity: 0; clip-path: inset(0 50% 0 50%); }
  to   { opacity: .82; clip-path: inset(0 -1% 0 -1%); }
}

@media (prefers-reduced-motion: reduce) {
  .curtain { display: none; }
  .aniko, .partner, .star { animation: none; }
  .lockup i { clip-path: none; }
  .in .lockup .lt, .in .lockup .lb,
  .in .frame, .in .brands { animation: none; }
}


/* (The block that used to size the act at the foot of the hero lived
   here. It was written when the second screen still existed and has
   been superseded by the rules further down, which do the same job
   with the current layout.) */

/* The green screen is now a single frame in a box: header rule, clip,
   spec row. Nothing to centre against, so the clip just takes the row. */
.bill .stage {
  display: block;
  align-self: stretch;
  min-height: 0;
}

.bill .frame {
  height: 100%;
  width: 100%;
  display: grid;
  place-items: center;
}

/* With the whole middle band to itself the clip can run bigger than it
   did when it was sharing the screen with the name. */
.bill .video { height: 100%; }

/* the third anchor: a stamp on the frame's top right corner */
#slot-c { position: absolute; right: 4%; top: 3%; width: 0; height: 0; }
.bill .frame { position: relative; }

@media (min-width: 901px) {
  /* Landscape here, so the limit is width rather than height. */
  .bill .video { max-width: min(100%, 168vh); }
}

@media (max-width: 600px) {
}

/* The tickets screen loses its bloom: on a flat black page a radial
   wash is the one thing that cannot hold an edge, and it was reading as
   a smudge behind the type rather than as light. */
.glow { display: none; }

/* ============================================================
   Sponsor bar with no plate at all.

   The white is gone from the file rather than from the CSS: every
   pixel's alpha is now 255 minus its darkest channel, un-premultiplied
   against the white it was sitting on, so the marks keep their own
   colours and the ground has nothing left to show. The Native N was
   the one mark printed in black, so it is the one mark inverted - on a
   black page it would otherwise be a hole in the row.
   ============================================================ */

.brands {
  filter: none;
  background: none;
  padding: 0;
  border-radius: 0;
}

/* No plate to lift out of, so the hover just brings it up full. */
.brands:hover { transform: none; opacity: 1; }

/* ============================================================
   The green screen is gone. What it owned - the clip, the header
   rule, the spec row, its ticker and its sound control - went with
   it. These rules are kept rather than deleted because the concept
   has been through several passes and the block may come back; they
   cost nothing with no .bill in the page.

   What does need saying: the flow line and the asterisk were both
   built to change colour as they crossed that screen. Both now run
   green the whole way down, handled in star.js.
   ============================================================ */

/* Three screens, all black, so the arrow on the hero is pointing at
   the tickets one screen down rather than two. */
.hero .jump { bottom: 92px; }

/* ============================================================
   The act goes back to the floor of the hero. Promoter's stack in
   the top left, act along the bottom: the two ends of a poster,
   with Aniko's face in the space between them.
   ============================================================ */

.hero .billing {
  position: absolute;
  left: 24px;
  bottom: clamp(155px, 19vh, 215px);
  z-index: 3;
  max-width: min(92vw, 700px);
  margin-top: 0;
  color: var(--green);
  text-align: left;
}

.hero .billing p { margin: 0; }

/* Down here it has the width of the screen rather than a column under
   a tracked label, so the name goes back up to poster scale. */
.hero .act {
  font-size: clamp(58px, 11vw, 168px);
  line-height: .78;
}

.hero .set {
  margin-top: .38em;
  font-size: clamp(16px, 2.6vw, 34px);
}

/* The masthead can have its old top margin back: it is a three-block
   stack again, not four. */
.masthead { top: 40px; }

@media (max-width: 600px) {
  .hero .billing { left: 20px; bottom: 17vh; }
  .hero .act { font-size: clamp(52px, 15vw, 84px); }
  .hero .set { font-size: 15px; }
}

/* The label is off the hero's arrow: the button it points at says "buy
   tickets" itself, and on a screen this quiet the mark carries further
   than the word did. The link keeps its name through aria-label so it
   still announces as "buy tickets" rather than as a bare link.

   Without the word under it there is no underline to hold the pair
   together, and the arrow can run bigger than 15px. */
.jump {
  text-decoration: none;
  gap: 0;
}

.jump svg { width: 26px; height: 26px; stroke-width: 2; }

@media (max-width: 600px) {
  .jump svg { width: 22px; height: 22px; }
}

/* The set line is measured and tracked in script, so it has to be an
   inline box the measurement can read, and it must not wrap while the
   tracking is being worked out. */
.set .fit {
  display: inline-block;
  white-space: nowrap;
}

/* The underline belonged to the old centred layout, where it tied the
   line to the connector below it. In a locked block the two lines share
   an edge already, and a rule under a tracked line only blurs it. */
.hero .set { text-decoration: none; }

/* Two spans rather than a line break, so the same markup can stack on a
   wide screen and run along one line on a phone. */
.corner.tr {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

@media (max-width: 600px) {
  /* On a phone the slug drops out of the top corner and sits on the end
     of the set line instead: name and set on the left, place and date on
     the right, one rule of information across the foot of the poster.

     It is pinned to the same bottom as the billing block, which is where
     the set line's own baseline sits, so the two line up without either
     one knowing about the other. */
  .hero .corner.tr {
    top: auto;
    bottom: 17vh;
    right: 20px;
    flex-direction: row;
    align-items: baseline;
    gap: .8em;
    line-height: 1;
  }

  /* A hairline between them, since they are no longer separated by
     being on different lines. */
  .hero .corner.tr span + span {
    padding-left: .8em;
    border-left: 1px solid currentColor;
  }
}


/* ============================================================
   Ambient motion. Every block on every screen drifts on its own
   slow clock, so a screen sitting still still has a pulse in it.

   One rule holds the whole system together: ambient motion only
   ever uses the standalone `translate` and `rotate` properties,
   never `transform`. The entrances use transform, clip-path,
   scale and opacity, and the reveals transition transform. Keeping
   the drift on a different property means the two compose instead
   of overwriting each other, and nothing needs sequencing.
   ============================================================ */

.masthead   { animation: drift-a 11s ease-in-out infinite; }
.hero .billing { animation: drift-b 13s ease-in-out infinite; }
.hero .corner  { animation: drift-c 9s ease-in-out infinite; }
.aniko      { animation-name: plate, drift-plate;
              animation-duration: 1.6s, 17s;
              animation-timing-function: cubic-bezier(.16,1,.3,1), ease-in-out;
              animation-delay: .35s, 0s;
              animation-iteration-count: 1, infinite;
              animation-fill-mode: both, none; }

.ticketing  { animation: drift-b 12s ease-in-out infinite; }
.tiers      { animation: drift-c 15s ease-in-out infinite; }
.count      { animation: drift-a 10s ease-in-out infinite .6s; }

.sponsors   { animation: drift-a 14s ease-in-out infinite; }
.closing footer { animation: drift-c 12s ease-in-out infinite 1s; }

/* Three amplitudes, so the screens do not all sway on the same beat.
   Nothing here travels more than a few pixels: past that it stops
   reading as the page being alive and starts reading as a bug. */
@keyframes drift-a {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -7px; }
}

@keyframes drift-b {
  0%, 100% { translate: 0 0; }
  33%      { translate: 3px -5px; }
  66%      { translate: -3px 4px; }
}

@keyframes drift-c {
  0%, 100% { translate: 0 0; }
  50%      { translate: -4px 3px; }
}

/* The plate gets more room than the type: it is the biggest thing on
   the page, so the same few pixels would barely register on it. */
@keyframes drift-plate {
  0%, 100% { translate: 0 0; }
  50%      { translate: -10px 6px; }
}

@media (prefers-reduced-motion: reduce) {
  .masthead, .hero .billing, .hero .corner, .ticketing, .tiers,
  .count, .sponsors, .closing footer { animation: none; }
  .aniko { animation: none; }
}

/* ============================================================
   Squiggling. The marks now boil the way a hand-drawn line does
   when it is redrawn every few frames: a turbulence field pushes
   the stroke around, and the field's seed jumps to a new value
   four times a second rather than sliding, so the line snaps
   between shapes instead of oozing between them. Sliding is what
   makes a filter look like a filter; snapping is what makes it
   look drawn.

   The seed is stepped in SMIL rather than CSS because a filter's
   attributes are not CSS properties and keyframes cannot reach them.
   ============================================================ */

.defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.scribble { filter: url(#boil); }

/* The ring around the date is the one mark reading as an annotation on
   something rather than as decoration, and a boiling annotation reads
   as a mistake. It stays still. */
.s-circle { filter: none; }

@media (prefers-reduced-motion: reduce) {
  .scribble { filter: none; }
}

/* ============================================================
   The empty black on the hero. The event is called Black Noise, so
   the thing filling it is noise: three traces running across the
   lower half of the screen, drawn in script because a signal that
   repeats on a CSS loop stops being a signal after one cycle.

   It sits at z-index 0, under the plate and under the type, so it
   reads as something the page is sitting on rather than a graphic
   in the layout. A faint grid goes behind it to give the black a
   floor: pure #000 has no depth to it at all, and any texture at
   all is enough to stop the screen looking switched off.
   ============================================================ */

.hero {
  background-image:
    radial-gradient(circle at center, rgba(22, 172, 31, .14) .9px, transparent 1px);
  background-size: 26px 26px;
  background-position: -13px -13px;
}

.field {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* ============================================================
   Aniko gets a real matte, so things can pass behind her.

   The plate was keyed with mix-blend-mode: screen, which works
   beautifully against flat black - the shot is lit against black,
   so screening it over #000 drops the ground exactly. What screen
   cannot do is hide anything: it only ever brightens, so the signal
   running underneath was being added into her face rather than
   being covered by it.

   The fix is to key her with her own luminance instead. The bright
   parts of the shot become opaque, the near-black ground becomes
   transparent, and because the layer is now painting normally
   rather than blending, it occludes whatever is behind it. Her
   hair, which sits mid-grey, stays part transparent - so the trace
   bleeds faintly through it and is gone completely across her face,
   which is the right behaviour for a plate cut out of darkness.

   The two gradients are the edge fade that was already here; the
   image joins them as a third mask layer and all three intersect.
   ============================================================ */

.aniko {
  mix-blend-mode: normal;
}

.aniko img {
  /* Crushing the low end before the mask is read keeps the ground from
     lifting into a grey haze at the crop edges. */
  filter: contrast(1.35) brightness(1.04);

  -webkit-mask-image:
    url(assets/aniko.jpg),
    linear-gradient(to right, transparent 0, #000 22%),
    linear-gradient(to bottom, transparent 0, #000 6%, #000 94%, transparent 100%);
  -webkit-mask-size: 100% 100%, auto, auto;
  -webkit-mask-source-type: luminance, alpha, alpha;
  -webkit-mask-composite: source-in;

  mask-image:
    url(assets/aniko.jpg),
    linear-gradient(to right, transparent 0, #000 22%),
    linear-gradient(to bottom, transparent 0, #000 6%, #000 94%, transparent 100%);
  mask-size: 100% 100%, auto, auto;
  mask-mode: luminance, alpha, alpha;
  mask-composite: intersect;
}

/* The traces can now run right across the frame: the part of them that
   lands on her is hidden, so there is no need to keep them low and out
   of her way. */

/* ============================================================
   Crushing the masks. Both of these files are JPEGs whose black is
   about #0d0d0d, not #000, and a luminance mask reads that as five
   percent alpha rather than none.

   On the lockup that paints a faint green wash across the whole
   477x197 box - the grey rectangle sitting behind the letters. On
   Aniko it means her mid-tones never go fully opaque, so anything
   drawn behind her shows through her face.

   filter cannot fix it: filter applies to the element being
   painted, not to the image being used as its mask. Stacking the
   mask on itself does, because intersecting two mask layers
   multiplies their alpha. 0.05 x 0.05 is 0.0025, which is nothing;
   1.0 x 1.0 is still 1.0. The dark end collapses and the bright
   end is untouched, which is exactly the curve wanted.
   ============================================================ */

.lt {
  -webkit-mask-image: url(assets/logo-top.jpg), url(assets/logo-top.jpg);
          mask-image: url(assets/logo-top.jpg), url(assets/logo-top.jpg);
}

.lb {
  -webkit-mask-image: url(assets/logo-bot.jpg), url(assets/logo-bot.jpg);
          mask-image: url(assets/logo-bot.jpg), url(assets/logo-bot.jpg);
}

.lt, .lb {
  -webkit-mask-size: 100% 100%, 100% 100%;
          mask-size: 100% 100%, 100% 100%;
  -webkit-mask-source-type: luminance, luminance;
          mask-mode: luminance, luminance;
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

/* Aniko gets three passes of herself: her ground is the same washed
   black, and she is carrying a signal behind her that will read through
   anything left translucent. */
.aniko img {
  -webkit-mask-image:
    url(assets/aniko.jpg),
    url(assets/aniko.jpg),
    url(assets/aniko.jpg),
    linear-gradient(to right, transparent 0, #000 22%),
    linear-gradient(to bottom, transparent 0, #000 6%, #000 94%, transparent 100%);
  -webkit-mask-size: 100% 100%, 100% 100%, 100% 100%, auto, auto;
  -webkit-mask-source-type: luminance, luminance, luminance, alpha, alpha;
  -webkit-mask-composite: source-in;

  mask-image:
    url(assets/aniko.jpg),
    url(assets/aniko.jpg),
    url(assets/aniko.jpg),
    linear-gradient(to right, transparent 0, #000 22%),
    linear-gradient(to bottom, transparent 0, #000 6%, #000 94%, transparent 100%);
  mask-size: 100% 100%, 100% 100%, 100% 100%, auto, auto;
  mask-mode: luminance, luminance, luminance, alpha, alpha;
  mask-composite: intersect;

  /* Three passes eat a lot of the midtones, so the plate is lifted
     back up to hold her hair and the edge of her jaw. */
  filter: contrast(1.15) brightness(1.5);
}

/* ============================================================
   Aniko back to one mask pass.

   Three passes was wrong. Stacking a luminance mask does not just
   crush the ground, it multiplies the whole curve: a midtone at 0.5
   goes to 0.125 and even a highlight at 0.8 falls to 0.51. Most of
   her is midtone, so the plate went translucent everywhere and read
   as dark.

   One pass gives alpha = luminance, and the pixel painted through it
   is colour x alpha, so the plate lands at roughly luminance squared
   - still darker than the old screen blend, which landed at plain
   luminance. That is the cost of an opaque plate, and it is paid
   back in brightness on the source rather than in more mask layers.

   The 5% wash the ground leaves behind is real but invisible here:
   it is a dark image over a black page, inside an edge fade. On the
   lockup it mattered because that box was solid green; the double
   pass stays there.
   ============================================================ */

.aniko img {
  -webkit-mask-image:
    url(assets/aniko.jpg),
    linear-gradient(to right, transparent 0, #000 22%),
    linear-gradient(to bottom, transparent 0, #000 6%, #000 94%, transparent 100%);
  -webkit-mask-size: 100% 100%, auto, auto;
  -webkit-mask-source-type: luminance, alpha, alpha;
  -webkit-mask-composite: source-in;

  mask-image:
    url(assets/aniko.jpg),
    linear-gradient(to right, transparent 0, #000 22%),
    linear-gradient(to bottom, transparent 0, #000 6%, #000 94%, transparent 100%);
  mask-size: 100% 100%, auto, auto;
  mask-mode: luminance, alpha, alpha;
  mask-composite: intersect;

  /* Undoing the squaring. Brightness is what buys the highlights back
     without touching the mask, so her face keeps the punch it had
     under the old blend while still being opaque enough to hide the
     signal running behind it. */
  filter: brightness(1.95) contrast(1.05);
}

/* ============================================================
   Once the green screen plate is keyed, all of the luminance
   machinery comes off. A real matte does not need the mask, does
   not need the brightness that was compensating for the squaring,
   and does not need the contrast crush that was holding the ground
   down. Only the edge fade stays, and that is layout rather than
   keying: it stops the crop reading as a rectangle.
   ============================================================ */

.aniko.keyed img {
  filter: none;

  -webkit-mask-image:
    linear-gradient(to right, transparent 0, #000 22%),
    linear-gradient(to bottom, transparent 0, #000 6%, #000 94%, transparent 100%);
  -webkit-mask-size: auto, auto;
  -webkit-mask-source-type: alpha, alpha;
  -webkit-mask-composite: source-in;

  mask-image:
    linear-gradient(to right, transparent 0, #000 22%),
    linear-gradient(to bottom, transparent 0, #000 6%, #000 94%, transparent 100%);
  mask-size: auto, auto;
  mask-mode: alpha, alpha;
  mask-composite: intersect;
}

/* ============================================================
   Grid on the other two black screens. Same idea as the hero -
   pure #000 has no depth and reads as switched off - but not the
   same measurements, or the three screens look like one screen
   scrolled past three times.

   The tickets screen gets a tighter, dimmer grid so the price list
   sits on something that reads as ruled paper. The closing screen
   gets a wider, fainter one, because it is the quietest screen on
   the page and should stay that way.
   ============================================================ */

.tickets {
  background-image:
    radial-gradient(circle at center, rgba(22, 172, 31, .10) .8px, transparent 1px);
  background-size: 18px 18px;
  background-position: -9px -9px;
}

.closing {
  background-image:
    radial-gradient(circle at center, rgba(22, 172, 31, .07) .8px, transparent 1px);
  background-size: 38px 38px;
  background-position: -19px -19px;
}

/* The canvases go under everything the screens hold. The ticket stack
   and the credits already sit above this, but the scribbles are at
   z-index 1 and want to stay on top of the signal rather than under
   it: a mark drawn over interference reads as drawn on the page. */
.field { z-index: 0; }

/* ============================================================
   Real alpha masks for the marks.

   The lockup was reading darker green than the rest of the page and
   the reason was in the art: the "white" in logo-top.jpg peaks at a
   luminance of 190, not 255. Used as a luminance mask that is alpha
   0.75, so the letters were painting green at three-quarter strength
   over black - a darker green, from the same hex. The double pass
   added earlier made it worse, squaring 0.75 down to 0.55.

   CSS cannot fix this. Crushing the ground and lifting the letters is
   a levels curve, and mask-composite can only multiply or add - one
   of which darkens the letters, the other of which brightens the
   ground. So the curve is applied once, offline, and the result
   stored as a PNG that carries a real alpha channel: everything
   under luminance 40 is fully clear, everything over 170 is fully
   solid, and the ramp between is the antialiasing.

   These are plain alpha masks now, so mask-mode drops out too.
   ============================================================ */

.lt, .lb, .partner, .star .art {
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-source-type: alpha;
          mask-mode: alpha;
}

.lt {
  -webkit-mask-image: url(assets/logo-top-mask.png);
          mask-image: url(assets/logo-top-mask.png);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
}

.lb {
  -webkit-mask-image: url(assets/logo-bot-mask.png);
          mask-image: url(assets/logo-bot-mask.png);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
}

.partner {
  -webkit-mask: url(assets/14a-mask.png) center / contain no-repeat;
          mask: url(assets/14a-mask.png) center / contain no-repeat;
}

.star .art {
  -webkit-mask: url(assets/star-mask.png) center / contain no-repeat;
          mask: url(assets/star-mask.png) center / contain no-repeat;
}


/* ============================================================
   The gate. Everything the page does needs a gesture anyway -
   audio will not start without one - so rather than pretending
   otherwise, the page asks for it up front and spends the moment
   on the two marks.
   ============================================================ */

.gate {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: clamp(40px, 8vh, 90px);
  background: #000;
  cursor: pointer;
  transition: opacity .7s ease, visibility .7s;
}

.gate.gone { opacity: 0; visibility: hidden; }

.gate-marks {
  display: grid;
  justify-items: center;
  gap: clamp(26px, 5vh, 54px);
}

.gate .logo { width: clamp(240px, 36vw, 480px); }
.gate .partner { width: clamp(72px, 9vw, 120px); margin-top: 0; }

/* Typewriter. The text is written a character at a time in script
   rather than by a steps() animation on width, because a width
   animation cuts glyphs in half mid-stroke - it reveals pixels, not
   letters. Writing the string is the only way the caret lands
   between characters every time. */
.type {
  margin: 0;
  display: flex;
  align-items: center;
  gap: .08em;
  color: var(--green);
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .28em;
  font-size: clamp(11px, 1.5vw, 15px);
  min-height: 1.4em;
}

.caret {
  width: .6em;
  height: 1.05em;
  background: currentColor;
  animation: blink 1s steps(1) infinite;
}

@keyframes blink {
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0; }
}

/* Nothing behind the gate runs until it is dismissed. Otherwise the
   hero plays its whole entrance while nobody is looking at it and is
   already settled by the time the page is uncovered. */
html.boot { overflow: hidden; }
html.boot .aniko,
html.boot .partner,
html.boot .star { animation-play-state: paused; }

/* ============================================================
   Sound toggle. Back, and now driving a track rather than a clip.
   ============================================================ */

.sound {
  position: fixed;
  right: 20px;
  bottom: 22px;
  z-index: 9;
  display: inline-flex;
  align-items: center;
  gap: .7em;
  padding: .7em 1.1em;
  border: 1px solid rgba(22, 172, 31, .4);
  border-radius: 999px;
  background: rgba(0, 0, 0, .5);
  color: var(--green);
  cursor: pointer;
  font-family: "Helvetica Now Display Condensed", "HelveticaNeue-CondensedBold",
               "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-stretch: condensed;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .18em;
  font-size: 11px;
  transition: opacity .4s ease, transform .4s cubic-bezier(.2,.7,.2,1),
              background .3s ease, color .3s ease, border-color .3s ease;
}

.sound[hidden] { display: none; }
.sound.on { background: var(--green); color: #000; border-color: var(--green); }

.bars { display: flex; align-items: flex-end; gap: 2px; height: 12px; }

.bars i {
  width: 2px;
  height: 3px;
  background: currentColor;
  animation: eq .9s ease-in-out infinite alternate;
  animation-play-state: paused;
}

.bars i:nth-child(2) { animation-duration: .62s; }
.bars i:nth-child(3) { animation-duration: 1.1s; }
.bars i:nth-child(4) { animation-duration: .78s; }

.sound.on .bars i { animation-play-state: running; }

@keyframes eq {
  from { height: 3px; }
  to   { height: 12px; }
}

/* ============================================================
   "presents" flickers, the way a sign with a bad tube does: mostly
   lit, dropping out for a frame or two at intervals that do not
   divide evenly, so the eye cannot learn the pattern.
   ============================================================ */

.presents {
  color: #fff;
  animation: flicker 6.3s steps(1) infinite;
}

@keyframes flicker {
  0%, 100%    { opacity: 1; }
  41%         { opacity: 1; }
  42%         { opacity: .18; }
  43%         { opacity: 1; }
  57%         { opacity: 1; }
  57.6%       { opacity: .35; }
  58.2%       { opacity: 1; }
  58.8%       { opacity: .12; }
  59.4%       { opacity: 1; }
  83%         { opacity: 1; }
  83.7%       { opacity: .4; }
  84.4%       { opacity: 1; }
}

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

/* The button reads green on black like the rest of the screen, and only
   flips to black once the asterisk has landed behind it and become the
   pill. It was black from the start, which was right when the pill
   arrived with the scroll - now that the flight lags a beat behind, it
   spent that beat as black type on a black page. */
.cta { color: var(--green); }
.cta.filled { color: #000; }

/* ============================================================
   The asterisk, made fail-safe.

   Its width and height are written by script every frame. That is
   fine while the script is running and total if it is not: an
   element with no width is not a small asterisk, it is no asterisk,
   and nothing on the page says why. These are the size it takes at
   rest on the hero, so a broken frame leaves it sitting on the
   lockup rather than disappearing.
   ============================================================ */

.star {
  width: clamp(49px, 6.9vw, 101px);
  height: clamp(48px, 6.7vw, 98px);
}

/* backwards rather than both. After the entrance finishes the element
   goes back to its own opacity, which is 1, instead of being held
   there by the animation's forwards fill - one less thing that has to
   go right for the asterisk to be on screen. */
.star {
  animation: pop .7s cubic-bezier(.2, 1.5, .4, 1) 1.1s backwards;
}

/* "presents" is gone from the hero. The flicker keyframes stay - they
   are wired to the class, not to that one line, and cost nothing
   unused. */


/* ============================================================
   Mute button, as a floating control rather than a labelled pill.

   Round, bottom right, sitting above everything including the
   ticker. It shows three things at once: the four bars say whether
   sound is playing, the slash across them says it is muted, and the
   ring around it fills as the track plays through, so it doubles as
   a progress read on a four minute song.
   ============================================================ */

.fab {
  position: fixed;
  right: 22px;
  bottom: 22px;
  z-index: 9;
  width: 54px;
  height: 54px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(6px);
  color: var(--green);
  cursor: pointer;
  transition: transform .35s cubic-bezier(.2,.7,.2,1),
              background .3s ease, color .3s ease;
}

.fab[hidden] { display: none; }
.fab:hover { transform: scale(1.06); }
.fab:active { transform: scale(.96); }

/* Every layer sits in the same grid cell, stacked. */
.fab > * { grid-area: 1 / 1; }

/* The ring. A dash offset drives the fill, written from script off the
   track's own currentTime - rotated a quarter turn so it starts at
   twelve rather than at three. */
.ring { width: 54px; height: 54px; }
.ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }

.ring circle {
  fill: none;
  stroke-width: 1.5;
  /* 2 x pi x 16.5, the circumference the dash array is measured in */
  stroke-dasharray: 103.67;
}

.ring .track { stroke: rgba(22, 172, 31, .22); }

.ring .run {
  stroke: currentColor;
  stroke-linecap: round;
  stroke-dashoffset: 103.67;
  transition: stroke-dashoffset .3s linear;
}

.bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 14px;
}

.bars i {
  width: 2.5px;
  height: 3px;
  background: currentColor;
  border-radius: 1px;
  animation: eq .9s ease-in-out infinite alternate;
  animation-play-state: paused;
}

.bars i:nth-child(2) { animation-duration: .62s; }
.bars i:nth-child(3) { animation-duration: 1.1s; }
.bars i:nth-child(4) { animation-duration: .78s; }

.fab.on .bars i { animation-play-state: running; }

@keyframes eq {
  from { height: 3px; }
  to   { height: 14px; }
}

/* The slash. Drawn rather than swapped for a second icon, so muting is
   a stroke being struck through the bars and unmuting is it being
   taken away - the bars never disappear, which is what keeps the two
   states reading as one control. */
.slash {
  width: 30px;
  height: 1.8px;
  background: currentColor;
  border-radius: 2px;
  rotate: -45deg;
  transform-origin: center;
  scale: 0 1;
  transition: scale .3s cubic-bezier(.2,.7,.2,1);
}

.fab:not(.on) .slash { scale: 1 1; }

/* Muted state: the bars drop flat, the button dims, and the ring stops
   reading as live. */
.fab:not(.on) { color: rgba(255, 255, 255, .55); }
.fab:not(.on) .bars i { height: 3px; }

@media (max-width: 600px) {
  .fab { right: 14px; bottom: 76px; width: 48px; height: 48px; }
  .ring { width: 48px; height: 48px; }
}

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

/* ============================================================
   Celebration marks behind the ticket stack.

   This was a crowd of drawn dancers and they were doing too much: a
   figure has a face and a pose, so eight of them at any opacity read
   as a picture behind the page rather than as texture on it. Confetti
   and bursts carry the same idea - the room going off - without ever
   asking to be looked at.

   Everything is drawn in the page's own line weight and colour, held
   at a low opacity behind a gradient, and each piece runs on its own
   duration, delay, spin and drift so the field never pulses together.
   ============================================================ */

.celebrate {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  filter: blur(.4px);
  mask-image: radial-gradient(120% 90% at 50% 60%, #000 20%, transparent 78%);
  -webkit-mask-image: radial-gradient(120% 90% at 50% 60%, #000 20%, transparent 78%);
}

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

.bit, .burst {
  fill: var(--green, #16ac1f);
  stroke: none;
  /* --x and --y are read as percentages of the 1000 x 600 box, so a
     piece can be placed in the markup without knowing the units. */
  transform: translate(calc(var(--x) * 10px), calc(var(--y) * 6px))
             scale(var(--s));
}

/* The chips travel; the inner g does the turning, so the two do not
   have to share one transform. */
.bit > g {
  animation: chip-turn var(--dur) var(--d) infinite linear;
}

.bit {
  opacity: .13;
  animation: chip-rise var(--dur) var(--d) infinite linear;
}

.b-ribbon, .b-spark {
  fill: none;
  stroke: var(--green, #16ac1f);
  stroke-width: 1.6;
  stroke-linecap: round;
}

/* The bursts do not travel. They flash: open fast, hold for nothing,
   fade out, and stay gone for most of the cycle, which is what keeps
   four of them from reading as a pattern. */
.burst {
  fill: none;
  stroke: var(--green, #16ac1f);
  stroke-width: 2.2;
  stroke-linecap: round;
  opacity: 0;
}

.burst path {
  animation: burst-open 5.5s var(--d) infinite cubic-bezier(.16, 1, .3, 1);
}

@keyframes chip-rise {
  from { translate: 0 40px; opacity: 0; }
  12%  { opacity: .13; }
  80%  { opacity: .13; }
  to   { translate: calc(var(--drift) * 1px) -190px; opacity: 0; }
}

@keyframes chip-turn {
  to { rotate: var(--spin); }
}

@keyframes burst-open {
  0%   { opacity: 0; scale: .2; }
  6%   { opacity: .34; scale: 1.15; }
  14%  { opacity: .16; scale: 1; }
  32%  { opacity: 0; scale: 1.3; }
  100% { opacity: 0; scale: .2; }
}

@media (prefers-reduced-motion: reduce) {
  .bit, .bit > g, .burst path { animation: none; }
  .burst { opacity: .14; }
}

/* ============================================================
   The asterisk on the footer mark.

   The hero lockup does not draw one: that gap is #slot-a, and the
   flying star parks in it. Down here the star is long gone, three
   screens up and living on the ticket button, so the footer mark was
   the only place the lockup appeared without its asterisk.

   Same art and same coordinates as the slot, expressed as a share of
   the lockup box so it holds at any width: 78/477 of the mark for the
   size, and the 105.2% / 45.7% the slot already uses for the centre.
   ============================================================ */

.lockup .lstar {
  left: 105.2%;
  top: 45.7%;
  width: 16.352%;
  aspect-ratio: 1;
  translate: -50% -50%;
  -webkit-mask-image: url(assets/star-mask.png);
          mask-image: url(assets/star-mask.png);
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-source-type: alpha;
          mask-mode: alpha;
}

/* No entrance of its own any more: the flying asterisk finishes its
   trip here, so this one is only the no-script fallback and an
   animation on it would fight --ghost-foot for the opacity. */

/* The lockup wipe hides every i in the mark until its own animation
   runs (.lockup i is clipped to nothing by default). The asterisk is
   not part of that two-band wipe, so it has to opt out of the clip or
   it never draws at all. */
.lockup .lstar { clip-path: none; }

/* The hero lockup draws no asterisk of its own. The gap is #slot-a, the
   flying star launches out of it, and the mark is meant to be left with
   the gap open once it has gone. */

/* The footer asterisk is drawn so the mark is whole with no script. When
   star.js is running the flying asterisk lands here instead, and the
   drawn one steps aside. */
.closing .lockup .lstar { opacity: var(--ghost-foot, 1); }

/* ============================================================
   A black scrim over Aniko.

   Done on the plate rather than as a layer over it: she is a real
   matte now, so a black box laid on top would darken the rectangle
   she is cropped from, not her. Brightness follows the alpha exactly,
   and the small contrast lift keeps the highlights on her face from
   going to mud as it comes down.

   --plate-dark is the whole control: 1 is the plate as shot, lower is
   further under.
   ============================================================ */

.aniko {
  --plate-dark: .62;
}

.aniko.keyed img {
  filter: brightness(var(--plate-dark)) contrast(1.08);
}

/* Before the key lands the plate is still carrying the luminance mask
   and its own brightness compensation, so the scrim multiplies into
   that rather than replacing it. */
.aniko img {
  filter: brightness(calc(1.95 * var(--plate-dark))) contrast(1.05);
}

/* ============================================================
   The buy button breathes.

   It is the one thing on the page anyone is meant to press, and on a
   screen where everything else is either type or a slow drift it was
   the quietest element there. The pulse is on the ring rather than the
   label: an outline that swells and fades reads as the button asking,
   where a scaling label just reads as motion.
   ============================================================ */

.cta {
  animation: cta-pulse 2.6s ease-in-out infinite;
}

/* The hover already owns transform and outline-offset, so the pulse
   takes box-shadow and the outline colour and leaves those alone. */
@keyframes cta-pulse {
  0%, 100% {
    outline-color: rgba(22, 172, 31, .45);
    box-shadow: 0 0 0 0 rgba(22, 172, 31, .28);
  }
  45% {
    outline-color: rgba(22, 172, 31, .95);
    box-shadow: 0 0 0 10px rgba(22, 172, 31, 0);
  }
  46% {
    box-shadow: 0 0 0 0 rgba(22, 172, 31, 0);
  }
}

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

/* The gate draws the mark whole, asterisk and all. Nothing is flying
   yet - the star does not leave the lockup until the page is being
   scrolled - so this is the one place the lockup can be shown complete
   without two asterisks in the same gap. It lands after the wordmark
   has wiped on, and it is already turning, which is what the mark does
   for the rest of the page. */
.gate .lockup .lstar {
  animation: pop .7s cubic-bezier(.2, 1.5, .4, 1) .9s both,
             roll 9s linear .9s infinite;
}

@keyframes roll {
  from { rotate: 0deg; }
  to   { rotate: -360deg; }
}

@media (prefers-reduced-motion: reduce) {
  .gate .lockup .lstar { animation: none; }
}

/* ============================================================
   The tickets screen flickers.

   Everything on it is green type on black behind a spectrum meter, so
   it already reads as something lit rather than printed. A tube that
   stutters is what sells that: the flicker is mostly nothing, with a
   couple of fast dropouts per cycle and one deeper one, so it catches
   the eye without ever making the price list hard to read.

   Two shapes, and every element gets its own duration and delay, so
   the screen never blinks all at once - that would read as a page
   glitch rather than as the room.
   ============================================================ */

@keyframes flicker {
  0%, 41%, 43%, 62%, 64.5%, 100% { opacity: 1; }
  42%   { opacity: .45; }
  63%   { opacity: .68; }
  63.6% { opacity: .3; }
}

@keyframes flicker-hard {
  0%, 17%, 19.5%, 21%, 68%, 70%, 100% { opacity: 1; }
  18%   { opacity: .25; }
  20%   { opacity: .6; }
  69%   { opacity: .12; }
  69.4% { opacity: .8; }
}

/* All of it hangs off .in. The reveal on this screen is a transition on
   opacity, and a keyframe that touches opacity outranks a transition:
   unscoped, the flicker would pin every line visible before the screen
   had arrived and there would be no entrance left. */
.tickets.in .kicker { animation: flicker 6.1s 1.6s infinite steps(1, end); }
.tickets.in .when   { animation: flicker-hard 7.3s 1.2s infinite steps(1, end); }
.tickets.in .where  { animation: flicker 5.4s 2.6s infinite steps(1, end); }

/* The countdown flickers per unit, so the four digits fall out of step
   with each other the way separate tubes would. */
.tickets.in .count .unit { animation: flicker 4.8s 1.5s infinite steps(1, end); }
.tickets.in .count .unit:nth-child(2) { animation-duration: 6.6s; animation-delay: .9s; }
.tickets.in .count .unit:nth-child(3) { animation-duration: 5.7s; animation-delay: 2.1s; }
.tickets.in .count .unit:nth-child(4) { animation-duration: 7.9s; animation-delay: 1.4s; }

.tickets.in .tiers li { animation: flicker 8.2s 1.8s infinite steps(1, end); }
.tickets.in .tiers li:nth-child(2) { animation-name: flicker-hard; animation-duration: 6.9s; animation-delay: 1.7s; }
.tickets.in .tiers li:nth-child(3) { animation-duration: 7.4s; animation-delay: 3.3s; }

/* The button keeps its pulse and takes the flicker on top: the pulse
   owns the ring, the flicker owns the opacity, so neither is dropped. */
.tickets.in .cta {
  animation: cta-pulse 2.6s ease-in-out infinite,
             flicker 9.5s 3.1s infinite steps(1, end);
}

@media (prefers-reduced-motion: reduce) {
  .tickets.in .kicker, .tickets.in .when, .tickets.in .where,
  .tickets.in .count .unit, .tickets.in .tiers li,
  .tickets.in .cta, .cta { animation: none; }
}

/* sponsors.png is recut: the two top rows were a dark rule that came
   off the flyer with the marks, and they are gone from the file, so
   nothing here has to clip them. 690x71 now, not 690x73. */

/* ============================================================
   The second portrait, down the left of the tickets screen.

   Same shoot and the same green screen as the hero plate, so it is
   keyed by the same routine and lands with a real matte. It is
   background here rather than a picture: held low, pushed under a
   black overlay, and faded out on its inside edge so the price list
   never has to sit on an edge.
   ============================================================ */

.decks {
  position: absolute;
  inset: auto auto 0 0;
  /* Above the spectrum canvas, which sits at 0: the meter is the room
     behind her, not something painted across her. Still under the
     scribbles and well under the ticket stack. */
  z-index: 1;
  width: clamp(230px, 38vw, 560px);
  height: min(86%, 760px);
  margin: 0;
  pointer-events: none;
}

.decks img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: left bottom;
  display: block;

  /* --decks-dark is the black overlay, and it is done on the plate
     rather than as a layer over it: she is a matte, so a black box
     would darken the box she sits in, not her. */
  filter: brightness(var(--decks-dark, .3)) contrast(1.05);

  /* Until the key lands the file is still a green rectangle, so it
     stays hidden rather than flashing a green panel onto the screen. */
  opacity: 0;
  transition: opacity .8s ease;

  /* Soft on every side. One gradient per edge, intersected, so she has
     no border anywhere: she comes out of the left of the screen, thins
     out towards the type, and dissolves into the floor rather than
     ending on a crop. */
  -webkit-mask-image:
    linear-gradient(to right, #000 38%, transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 12%, #000 82%, transparent 100%),
    linear-gradient(to left, #000 88%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to right, #000 38%, transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 12%, #000 82%, transparent 100%),
    linear-gradient(to left, #000 88%, transparent 100%);
  mask-composite: intersect;
}

.decks.keyed img { opacity: 1; }

/* On a phone the price list runs the full width, so she comes down and
   goes further under. */
@media (max-width: 700px) {
  .decks {
    width: 74%;
    height: 54%;
  }
  .decks img { filter: brightness(.2) contrast(1.05); }
}

/* She grows out of the corner she is standing in, not out of her own
   middle: scaling from the centre would walk her off the left edge and
   out from under the mask on every kick. */
.decks img { transform-origin: left bottom; }
