/* MTB Frame — Motion toolkit: hover effects, animated hover borders,
   continuous motion, moving gradient backgrounds. Emitted as classes by
   includes/common-attrs.php. Loaded on the frontend AND the builder iframe so
   effects preview live. All continuous/large motion respects reduced-motion. */

/* ── Hover motion ──────────────────────────────────────────────── */
.frame-hover-lift { transition: transform .3s cubic-bezier(.22,.61,.36,1), box-shadow .3s ease; }
.frame-hover-lift:hover { transform: translateY(-6px); box-shadow: 0 18px 44px rgba(0,0,0,.28); }
.frame-hover-grow { transition: transform .3s cubic-bezier(.22,.61,.36,1); }
.frame-hover-grow:hover { transform: scale(1.04); }
.frame-hover-sink { transition: transform .18s ease; }
.frame-hover-sink:hover { transform: translateY(2px) scale(.99); }
.frame-hover-tilt { transition: transform .3s ease; transform-style: preserve-3d; }
.frame-hover-tilt:hover { transform: perspective(800px) rotateX(4deg) rotateY(-4deg); }
.frame-hover-glow { transition: box-shadow .3s ease; }
.frame-hover-glow:hover { box-shadow: 0 0 0 1px var(--frame-accent,#FF5B04), 0 0 26px color-mix(in srgb, var(--frame-accent,#FF5B04) 50%, transparent); }

/* ── Animated hover borders ────────────────────────────────────── */
.frame-hb-glow, .frame-hb-sweep, .frame-hb-pulse { position: relative; }
.frame-hb-glow::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  border: 2px solid transparent; transition: border-color .3s ease, box-shadow .3s ease;
}
.frame-hb-glow:hover::after { border-color: var(--frame-accent,#FF5B04); box-shadow: 0 0 18px color-mix(in srgb, var(--frame-accent,#FF5B04) 60%, transparent); }

/* Spinning gradient ring revealed on hover (the "animated hover border"). */
.frame-hb-sweep::before {
  content: ''; position: absolute; inset: -2px; border-radius: inherit; z-index: -1; opacity: 0;
  background: conic-gradient(from 0deg, var(--frame-accent,#FF5B04), #8b5cf6, #22d3ee, var(--frame-accent,#FF5B04));
  transition: opacity .3s ease;
}
.frame-hb-sweep:hover::before { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .frame-hb-sweep:hover::before { animation: frame-spin 3s linear infinite; }
}

.frame-hb-pulse::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  border: 2px solid var(--frame-accent,#FF5B04); opacity: 0; transition: opacity .3s ease;
}
@media (prefers-reduced-motion: no-preference) {
  .frame-hb-pulse:hover::after { opacity: 1; animation: frame-borderpulse 1.4s ease-in-out infinite; }
}
.frame-hb-pulse:hover::after { opacity: 1; }

/* ── Continuous motion ─────────────────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  .frame-loop-float     { animation: frame-float 4s ease-in-out infinite; }
  .frame-loop-bob       { animation: frame-bob 3.4s ease-in-out infinite; }
  .frame-loop-pulse     { animation: frame-pulse 2.4s ease-in-out infinite; }
  .frame-loop-spin-slow { animation: frame-spin 16s linear infinite; }
  .frame-loop-shimmer   { position: relative; overflow: hidden; }
  .frame-loop-shimmer::after {
    content: ''; position: absolute; inset: 0; pointer-events: none; transform: translateX(-120%);
    background: linear-gradient(110deg, transparent 30%, rgba(255,255,255,.18) 50%, transparent 70%);
    animation: frame-shimmer 2.8s ease-in-out infinite;
  }
}

/* ── Moving gradient background ────────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  .frame-bg-grad--anim { animation: frame-grad-move var(--frame-grad-dur, 12s) ease infinite; }
}

@keyframes frame-spin       { to { transform: rotate(360deg); } }
@keyframes frame-float      { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
@keyframes frame-bob        { 0%,100% { transform: translateY(0) rotate(0); } 50% { transform: translateY(-7px) rotate(1.4deg); } }
@keyframes frame-pulse      { 0%,100% { opacity: 1; } 50% { opacity: .58; } }
@keyframes frame-shimmer    { to { transform: translateX(120%); } }
@keyframes frame-borderpulse{ 0%,100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--frame-accent,#FF5B04) 60%, transparent); } 50% { box-shadow: 0 0 0 5px transparent; } }
@keyframes frame-grad-move  { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }

/* ── Revolve: tri-colour shadow that orbits the box on hover ─────
   Ported from honeywreck.com's .revolving-box, but palette-driven:
   the three colours come from the active palette, so the same effect
   reads as ember on mountainbaker and yellow/green/orange on
   honeywreck. Nothing here is site-specific. */
.frame-hover-revolve {
  transition: box-shadow .2s ease;
  position: relative;
}
.frame-hover-revolve:hover {
  box-shadow:
    0 8px 24px var(--frame-accent, #FF5B04),
    8px 0 24px var(--frame-glow, #00E5FF),
    0 -8px 24px var(--frame-accent-alt, #FACC15);
}
@media (prefers-reduced-motion: no-preference) {
  .frame-hover-revolve:hover { animation: frame-revolve 2.2s cubic-bezier(.77,0,.18,1) infinite; }
}
@keyframes frame-revolve {
  0%   { box-shadow: 0 8px 24px var(--frame-accent,#FF5B04), 8px 0 24px var(--frame-glow,#00E5FF), 0 -8px 24px var(--frame-accent-alt,#FACC15); }
  18%  { box-shadow: 7px -5px 24px var(--frame-accent,#FF5B04), -5px -7px 24px var(--frame-glow,#00E5FF), -7px 5px 24px var(--frame-accent-alt,#FACC15); }
  36%  { box-shadow: 15px -3px 32px var(--frame-accent-alt,#FACC15), -12px 12px 32px var(--frame-accent,#FF5B04), 2px 14px 32px var(--frame-glow,#00E5FF); }
  58%  { box-shadow: -7px -5px 24px var(--frame-accent,#FF5B04), -5px 7px 24px var(--frame-glow,#00E5FF), 7px 5px 24px var(--frame-accent-alt,#FACC15); }
  76%  { box-shadow: 0 -8px 24px var(--frame-accent,#FF5B04), -8px 0 24px var(--frame-glow,#00E5FF), 0 8px 24px var(--frame-accent-alt,#FACC15); }
  90%  { box-shadow: -15px 8px 32px var(--frame-glow,#00E5FF), 12px -10px 32px var(--frame-accent-alt,#FACC15), -8px -14px 32px var(--frame-accent,#FF5B04); }
  100% { box-shadow: 0 8px 24px var(--frame-accent,#FF5B04), 8px 0 24px var(--frame-glow,#00E5FF), 0 -8px 24px var(--frame-accent-alt,#FACC15); }
}

/* ── Glitch: the original, verbatim ──────────────────────────────
   honeywreck's .glitch: BOTH clones are the same colour as the text at
   opacity .8, sitting BEHIND it (z-index:-1), offset -2/+2px vertically
   and +2/-2px horizontally, sliced by clip-path. My first two attempts
   coloured the clones from the palette and dropped the offsets — that is
   an RGB-split, a different effect. This is the source rule.
   Colour is overridable via --frame-glitch-color for sites that do want
   a coloured split; unset, it inherits the text colour. */
.frame-loop-glitch { position: relative; display: inline-block; }
.frame-loop-glitch::before,
.frame-loop-glitch::after {
  content: attr(data-text);
  position: absolute;
  left: 0;
  width: 100%;
  pointer-events: none;
  opacity: 0.8;
  z-index: -1;
  color: var(--frame-glitch-color, currentColor);
}
.frame-loop-glitch::before { top: calc(-1 * var(--frame-glitch-offset, 2px)); left: var(--frame-glitch-offset, 2px); }
.frame-loop-glitch::after  { top: var(--frame-glitch-offset, 2px);  left: calc(-1 * var(--frame-glitch-offset, 2px)); }

@media (prefers-reduced-motion: no-preference) {
  .frame-loop-glitch::before { animation: frame-glitch-slice-1 var(--frame-glitch-dur, 8s) infinite var(--frame-glitch-delay, 0s); }
  .frame-loop-glitch::after  { animation: frame-glitch-slice-2 var(--frame-glitch-dur, 8s) infinite var(--frame-glitch-delay, 0s); }
}
/* The original's 4/6/8/10/12/14/16/18s ladder, one per heading. */
.frame-loop-glitch:nth-of-type(8n+1){--frame-glitch-dur:4s; --frame-glitch-delay:0s;}
.frame-loop-glitch:nth-of-type(8n+2){--frame-glitch-dur:6s; --frame-glitch-delay:1.5s;}
.frame-loop-glitch:nth-of-type(8n+3){--frame-glitch-dur:8s; --frame-glitch-delay:3s;}
.frame-loop-glitch:nth-of-type(8n+4){--frame-glitch-dur:10s;--frame-glitch-delay:5s;}
.frame-loop-glitch:nth-of-type(8n+5){--frame-glitch-dur:12s;--frame-glitch-delay:7s;}
.frame-loop-glitch:nth-of-type(8n+6){--frame-glitch-dur:14s;--frame-glitch-delay:9s;}
.frame-loop-glitch:nth-of-type(8n+7){--frame-glitch-dur:16s;--frame-glitch-delay:12s;}
.frame-loop-glitch:nth-of-type(8n+8){--frame-glitch-dur:18s;--frame-glitch-delay:14s;}

@keyframes frame-glitch-slice-1 {
  0%,8%,100% { opacity:0; clip-path:inset(0 0 0 0); transform:none; }
  10%,13% { opacity:1; clip-path:inset(0 0 60% 0); transform:translate(-2px,2px) skew(-.5deg); }
  14%,17% { opacity:0; }
  20%,23% { opacity:1; clip-path:inset(0 0 20% 0); transform:translate(2px,-1px) skew(.5deg); }
  24%,27% { opacity:0; }
  30%,33% { opacity:1; clip-path:inset(0 0 40% 0); transform:translate(-1px,1px) skew(-.5deg); }
  34%,100% { opacity:0; clip-path:inset(0 0 0 0); transform:none; }
}
@keyframes frame-glitch-slice-2 {
  0%,8%,100% { opacity:0; clip-path:inset(0 0 0 0); transform:none; }
  12%,15% { opacity:1; clip-path:inset(80% 0 0 0); transform:translate(1px,-1px) skew(-.5deg); }
  16%,19% { opacity:0; }
  22%,25% { opacity:1; clip-path:inset(40% 0 0 0); transform:translate(-1px,1px) skew(.5deg); }
  26%,29% { opacity:0; }
  32%,35% { opacity:1; clip-path:inset(60% 0 0 0); transform:translate(2px,-2px) skew(-.5deg); }
  36%,100% { opacity:0; clip-path:inset(0 0 0 0); transform:none; }
}
}

/* ── frame/show — three states, three layouts ────────────────────────
   The tonight takeover is not the card with a badge: the show leaves the
   list and becomes a full-bleed band under the masthead. */
.frame-show__tonight { display: none; }
.frame-show--tonight .frame-show__tonight { display: block; text-align: center; padding: clamp(20px, 3vw, 40px) 16px clamp(24px, 3.5vw, 48px); }
.frame-show--tonight .frame-show__card { display: none; }

.frame-show__word {
  /* Divi (and most themes) set `p { padding-bottom: 1em }`. At a 243px display
     size that is 243px of dead space under the word — the gap between TONIGHT
     and the venue line was one inherited paragraph rule, not layout. */
  padding: 0 !important;
  font-family: var(--frame-type-display, sans-serif);
  color: var(--frame-signal, #E02B20);
  /* fills the band at any width — the one piece of type that must not wrap */
  font-size: clamp(3rem, 17vw, 16rem);
  line-height: .84;
  margin: 0 0 .18em;
  letter-spacing: -.02em;
  white-space: nowrap;
}
.frame-show__where { font-size: clamp(.95rem, 1.6vw, 1.25rem); color: var(--frame-fg1,#fff); margin: 0 0 .35em; padding: 0 !important; }
.frame-show__times { font-size: .95rem; color: var(--frame-fg3, rgba(255,255,255,.68)); margin: 0 0 1.1em; padding: 0 !important; }
.frame-show__eta   { padding: 0 !important; font-family: var(--frame-type-mono, monospace); font-size: .8rem;
                     letter-spacing: .08em; color: var(--frame-fg3, rgba(255,255,255,.68)); margin: .9em 0 0; }

.frame-show--tonight .frame-show__cta {
  display: inline-block; background: var(--frame-signal, #E02B20); color: #fff;
  font-family: var(--frame-type-display, sans-serif); font-size: 1.05rem;
  padding: 14px 34px; border-radius: 3px; text-decoration: none;
  transition: transform .12s ease, box-shadow .12s ease;
}
.frame-show--tonight .frame-show__cta:hover { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(224,43,32,.45); }

/* While a show is tonight it owns the page's only glitch. The show block is
   nested inside a section, so a sibling selector never reaches the other
   headings — scope from the body, which :has() can see. */
body:has(.frame-show--tonight) .frame-loop-glitch:not(.frame-show__word)::before,
body:has(.frame-show--tonight) .frame-loop-glitch:not(.frame-show__word)::after {
  animation: none !important;
  opacity: 0 !important;
}

/* upcoming / past card */
.frame-show__card { display: grid; grid-template-columns: 100px minmax(0,1fr); gap: 12px; align-items: start; }
.frame-show__poster img { display: block; width: 100px; height: 100px; object-fit: cover; }
.frame-show__title { font-family: var(--frame-type-display, sans-serif); font-size: 19px; line-height: 1.3em;
                     color: rgba(255,255,255,.87); margin: 0 0 .5em; }
.frame-show__title a { color: inherit; text-decoration: none; }
.frame-show__meta  { font-size: 15px; line-height: 1.3em; color: var(--frame-fg3, rgba(255,255,255,.68)); margin: 0 0 .5em; }
.frame-show__card .frame-show__cta { color: #2EA3F2; font-size: 15px; text-decoration: none; }

/* past: the archive, visibly cooled off */
.frame-show--past .frame-show__card { filter: saturate(.25) brightness(.8); }
@media (max-width: 600px) {
  .frame-show--tonight .frame-show__cta { position: sticky; bottom: 12px; }
}

/* On show day the show leaves the list: the band runs edge to edge and the
   "Upcoming Shows" label steps aside — there is nothing upcoming about it. */
.frame-show--tonight { width: 100vw; margin-left: calc(50% - 50vw); position: relative; }
/* :has() is doing real work here — the band has to escape a 560px column and
   silence the "Upcoming Shows" label above it. Both are stated as fallbacks
   too, so an engine without :has() still gets a readable band. */
.frame-section__inner:has(.frame-show--tonight) { max-width: none !important; padding-left: 0 !important; padding-right: 0 !important; }
.frame-section__inner:has(.frame-show--tonight) > .frame-w > .frame-heading,
.frame-section__inner:has(.frame-show--tonight) > .frame-heading { display: none; }

/* ── frame/video-carousel ────────────────────────────────────────── */
.frame-vc__viewport { overflow: hidden; border-radius: var(--frame-vc-radius, 10px); }
.frame-vc__track { display: flex; transition: transform .38s cubic-bezier(.22,.61,.36,1); }
.frame-vc__slide { position: relative; flex: 0 0 100%; aspect-ratio: var(--frame-vc-aspect, 16/9); }
.frame-vc__slide > iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.frame-vc__poster { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; border: 0;
  background: #000; cursor: pointer; display: block; z-index: 2; }
.frame-vc__poster[hidden] { display: none !important; }
.frame-vc__slide.is-playing .frame-vc__poster,
.frame-vc__slide.is-playing .frame-vc__badge { display: none !important; }
.frame-vc__slide > iframe { z-index: 1; }
.frame-vc__poster img { width: 100%; height: 100%; object-fit: cover; display: block; opacity: .82;
  transition: opacity .18s ease, transform .3s ease; }
.frame-vc__poster:hover img { opacity: 1; transform: scale(1.02); }
.frame-vc__play { position: absolute; left: 50%; top: 50%; width: 72px; height: 50px; margin: -25px 0 0 -36px;
  border-radius: 12px; background: var(--frame-signal, #E02B20); }
.frame-vc__play::after { content: ''; position: absolute; left: 28px; top: 15px;
  border: 10px solid transparent; border-left: 16px solid #fff; border-right: 0; }
.frame-vc__poster:focus-visible { outline: 3px solid var(--frame-accent-alt, #ffe600); outline-offset: -3px; }
.frame-vc__title { margin: 8px 0 0; font-size: 15px; color: var(--frame-fg3, rgba(255,255,255,.68)); }
.frame-vc__ui { display: flex; align-items: center; justify-content: center; gap: 14px; margin-top: 10px; }
.frame-vc__arrow { background: transparent; border: 0; color: var(--frame-fg2, rgba(255,255,255,.87));
  font-size: 28px; line-height: 1; padding: 2px 10px; cursor: pointer; border-radius: 6px; }
.frame-vc__arrow:hover { color: var(--frame-signal, #E02B20); }
.frame-vc__dots { display: inline-flex; gap: 8px; }
.frame-vc__dot { width: 8px; height: 8px; padding: 0; border: 0; border-radius: 999px; cursor: pointer;
  background: rgba(255,255,255,.28); }
.frame-vc__dot.is-on { background: var(--frame-signal, #E02B20); }
.frame-vc__arrow:focus-visible, .frame-vc__dot:focus-visible {
  outline: 2px solid var(--frame-accent-alt, #ffe600); outline-offset: 2px; }

/* ── frame/music-links ──────────────────────────────────────────────
   The grid is the base state and stays the base state: collapse only
   happens once JS has read a stored preference, so a cached page or a
   JS-off visitor sees every service. */
.frame-ml__grid { display: inline-flex; align-items: center; flex-wrap: wrap; }
.frame-ml__mark { opacity: .9; }
.frame-ml__mark:hover { opacity: 1; transform: translateY(-1px); }
.frame-ml__mark[aria-current="true"] { opacity: 1; }
.frame-ml__mark:focus-visible { outline: 2px solid var(--frame-accent-alt, #ffe600); outline-offset: 3px; border-radius: 6px; }
.frame-ml__heading { font-family: var(--frame-type-mono, monospace); font-size: .72rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--frame-fg3, rgba(255,255,255,.68)); margin: 0 0 .7em; }
.frame-ml__note { font-family: var(--frame-type-mono, monospace); font-size: .72rem; letter-spacing: .08em;
  color: var(--frame-fg3, rgba(255,255,255,.68)); margin: .6em 0 0; }
.frame-ml__controls { display: block; }
.frame-ml__controls:empty { display: none; }
.frame-ml__more, .frame-ml__change {
  background: none; border: 0; padding: 0; margin: .7em .9em 0 0; cursor: pointer;
  font-family: var(--frame-type-mono, monospace); font-size: .72rem; letter-spacing: .08em;
  color: var(--frame-fg3, rgba(255,255,255,.68));
}
.frame-ml__more:hover, .frame-ml__change:hover { color: var(--frame-signal, #E02B20); }
.frame-ml__more:focus-visible, .frame-ml__change:focus-visible {
  outline: 2px solid var(--frame-accent-alt, #ffe600); outline-offset: 2px; }

/* ── show: the poster in the tonight band ────────────────────────────
   Behind mode is the default — the flyer becomes the field the word sits
   on, heavily scrimmed so TONIGHT keeps its contrast. The poster is the
   artwork the band already made; the band should not ignore it. */
.frame-show__tonight { position: relative; }
.frame-show__tonight--poster-behind::before {
  content: ''; position: absolute; inset: 0; z-index: 0;
  background-image: var(--frame-show-poster); background-size: cover; background-position: center;
  opacity: .28; filter: saturate(.7) contrast(1.1);
}
.frame-show__tonight--poster-behind::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(ellipse at 50% 45%, rgba(0,0,0,.62) 0%, rgba(0,0,0,.86) 60%, #000 100%);
}
.frame-show__tonight > * { position: relative; z-index: 2; }

.frame-show--tonight .frame-show__tonight--poster-beside {
  display: grid; grid-template-columns: 200px minmax(0,1fr); gap: 28px;
  align-items: center; text-align: center; max-width: 1100px; margin: 0 auto;
  justify-items: center;
}
/* Every text node is a direct child, so without explicit placement they
   alternate across the two columns. Poster owns column 1 for the full
   height; everything else stacks in column 2. */
.frame-show--tonight .frame-show__tonight--poster-beside > .frame-show__poster-beside {
  grid-column: 1; grid-row: 1 / -1; align-self: center;
}
.frame-show--tonight .frame-show__tonight--poster-beside > :not(.frame-show__poster-beside) {
  grid-column: 2;
  justify-self: center;
  margin-bottom: .5em;
}
/* The details were drifting to the bottom of a tall poster column; pull the
   whole text stack together and centre it against the poster. */
.frame-show--tonight .frame-show__tonight--poster-beside { align-content: center; row-gap: 2px; }
.frame-show--tonight .frame-show__tonight--poster-beside .frame-show__word { margin-bottom: .12em; }
.frame-show--tonight .frame-show__tonight--poster-beside .frame-show__times { margin-bottom: .8em; }
.frame-show__poster-beside img {
  display: block; width: 100%; height: auto; border-radius: 8px;
  box-shadow: 0 18px 48px rgba(0,0,0,.6);
}
/* Phones: the word leads, the poster sits under it, everything centred.
   DOM order puts the poster first (it is the link target on desktop), so it
   is re-ordered rather than re-authored. */
@media (max-width: 700px) {
  .frame-show--tonight .frame-show__tonight--poster-beside {
    grid-template-columns: 1fr; text-align: center; gap: 0;
  }
  .frame-show--tonight .frame-show__tonight--poster-beside > .frame-show__poster-beside {
    grid-column: 1; grid-row: auto; order: 5; max-width: 240px; margin: 18px auto 0;
  }
  .frame-show--tonight .frame-show__tonight--poster-beside > :not(.frame-show__poster-beside) {
    grid-column: 1; justify-self: center;
  }
  .frame-show--tonight .frame-show__tonight--poster-beside > .frame-show__word  { order: 1; }
  .frame-show--tonight .frame-show__tonight--poster-beside > .frame-show__where { order: 2; }
  .frame-show--tonight .frame-show__tonight--poster-beside > .frame-show__times { order: 3; }
  .frame-show--tonight .frame-show__tonight--poster-beside > .frame-show__cta   { order: 4; }
  .frame-show--tonight .frame-show__tonight--poster-beside > .frame-show__eta   { order: 6; }
}

/* A heading that exists for structure, not for the eye. */
.frame-sr-only {
  position: absolute !important; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ── Release colour ──────────────────────────────────────────────────
   Carter: the record carries the colour, not a fixed brand mark. Zivjeli
   is purple, Lost Satellite is the fireworks, the first single was red.
   So: the SHOW owns signal red, and MUSIC owns violet. A section can
   claim a release colour with data-release="violet". */
[data-release="violet"] .frame-heading,
#music .frame-heading { color: var(--frame-fg1, #fff); }

#music .frame-loop-glitch::before { color: var(--frame-violet-lit, #9A6BFF); }
#music .frame-loop-glitch::after  { color: var(--frame-violet, #6C2BD9); }

/* the release card leans violet on hover instead of the generic tri-colour */
#music .frame-hover-revolve:hover {
  box-shadow: 0 8px 28px var(--frame-violet, #6C2BD9),
              6px 0 24px var(--frame-violet-lit, #9A6BFF),
              0 -8px 24px rgba(108,43,217,.55);
  animation: none;
}
#music .frame-vc__play { background: var(--frame-violet, #6C2BD9); }
#music .frame-vc__dot.is-on { background: var(--frame-violet-lit, #9A6BFF); }
#music .frame-vc__arrow:hover { color: var(--frame-violet-lit, #9A6BFF); }
#music .frame-ml__more:hover, #music .frame-ml__change:hover { color: var(--frame-violet-lit, #9A6BFF); }
#music .frame-button--naked { color: var(--frame-violet-lit, #9A6BFF) !important; }

/* The brand red as TEXT on ink measures 4.17:1 — under the bar, and worse
   (3.73:1) where the riso gif runs purple behind it. Fills keep #E02B20;
   text lifts to the same hue at a readable luminance. The #music rule above
   still wins inside the music block. */
.frame-button--naked { color: var(--frame-signal-lit, #FF6F62) !important; }


/* Festival laurels and the like — an overlay belongs on the player, not
   burned into the thumbnail, so it survives a new upload. */
.frame-vc__badge {
  position: absolute; width: var(--frame-vc-badge-size, 22%); height: auto;
  pointer-events: none; z-index: 3; filter: drop-shadow(0 2px 8px rgba(0,0,0,.6));
}
.frame-vc__viewport[data-badge-pos="top-left"]     .frame-vc__badge { top: 4%; left: 4%; }
.frame-vc__viewport[data-badge-pos="top-right"]    .frame-vc__badge { top: 4%; right: 4%; }
.frame-vc__viewport[data-badge-pos="bottom-left"]  .frame-vc__badge { bottom: 12%; left: 4%; }
.frame-vc__viewport[data-badge-pos="bottom-right"] .frame-vc__badge { bottom: 12%; right: 4%; }
.frame-vc__slide:has(iframe) .frame-vc__badge { display: none; }

/* ── frame/release ─────────────────────────────────────────────────── */
.frame-rel {
  display: grid; gap: 16px; align-items: start;
  background: rgba(0,0,0,.72); border-radius: 8px; padding: 15px;
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  transition: box-shadow .2s ease;
}
.frame-rel--art-left { grid-template-columns: 120px minmax(0,1fr); }
.frame-rel--stacked  { grid-template-columns: 1fr; }
.frame-rel:hover {
  box-shadow: 0 8px 28px var(--frame-rel-accent, var(--frame-accent, #ff7300)),
              6px 0 24px color-mix(in srgb, var(--frame-rel-accent, #6C2BD9) 70%, transparent);
}
.frame-rel__art img { display: block; width: 100%; height: auto; border-radius: 4px; }
.frame-form__submit { margin-top: 10px; }

.frame-rel { padding: clamp(20px, 2vw, 30px); }

.frame-rel__eyebrow {
  margin: 0 0 .35em; font-family: var(--frame-type-mono, monospace); font-size: .7rem;
  letter-spacing: .14em; text-transform: uppercase; color: var(--frame-rel-accent, var(--frame-violet-lit, #9A6BFF));
}
.frame-rel__title { margin: 0 0 .35em; font-family: var(--frame-type-display, sans-serif);
  font-size: 19px; line-height: 1.3; color: var(--frame-fg1, #fff); }
.frame-rel__blurb { margin: 0 0 .7em; font-size: 15px; line-height: 1.35;
  color: var(--frame-fg3, rgba(255,255,255,.68)); }

/* Tracks read as a playlist; picking one swaps the player in place. */
.frame-rel__tracks { list-style: none; margin: 4px 0 0; padding: 0;
  border-top: 1px solid var(--frame-border, rgba(255,255,255,.12)); }
.frame-rel__tracks li { border-bottom: 1px solid var(--frame-border, rgba(255,255,255,.08)); }
.frame-rel__tracks button { display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 9px 2px; background: none; border: 0; cursor: pointer; text-align: left;
  color: var(--frame-fg3, rgba(255,255,255,.68)); font-size: 15px; }
.frame-rel__tracks button:hover { color: var(--frame-fg1, #fff); }
.frame-rel__tracks button[aria-current="true"] { color: var(--frame-fg1, #fff); }
.frame-rel__tracks button[aria-current="true"] .frame-rel__tnum { color: var(--frame-rel-accent, #9A6BFF); }
.frame-rel__tnum { font-family: var(--frame-type-mono, monospace); font-size: .72rem;
  color: var(--frame-fg3, rgba(255,255,255,.4)); min-width: 1.4em; }
.frame-rel__ttitle { flex: 1 1 auto; }

/* Services sit at the foot of the block, white, quiet — they are a fact
   about the record, not the call to action. */
.frame-rel__services { grid-column: 1 / -1; margin-top: 4px;
  border-top: 1px solid var(--frame-border, rgba(255,255,255,.12)); padding-top: 12px; }
.frame-rel__svclabel { margin: 0 0 8px; font-family: var(--frame-type-mono, monospace);
  font-size: .66rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--frame-fg3, rgba(255,255,255,.5)); }
.frame-rel__list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 18px; }
.frame-rel__list a { font-size: 14px; text-decoration: none; color: var(--frame-fg1, #fff); opacity: .82; }
.frame-rel__list a:hover { opacity: 1; text-decoration: underline; }
.frame-rel__list a.is-mine { font-weight: 700; opacity: 1; }
.frame-rel__playerwrap {
  grid-column: 1 / -1;
  background: rgba(255,255,255,.04);
  border: 1px solid var(--frame-border, rgba(255,255,255,.12));
  border-radius: 8px; min-height: 80px;
}
.frame-rel__player { width: 100%; border: 0; border-radius: 8px; display: block; }
.frame-rel__player--video { position: relative; aspect-ratio: 16/9; border-radius: 8px; overflow: hidden; }
.frame-rel__player--video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

@media (max-width: 600px) {
  .frame-rel--art-left { grid-template-columns: 92px minmax(0,1fr); }
  /* This used to `display:none` the list and hand over to `.frame-rel__pick`
     — a control that was never built. The result on a phone was the words
     "AVAILABLE ON" over nothing at all. Carter asked for full-width service
     targets on mobile, so that is what this is: two per row, tappable. */
  .frame-rel__list { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .frame-rel__list a {
    display: block; padding: 11px 12px; text-align: center; font-size: 13px;
    border: 1px solid var(--frame-border, rgba(255,255,255,.18)); border-radius: 6px;
    opacity: 1;
  }
  .frame-rel__list a.is-mine { border-color: var(--frame-rel-accent, #9A6BFF); }
}

/* Playlist chrome — a row of dots does not tell you what is in the carousel. */
.frame-vc__now { display: flex; align-items: baseline; gap: 10px; margin: 10px 0 6px; }
.frame-vc__nowlabel { font-family: var(--frame-type-mono, monospace); font-size: .66rem;
  letter-spacing: .14em; text-transform: uppercase; color: var(--frame-fg3, rgba(255,255,255,.55)); }
.frame-vc__nowtitle { font-family: var(--frame-type-display, sans-serif); font-size: 15px;
  color: var(--frame-fg1, #fff); flex: 1 1 auto; }
.frame-vc__count { font-family: var(--frame-type-mono, monospace); font-size: .7rem;
  color: var(--frame-fg3, rgba(255,255,255,.55)); }
.frame-vc__playlist { list-style: none; margin: 0 0 8px; padding: 0;
  border-top: 1px solid var(--frame-border, rgba(255,255,255,.14)); }
.frame-vc__playlist li { border-bottom: 1px solid var(--frame-border, rgba(255,255,255,.10)); }
.frame-vc__playlist button {
  display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 4px;
  background: none; border: 0; cursor: pointer; text-align: left;
  color: var(--frame-fg3, rgba(255,255,255,.68)); font-size: 14px;
}
.frame-vc__playlist button:hover { color: var(--frame-fg1, #fff); }
.frame-vc__playlist button[aria-current="true"] { color: var(--frame-fg1, #fff); }
.frame-vc__playlist button[aria-current="true"] .frame-vc__num { color: var(--frame-violet-lit, #9A6BFF); }
.frame-vc__num { font-family: var(--frame-type-mono, monospace); font-size: .72rem;
  color: var(--frame-fg3, rgba(255,255,255,.4)); min-width: 1.4em; }
.frame-vc__pltitle { flex: 1 1 auto; }
.frame-vc__playlist button:focus-visible { outline: 2px solid var(--frame-accent-alt, #ffe600); outline-offset: -2px; }

/* Phones: the row becomes a full-width grid of targets — six 30px marks in a
   line is a row of things nobody can hit. */
@media (max-width: 600px) {
  .frame-ml__grid {
    display: grid !important;
    grid-template-columns: repeat(4, 1fr);
    gap: 10px !important;
    width: 100%;
  }
  .frame-ml__mark {
    width: 100% !important; height: 56px !important;
    background: rgba(255,255,255,.06); border-radius: 10px;
  }
  .frame-ml__mark img, .frame-ml__mark svg { width: 28px !important; height: 28px !important; }
}

/* ── frame/subscribe ────────────────────────────────────────────────── */
.frame-sub { max-width: 560px; }
.frame-sub__heading { margin: 0 0 .3em; font-family: var(--frame-type-display, sans-serif);
  font-size: 19px; color: var(--frame-fg1, #fff); }
.frame-sub__blurb { margin: 0 0 .8em; font-size: 15px; color: var(--frame-fg3, rgba(255,255,255,.68)); }
.frame-sub__form { display: flex; gap: 8px; flex-wrap: wrap; }
.frame-sub__srlabel { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.frame-sub__hp { position: absolute !important; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.frame-sub__input {
  flex: 1 1 220px; min-width: 0; padding: 12px 14px; font-size: 16px; border-radius: 6px;
  color: var(--frame-fg1, #fff); background: rgba(255,255,255,.07);
  border: 1px solid var(--frame-border, rgba(255,255,255,.18));
}
.frame-sub__input::placeholder { color: var(--frame-fg3, rgba(255,255,255,.45)); }
.frame-sub__input:focus { outline: none; border-color: var(--frame-sub-accent, var(--frame-signal, #E02B20)); }
.frame-button--solid, .frame-sub__btn, .woocommerce a.button, .woocommerce button.button {
  /* white-on-#E02B20 measures 4.41:1 — under the bar. Same hue, 14% deeper. */
  --frame-btn-bg: color-mix(in srgb, var(--frame-signal, #E02B20) 86%, #000);
}
.frame-sub__btn {
  padding: 12px 22px; border: 0; border-radius: 3px; cursor: pointer;
  background: color-mix(in srgb, var(--frame-sub-accent, var(--frame-signal, #E02B20)) 84%, #000);
  color: #fff;
  font-family: var(--frame-type-display, sans-serif); font-size: 15px;
}
.frame-sub__btn:hover { background: var(--frame-accent, #ff7300); }
.frame-sub__btn[disabled] { opacity: .6; cursor: default; }
.frame-sub__msg { margin: .7em 0 0; font-family: var(--frame-type-mono, monospace);
  font-size: .78rem; letter-spacing: .06em; color: var(--frame-fg3, rgba(255,255,255,.68)); }
.frame-sub.is-done .frame-sub__msg { color: var(--frame-sub-accent, var(--frame-accent-alt, #ffe600)); }
.frame-sub.is-error .frame-sub__msg { color: var(--frame-signal, #E02B20); }
@media (max-width: 600px) { .frame-sub__btn { flex: 1 1 100%; } }

/* ── Riso / halftone surface ─────────────────────────────────────────
   The band's Spotify artwork is a printed thing: a coarse dot screen, ink
   channels that do not quite register, colour that sits ON the surface
   rather than behind glass. These are the mechanisms, as reusable classes
   — a block claims one, nothing is baked into an image.

   The screen is a repeating radial-gradient, not an image: no request, no
   bytes, and it scales to any element. */
.frame-riso {
  position: relative;
  isolation: isolate;
}
.frame-riso::before {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background-image: radial-gradient(circle at 50% 50%, rgba(0,0,0,.55) 0.6px, transparent 0.8px);
  background-size: var(--frame-riso-dot, 3px) var(--frame-riso-dot, 3px);
  mix-blend-mode: multiply;
  opacity: var(--frame-riso-screen, .55);
}
/* Misregistration: the same content, offset a hair, in a second ink. Applied
   to text via a shadow pair so there is no duplicate node to keep in sync. */
.frame-ink-offset {
  text-shadow:
    calc(var(--frame-ink-shift, 2px) * -1) 0 0 var(--frame-ink-a, #ff7300),
    var(--frame-ink-shift, 2px) 0 0 var(--frame-ink-b, #39ff14);
}
/* Paper: a warm grain over the whole band, so black is never flat black. */
.frame-paper::after {
  content: ''; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  opacity: var(--frame-paper, .10); mix-blend-mode: overlay;
  background-image:
    repeating-linear-gradient(0deg, rgba(255,255,255,.7) 0 1px, transparent 1px 2px),
    repeating-linear-gradient(90deg, rgba(255,255,255,.5) 0 1px, transparent 1px 3px);
}

/* ── frame/shows — the tour list as a poster ─────────────────────────
   Date huge on the left, venue over city on the right, a hairline between.
   The flyer is not in the layout; it trails the cursor, so the list keeps
   its rhythm and the artwork still gets seen. */
.frame-shows { position: relative; }
.frame-shows__list { list-style: none; margin: 0; padding: 0;
  border-top: 1px solid var(--frame-border, rgba(255,255,255,.22)); }
.frame-shows__row { border-bottom: 1px solid var(--frame-border, rgba(255,255,255,.22)); }
.frame-shows__link {
  display: grid; grid-template-columns: auto minmax(160px, 240px);
  align-items: center; gap: 24px; padding: 8px 0; text-decoration: none; color: inherit;
}
.frame-shows__date {
  font-family: var(--frame-type-display, sans-serif);
  font-size: var(--frame-shows-size, clamp(2.4rem, 7vw, 5.4rem));
  line-height: 1.02; letter-spacing: -.02em;
  color: var(--frame-shows-date, var(--frame-accent-alt, #ffe600));
  display: flex; align-items: baseline; gap: .22em; white-space: nowrap;
}
.frame-shows__where {
  /* min-width:0 lets the column shrink; overflow-wrap keeps a long support
     line inside it instead of spilling back under the date. */
  min-width: 0; overflow-wrap: anywhere;
  display: flex; flex-direction: column; align-items: flex-end; text-align: right; gap: 2px;
  font-family: var(--frame-type-mono, monospace); text-transform: uppercase; letter-spacing: .12em;
}
.frame-shows__date { min-width: 0; }
.frame-shows__venue { font-size: .78rem; color: var(--frame-fg1, #fff); white-space: nowrap; }
.frame-shows__city  { font-size: .74rem; color: var(--frame-fg3, rgba(255,255,255,.6)); white-space: nowrap; }
.frame-shows__note  { font-size: .66rem; color: var(--frame-fg3, rgba(255,255,255,.42));
  line-height: 1.5; letter-spacing: .08em; }

/* Played shows cool off only when they sit NEXT TO a live one. An archive
   list is all past by definition — dimming every row there just makes the
   section look switched off. */
.frame-shows[data-mode="all"] .frame-shows__row.is-past .frame-shows__date {
  color: var(--frame-fg3, rgba(255,255,255,.34));
}
.frame-shows[data-mode="all"] .frame-shows__row.is-past .frame-shows__venue {
  color: var(--frame-fg3, rgba(255,255,255,.6));
}

a.frame-shows__link:hover .frame-shows__date { color: var(--frame-signal, #E02B20); }
a.frame-shows__link:focus-visible { outline: 2px solid var(--frame-accent-alt, #ffe600); outline-offset: 2px; }

.frame-shows__art {
  position: absolute; top: 0; left: 0; width: var(--frame-shows-art, 220px); height: auto;
  pointer-events: none; opacity: 0; z-index: 5; border-radius: 4px;
  box-shadow: 0 18px 48px rgba(0,0,0,.65);
  transition: opacity .18s ease;
  will-change: transform;
}
.frame-shows__art.is-on { opacity: 1; }

@media (max-width: 600px) {
  .frame-shows__link { grid-template-columns: 1fr; gap: 2px; padding: 10px 0; }
  .frame-shows__where { align-items: flex-start; text-align: left; }
  .frame-shows__date { font-size: clamp(2.1rem, 13vw, 3.4rem); }
}
@media (hover: none) { .frame-shows__art { display: none; } }

/* Homepage merch grid: two cards of different title length ran to different
   heights, so "Add to cart" and "Select options" sat on different baselines.
   The card is a column and the button is pushed to the floor. */
.frame-wc-product-grid ul.products li.product {
  display: flex; flex-direction: column; height: 100%;
}
.frame-wc-product-grid ul.products li.product .button { margin-top: auto; }
.frame-wc-product-grid ul.products { align-items: stretch; }

/* ── frame/instagram — the feed as a contact sheet ───────────────────
   Photos hung on a strip of film. The strip travels with the scroll, a
   frame develops from grey to colour under the cursor, and a click opens
   it full size. Everything here is CSS on real elements — no embed, no
   third-party script, and nothing that paints a white card on a black
   page the way Instagram's own widget does. */
.frame-ig { --frame-ig-h: clamp(104px, 13vw, 172px);
  --frame-ig-accent: var(--frame-signal-lit, #FF6F62); }
.frame-ig__head { padding: 0 2px; display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }
.frame-ig__title { margin: 0; font-family: var(--frame-type-display, sans-serif); font-size: clamp(1.4rem, 3vw, 2rem); color: var(--frame-fg1, #fff); }
.frame-ig__handle { font-family: var(--frame-type-mono, monospace); font-size: .72rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--frame-fg3, rgba(255,255,255,.6)); text-decoration: none; }
.frame-ig__handle:hover { color: var(--frame-ig-accent); }

.frame-ig__strips { display: flex; flex-direction: column; gap: 18px; }

/* The strip itself: a black band with a sprocket rail top and bottom. The
   holes are a repeating gradient, not 40 divs — this scrolls at 60fps on a
   phone and costs one paint. */
.frame-ig__strip { position: relative; overflow: hidden; background: #08080a;
  border-radius: 3px; padding: 0; cursor: grab; touch-action: pan-y; }
.frame-ig__strip.is-dragging { cursor: grabbing; }
.frame-ig--film .frame-ig__perf { display: block; height: 13px;
  background: repeating-linear-gradient(90deg,
    transparent 0 7px, rgba(255,255,255,.86) 7px 20px, transparent 20px 27px);
  -webkit-mask-image: linear-gradient(#000, #000); opacity: .72; }
.frame-ig--film .frame-ig__perf::after { content: ''; }
.frame-ig:not(.frame-ig--film) .frame-ig__perf { display: none; }
.frame-ig__edge { position: absolute; left: 0; bottom: 1px; padding: 0 6px;
  font-family: var(--frame-type-mono, monospace); font-size: 8px; letter-spacing: .22em;
  color: rgba(255,140,60,.55); white-space: nowrap; pointer-events: none; }

.frame-ig__track { display: flex; gap: 6px; padding: 6px 6px 15px;
  will-change: transform; transition: transform .12s linear; }
.frame-ig__strip.is-dragging .frame-ig__track { transition: none; }

/* A frame. Greyscale until you touch it, then it develops. */
.frame-ig__frame { position: relative; display: block; margin: 0; flex: 0 0 auto;
  height: var(--frame-ig-h); width: calc(var(--frame-ig-h) * 0.8);
  overflow: hidden; background: #000; border: 0; cursor: zoom-in; outline-offset: 3px; }
/* Absolute, not just object-fit: a figure with a figcaption in normal flow was
   letting the caption claim height and leaving a black band under the photo. */
.frame-ig__frame img { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
  transition: filter .45s ease, transform .5s cubic-bezier(.22,.61,.36,1), opacity .3s ease; }
.frame-ig--gray .frame-ig__frame img { filter: grayscale(1) contrast(1.06) brightness(1.06); }
.frame-ig--gray .frame-ig__frame.is-colour img { filter: none; }
.frame-ig__frame:hover img, .frame-ig__frame:focus-visible img {
  filter: none; transform: scale(1.05); }

/* Corner brackets snap in, timecode fades up — the viewfinder language from
   the reference, drawn rather than pasted so it inherits the accent. */
.frame-ig__brackets { position: absolute; inset: 8px; pointer-events: none; opacity: 0;
  transition: opacity .2s ease, inset .3s cubic-bezier(.22,.61,.36,1); }
.frame-ig__frame:hover .frame-ig__brackets,
.frame-ig__frame:focus-visible .frame-ig__brackets { opacity: 1; inset: 6px; }
.frame-ig__brackets i { position: absolute; width: 13px; height: 13px; border: 1.5px solid #fff; }
.frame-ig__brackets i:nth-child(1) { top: 0; left: 0; border-right: 0; border-bottom: 0; }
.frame-ig__brackets i:nth-child(2) { top: 0; right: 0; border-left: 0; border-bottom: 0; }
.frame-ig__brackets i:nth-child(3) { bottom: 0; left: 0; border-right: 0; border-top: 0; }
.frame-ig__brackets i:nth-child(4) { bottom: 0; right: 0; border-left: 0; border-top: 0; }

.frame-ig__tc { position: absolute; top: 10px; left: 34px; font-family: var(--frame-type-mono, monospace);
  font-size: 9px; letter-spacing: .14em; color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,.9);
  opacity: 0; transform: translateY(-3px); transition: opacity .2s ease, transform .2s ease; }
.frame-ig__frame:hover .frame-ig__tc, .frame-ig__frame:focus-visible .frame-ig__tc { opacity: .92; transform: none; }

.frame-ig__rec { position: absolute; top: 9px; right: 10px; display: flex; align-items: center; gap: 5px;
  font-family: var(--frame-type-mono, monospace); font-size: 9px; letter-spacing: .16em; color: #fff; }
.frame-ig__rec::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--frame-ig-accent); }

.frame-ig__cap { position: absolute; left: 0; right: 0; bottom: 0; margin: 0; border: 0; padding: 22px 10px 9px;
  font-size: 11px; line-height: 1.35; color: #fff;
  background: linear-gradient(transparent, rgba(0,0,0,.86));
  opacity: 0; transform: translateY(6px); transition: opacity .25s ease, transform .25s ease; }
.frame-ig__frame:hover .frame-ig__cap, .frame-ig__frame:focus-visible .frame-ig__cap { opacity: 1; transform: none; }

.frame-ig__cta { display: inline-block; margin: 16px 0 4px; font-family: var(--frame-type-mono, monospace);
  font-size: .74rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--frame-ig-accent); text-decoration: none; border-bottom: 1px solid currentColor; padding-bottom: 2px; }
.frame-ig__cta:hover { color: var(--frame-fg1, #fff); }

/* Expanded frame */
.frame-ig__lightbox { position: fixed; inset: 0; z-index: 9999; display: none;
  align-items: center; justify-content: center; gap: 8px; padding: 4vh 3vw;
  background: rgba(4,4,6,.94); backdrop-filter: blur(6px); }
.frame-ig__lightbox.is-open { display: flex; animation: frame-ig-in .22s ease; }
@keyframes frame-ig-in { from { opacity: 0; } to { opacity: 1; } }
.frame-ig__lbfig { margin: 0; max-width: min(92vw, 1000px); max-height: 92vh; display: flex;
  flex-direction: column; gap: 10px; }
.frame-ig__lbfig img { max-width: 100%; max-height: 78vh; object-fit: contain; display: block;
  border-radius: 2px; box-shadow: 0 30px 90px rgba(0,0,0,.7); }
.frame-ig__lbfig figcaption { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  font-size: 13px; color: var(--frame-fg2, rgba(255,255,255,.86)); }
.frame-ig__lbnum { font-family: var(--frame-type-mono, monospace); font-size: 11px; letter-spacing: .16em;
  color: var(--frame-fg3, rgba(255,255,255,.5)); }
.frame-ig__lblink { font-family: var(--frame-type-mono, monospace); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--frame-ig-accent); text-decoration: none;
  border-bottom: 1px solid currentColor; }
.frame-ig__lbclose { position: absolute; top: 18px; right: 20px; width: 40px; height: 40px;
  background: none; border: 0; color: #fff; font-size: 20px; cursor: pointer; line-height: 1; }
.frame-ig__lbnav { background: none; border: 0; color: #fff; font-size: 40px; line-height: 1;
  padding: 10px 14px; cursor: pointer; opacity: .55; transition: opacity .15s ease; }
.frame-ig__lbnav:hover { opacity: 1; }
.frame-ig__lbclose:focus-visible, .frame-ig__lbnav:focus-visible { outline: 2px solid var(--frame-ig-accent); }

/* Divi paints `img{height:auto}` and its own figure sizing over the top, which
   left half the frames short with a black band under the photo and every frame
   a different width. These two are the ones worth forcing. */
.frame-ig .frame-ig__track > .frame-ig__frame {
  width: calc(var(--frame-ig-h) * 0.8) !important;
  height: var(--frame-ig-h) !important;
}
.frame-ig .frame-ig__frame > img {
  position: absolute !important; inset: 0 !important;
  width: 100% !important; height: 100% !important; max-width: none !important;
  object-fit: cover !important;
}
/* The edge code sat under the lower sprocket rail. Put it above it. */
.frame-ig__edge { bottom: 15px; }
/* The link is a flex child, so a border-bottom ran the whole column width. */
.frame-ig__cta { align-self: flex-start; width: fit-content; }

@media (max-width: 600px) {
  .frame-ig__lbnav { display: none; }        /* swipe instead — the buttons steal the photo's width */
  .frame-ig__lbfig img { max-height: 68vh; }
}
@media (prefers-reduced-motion: reduce) {
  .frame-ig__track { transition: none; }
  .frame-ig__frame:hover img { transform: none; }
}

/* ── frame/instagram, second pass: a SHEET, not a slider ─────────────
   The first version was one tidy row of identical rectangles wearing
   sprocket holes. A contact sheet is not tidy — the strips lie at
   different angles and overlap, the roll had verticals and horizontals
   on it so the frames are different widths, one print is enlarged and
   breaks the sprocket line, and there is a colour bar and a light leak
   because it is a physical object that was in a room. */
.frame-ig { position: relative; overflow: hidden; padding: 6px 0 2px; }

.frame-ig__strips { gap: 0; }
.frame-ig__strip {
  overflow-x: clip; overflow-y: visible;        /* heroes break the rail, ends still clip */
  margin-left: -4%; margin-right: -4%;          /* the sheet runs off the page */
  box-shadow: 0 18px 40px rgba(0,0,0,.55);
  transform-origin: 50% 50%;
}
.frame-ig__strip--0 { transform: rotate(-1.5deg); z-index: 1; }
.frame-ig__strip--1 { transform: rotate(1.1deg);  z-index: 3; margin-top: -7px; }
.frame-ig__strip--2 { transform: rotate(-0.6deg); z-index: 2; margin-top: -5px; }

/* Frames come off the roll in three widths. Portrait, landscape, and the
   narrow one where the shutter caught half a frame. */
.frame-ig .frame-ig__track > .frame-ig__frame.w1 { width: calc(var(--frame-ig-h) * 0.78) !important; }
.frame-ig .frame-ig__track > .frame-ig__frame.w2 { width: calc(var(--frame-ig-h) * 1.32) !important; }
.frame-ig .frame-ig__track > .frame-ig__frame.w3 { width: calc(var(--frame-ig-h) * 0.58) !important; }

/* The enlargement. Printed bigger, sitting proud of the strip it came from. */
.frame-ig .frame-ig__track > .frame-ig__frame.is-hero {
  height: calc(var(--frame-ig-h) * 1.22) !important;
  margin: calc(var(--frame-ig-h) * -0.11) 2px;
  z-index: 4; box-shadow: 0 10px 30px rgba(0,0,0,.75);
  outline: 1px solid rgba(255,255,255,.14); outline-offset: -1px;
}

/* Under the cursor the frame straightens up and comes forward — the sheet
   is crooked, the one you are looking at is not. */
.frame-ig__frame { transition: transform .35s cubic-bezier(.22,.61,.36,1), box-shadow .3s ease; }
.frame-ig__strip--0 .frame-ig__frame:hover { transform: rotate(1.5deg) scale(1.06); }
.frame-ig__strip--1 .frame-ig__frame:hover { transform: rotate(-1.1deg) scale(1.06); }
.frame-ig__strip--2 .frame-ig__frame:hover { transform: rotate(0.6deg) scale(1.06); }
.frame-ig__frame:hover { z-index: 6; box-shadow: 0 14px 40px rgba(0,0,0,.8); }

/* Frame number printed on the negative, always there — the hover timecode
   is a UI affordance, this is part of the object. */
.frame-ig__fnum { position: absolute; left: 0; top: 0; z-index: 2;
  padding: 2px 5px 3px; background: rgba(0,0,0,.72);
  font-family: var(--frame-type-mono, monospace); font-size: 8px; letter-spacing: .1em;
  color: #FFA86B; pointer-events: none; }

/* Colour calibration bar, printed down the edge of the sheet. */
.frame-ig__cal { position: relative; display: flex; align-items: center; gap: 12px;
  margin: 12px -4% 0; padding: 0 4%; transform: rotate(-0.6deg); }
.frame-ig__calbar { flex: 1 1 auto; height: 12px; opacity: .62;
  background: repeating-linear-gradient(90deg,
    #d8d2c4 0 26px, #b7452f 26px 52px, #d8a13a 52px 78px, #4f6b4a 78px 104px,
    #33507a 104px 130px, #7a3b63 130px 156px, #1a1a1c 156px 182px); }
.frame-ig__calnums { font-family: var(--frame-type-mono, monospace); font-size: 8px;
  letter-spacing: .28em; color: rgba(255,255,255,.32); white-space: nowrap; }

/* It is a physical object that was in a room with a light in it. */
.frame-ig__leak { position: absolute; inset: 0; pointer-events: none; z-index: 5;
  background:
    radial-gradient(60% 120% at -6% 24%, rgba(255,120,40,.34), transparent 62%),
    radial-gradient(46% 90%  at 104% 78%, rgba(255,60,90,.20), transparent 60%);
  mix-blend-mode: screen; }
.frame-ig__grain { position: absolute; inset: 0; pointer-events: none; z-index: 6; opacity: .12;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: overlay; }

@media (max-width: 700px) {
  /* Angles that read as a sheet on a desk read as a mistake on a phone. */
  .frame-ig__strip--0, .frame-ig__strip--1, .frame-ig__strip--2 { transform: none; }
  .frame-ig__strip--1, .frame-ig__strip--2 { margin-top: 8px; }
  .frame-ig__strip { margin-left: -5%; margin-right: -5%; }
  .frame-ig .frame-ig__track > .frame-ig__frame.is-hero { height: var(--frame-ig-h) !important; margin: 0 2px; }
  .frame-ig__cal { transform: none; }
  .frame-ig__leak { opacity: .7; }
}
@media (prefers-reduced-motion: reduce) {
  .frame-ig__frame:hover { transform: none !important; }
}

/* ── Critic pass, batch 1 ─────────────────────────────────────────────
   Five defects an outside eye found that every automated check passed. */

/* 1. Full-brand-colour streaming marks — Spotify green, TikTok cyan/pink,
      YouTube red — were the FIRST content on the page, and between them they
      broke the ink palette before the band's own work was seen. They are
      still Carter's real icons; they just develop under the cursor, the same
      grammar as the contact sheet. */
.frame-ml__grid .frame-ml__mark img,
.frame-ml__grid .frame-ml__mark svg {
  filter: grayscale(1) contrast(1.05) brightness(1.15);
  transition: filter .28s ease, transform .28s cubic-bezier(.22,.61,.36,1);
}
.frame-ml__grid .frame-ml__mark:hover img,
.frame-ml__grid .frame-ml__mark:focus-visible img,
.frame-ml__grid .frame-ml__mark:hover svg,
.frame-ml__grid .frame-ml__mark:focus-visible svg {
  filter: none; transform: scale(1.1);
}

/* 2. The newsletter field was the brightest object on the page — brighter than
      any brand element. It was already declared dark; Divi's own input rule
      out-specified it, so this one has to say so. */
html body .frame-sub .frame-sub__input,
html body .frame-sub input[type="email"] {
  background: rgba(255,255,255,.06) !important;
  color: var(--frame-fg1, #fff) !important;
  border: 1px solid var(--frame-border, rgba(255,255,255,.20)) !important;
  box-shadow: none !important;
}
html body .frame-sub .frame-sub__input::placeholder { color: rgba(255,255,255,.42) !important; }
html body .frame-sub .frame-sub__input:focus {
  border-color: var(--frame-signal-lit, #FF6F62) !important;
  outline: none !important;
}

/* 3. "AUG 15 / NOV 1 / JUN 21" with no years reads as an unordered pile. */
.frame-shows__year {
  display: block; margin-top: 2px;
  font-family: var(--frame-type-mono, monospace); font-size: .62rem;
  letter-spacing: .2em; color: var(--frame-fg3, rgba(255,255,255,.42));
}

/* 4. The release is the purple one. The card's own ink was on the eyebrow and
      the track numbers but not on the thing the eye lands on first. */
.frame-rel__title { color: var(--frame-rel-accent, #9A6BFF); }
.frame-rel__tracks button[aria-current="true"] .frame-rel__ttitle {
  color: var(--frame-rel-accent, #9A6BFF);
}

/* ══ RISO — the print language, four channels ════════════════════════
   RISO-DIRECTION §3. Offset (`inkOffset`) already existed and was simply
   never used on a page. Screen and tear did not exist at all. Tile is the
   masthead, below. */

/* SCREEN — every gradient quantised into a visible dot grid, so nothing
   reads as continuous tone. One shared grid, multiply, plus a contrast
   crunch: honest approximation, not true halftone (dots do not track
   luminance live — that is a per-image asset generated offline).
   NEVER on nav, forms, shop chrome, or any surface carrying <20px text. */
.frame-screen { position: relative; isolation: isolate; }
.frame-screen::after {
  content: ''; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  mix-blend-mode: multiply;
  background-image:
    radial-gradient(circle at 50% 50%, rgba(0,0,0,.92) 42%, transparent 46%);
  background-size: var(--frame-screen-dot, 4px) var(--frame-screen-dot, 4px);
  opacity: var(--frame-screen-a, .42);
}
.frame-screen--fine   { --frame-screen-dot: 3px;   --frame-screen-a: .30; }
.frame-screen--coarse { --frame-screen-dot: 6.5px; --frame-screen-a: .52; }
/* The screen goes on the FIELD and on IMAGERY — never on the letterforms.
   Applied to a section it sat over the type too, and the hero wordmark came
   out looking like a screen door. RISO-DIRECTION §2 refuses exactly this:
   "a poster is glanced; a site is read". Content rides above the dots. */
.frame-section.frame-screen > .frame-section__inner,
.frame-screen > .frame-section__inner { position: relative; z-index: 3; }

/* the crunch — a screened image has no soft midtones left */
.frame-screen > img, .frame-screen picture > img { filter: contrast(1.22) saturate(.92); }
@media (max-width: 600px) {
  /* a 6.5px grid on a 3x phone screen is moiré, not print */
  .frame-screen--coarse { --frame-screen-dot: 5px; --frame-screen-a: .44; }
}

/* TEAR — layers meet at a torn edge, never a blur and never a fade. Four
   masks, authored once as gradients so they cost nothing and scale. */
.frame-tear { --frame-tear: 14px; }
.frame-tear-top    { -webkit-mask-image: var(--tear-t); mask-image: var(--tear-t); }
.frame-tear-bottom { -webkit-mask-image: var(--tear-b); mask-image: var(--tear-b); }
.frame-tear-both   { -webkit-mask-image: var(--tear-t), var(--tear-b);
                     mask-image: var(--tear-t), var(--tear-b);
                     -webkit-mask-composite: source-in; mask-composite: intersect; }
.frame-tear-left   { -webkit-mask-image: var(--tear-l); mask-image: var(--tear-l); }
.frame-tear-right  { -webkit-mask-image: var(--tear-r); mask-image: var(--tear-r); }
.frame-tear {
  --tear-t: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none' viewBox='0 0 400 400'%3E%3Cpath fill='%23fff' d='M0 12 6 6 14 13 22 4 31 11 40 3 49 12 58 5 68 13 77 6 86 14 96 5 105 12 114 4 124 13 133 6 142 14 152 5 161 12 170 3 180 11 189 6 198 13 208 4 217 12 226 6 236 14 245 5 254 12 264 3 273 11 282 6 292 13 301 4 310 12 320 6 329 14 338 5 348 12 357 4 366 13 376 6 385 12 394 5 400 10 400 400 0 400Z'/%3E%3C/svg%3E");
  --tear-b: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none' viewBox='0 0 400 400'%3E%3Cpath fill='%23fff' d='M0 0 400 0 400 390 394 396 385 388 376 395 366 387 357 396 348 388 338 395 329 386 320 394 310 388 301 396 292 387 282 394 273 389 264 397 254 388 245 395 236 386 226 394 217 388 208 396 198 387 189 394 180 389 170 397 161 388 152 395 142 386 133 394 124 387 114 396 105 388 96 395 86 386 77 394 68 387 58 395 49 388 40 397 31 389 22 396 14 387 6 394 0 388Z'/%3E%3C/svg%3E");
  --tear-l: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none' viewBox='0 0 400 400'%3E%3Cpath fill='%23fff' d='M12 0 6 8 13 17 4 26 11 36 3 45 12 55 5 64 13 74 6 83 14 93 5 102 12 112 4 121 13 131 6 140 14 150 5 159 12 169 3 178 11 188 6 197 13 207 4 216 12 226 6 235 14 245 5 254 12 264 3 273 11 283 6 292 13 302 4 311 12 321 6 330 14 340 5 349 12 359 4 368 13 378 6 387 12 396 400 400 400 0Z'/%3E%3C/svg%3E");
  --tear-r: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none' viewBox='0 0 400 400'%3E%3Cpath fill='%23fff' d='M0 0 0 400 388 400 394 392 387 382 396 373 388 363 395 354 386 344 394 335 387 325 396 316 388 306 395 297 386 287 394 278 388 268 396 259 387 249 394 240 389 230 397 221 388 211 395 202 386 192 394 183 387 173 396 164 388 154 395 145 386 135 394 126 387 116 396 107 388 97 395 88 386 78 394 69 387 59 395 50 388 40 397 31 389 21 396 12 387 3 388 0Z'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}

/* §4 — glass is the digital opposite of ink on paper. Content cards go flat.
   The nav's 9px blur STAYS: that is legibility over a moving gif, a function,
   not the language. */
.frame-rel, .frame-vc, #frame-w-42 > .frame-section,
.frame-section[style*="backdrop"] .frame-card {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}
.frame-rel { background: #111; }

/* The satellite wears the same clothes as the archive it was split from. */
.frame-shows__heading {
  margin: 0 0 .5em; font-family: var(--frame-type-display, sans-serif);
  font-size: clamp(1.5rem, 3vw, 2rem); color: var(--frame-fg1, #fff);
}
.frame-shows__none {
  margin: 0; font-family: var(--frame-type-mono, monospace); font-size: .8rem;
  letter-spacing: .1em; color: var(--frame-fg3, rgba(255,255,255,.5));
}
