/* ==========================================================================
   Loone Berlin — loone.live prototype v8 (ORBIT) · Direction E
   --------------------------------------------------------------------------
   Challenges v7-lockup: instead of treating the mark as a hero moment, the
   logo's CONSTRUCTION SYSTEM (ellipses, goo fillets, the brand book's
   pattern library, the "circle fused with rectangle" motif, B&W photo
   treatment, yellow⇄orange alternation) becomes the structural language of
   every page. All marks are the REAL vectors extracted from
   PRES_LOONE_1607.pdf — no font-faked wordmark anywhere.
   Palette + rules from 02. Branding/design-tokens.json:
   yellow = accent/CTA only, never large backgrounds; black #333 dominant;
   #CCC body text.

   TYPE — ONE FAMILY. HK Grotesk Wide, four cuts: Light 300, Regular 400,
   Bold 700, Black 900. Nothing else is loaded and nothing falls back to a
   system face except in the seconds before the files land.
   (Medium 500 is in the family and is deliberately NOT shipped: nothing on
   the site resolves to it. The `font-weight: 600` call sites — labels, pills,
   inline <b> — match UP to Bold under CSS Fonts 4, which is what they did
   under Pelikan too, so the weight texture is unchanged. Add the cut back in
   two lines if a 500 is ever wanted.)

   The history, so nobody re-litigates it. Body and UI were Inter (variable,
   self-hosted) and the label tier was the visitor's OWN system monospace —
   SF Mono on a Mac, Consolas on Windows, whatever else elsewhere. Both were
   rejected on 2026-08-25: Inter is the default UI face of every SaaS product
   on earth, and a label tier that renders in whatever the machine happens to
   own cannot be brand by definition. Space Grotesk went the same day — it is
   the house face of AI-generated web design and reads as exactly that. ABC
   Pelikan Extended then carried the whole site for an afternoon, which was
   the brand book's own face but a display cut doing text work, and it was
   also a trial licence the client would have had to buy for every paragraph
   on the site rather than just the headings.

   HK Grotesk Wide is the answer to both: still a wide grotesque, so the
   headline voice is unchanged, but drawn with text in mind — and free for
   commercial use, so nothing here is blocked on a purchase order. Wide is
   still not a NARROW face, so the compensations stay: body copy runs Light
   at 15px with 1.55 leading, and the label tier is tracked at 0.07em rather
   than the 0.14em a normal-width design would want.

   Self-hosted, and it must stay that way: hotlinking fonts.googleapis.com
   ships the visitor's IP to Google, which a German court (LG München I,
   3 O 17493/20) called an actionable GDPR breach. This site carries a
   Berlin imprint.
   ⚠ LICENCE: HK Grotesk Wide, commercial use allowed (see the licence file
   shipped beside the OTFs in 00. Inbox/hk-grotesk-wide-font-family). Keep
   that file — it is the only proof of the grant.
   ========================================================================== */

@font-face {
  font-family: "HK Grotesk Wide";
  src: url("../fonts/hkgw-light.woff2") format("woff2");
  font-weight: 300;
  font-display: swap;
}
@font-face {
  font-family: "HK Grotesk Wide";
  src: url("../fonts/hkgw-regular.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "HK Grotesk Wide";
  src: url("../fonts/hkgw-bold.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: "HK Grotesk Wide";
  src: url("../fonts/hkgw-black.woff2") format("woff2");
  font-weight: 900;
  font-display: swap;
}

:root {
  --bg: #333333;         /* dominant neutral (brand "black") */
  --stage: #1a1a1a;      /* hero + deep sections backdrop */
  --panel: #3b3b3b;      /* raised cards */
  --well: #2b2b2b;       /* inputs, recessed surfaces */
  --ink: #cccccc;        /* body text */
  /* Secondary text. Was #606060, which measured 2.01:1 on --bg and 1.78:1 on
     --panel — it carries every form label, caption and running head on the
     site, so it is body text and needs 4.5:1, not a caption exemption. #9a9a9a
     is the lightest value that clears AA on all three grounds. */
  --dim: #a5a5a5;        /* secondary text — 4.5:1 on --panel, the lightest ground it sits on */
  /* Two rules, two jobs. --hair is a DECORATIVE divider and may be faint;
     --edge is the boundary of something you can click or type into, and WCAG
     1.4.11 wants 3:1 for those. Using --hair on a control was the bug. */
  --hair: rgba(204, 204, 204, 0.14);
  --edge: rgba(204, 204, 204, 0.48);   /* control borders — 3:1 on --bg, the darkest ground it sits on */
  --yellow: #ffd60a;     /* accent/CTA, keywords, dates — never large fills */
  --orange: #ff4200;     /* secondary accent — action buttons */
  --acc: var(--yellow);  /* per-section accent; sections may flip to orange */
  /* Three tokens, one family. They are kept apart because they still mean
     three different things — running text, headline, instrument label — and
     the site sets each differently in weight, size, tracking and case. What
     they no longer do is reach for three different typefaces to say it. */
  --sans: "HK Grotesk Wide", system-ui, sans-serif;
  --display: "HK Grotesk Wide", system-ui, sans-serif;
  --mono: "HK Grotesk Wide", system-ui, sans-serif;
  --wrap: 1200px;
  /* One height. There used to be two — 64px at rest, 52px once scrolled —
     but the bar is now hidden at the top of the hero and only ever appears
     in its condensed state, so the tall variant had nothing left to be. */
  --head-h: 56px;
  color-scheme: dark;

  /* ---- motion ----
     Three curves and three speeds for the whole site. --ease is the house
     curve (a long decelerating tail — things arrive and settle rather than
     stop); --ease-io for anything that has to leave as well as arrive;
     --ease-snap for the small confirmations a pointer expects back fast. */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-io: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-snap: cubic-bezier(0.34, 1.4, 0.64, 1);
  --t-fast: 0.18s;
  --t: 0.32s;
  --t-slow: 0.8s;
}

/* the brand's every-other-week yellow⇄orange pulse, as a layout rhythm */
[data-accent="orange"] { --acc: var(--orange); }

* { margin: 0; padding: 0; box-sizing: border-box; }

/* THE hidden ATTRIBUTE HAS TO WIN. The UA sheet gives it display:none at the
   lowest possible specificity, so ANY rule of ours that sets display — .btn,
   a grid, a flex row — silently beats it and the element stays on screen
   with its attribute cheerfully set to true. Every el.hidden = true in
   app.js depends on this line. */
[hidden] { display: none !important; }

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

/* Page changes are swapped in place by the soft router in app.js (so the
   stream keeps playing); this is the crossfade it uses. @view-transition
   still covers the hard navigations the router hands back to the browser. */
@view-transition { navigation: auto; }
/* Out is quick and slightly downward — the page sinks and dims rather than
   blinking. In is the slow curve, so arriving always feels longer than
   leaving. Nothing position:fixed lives inside <main>, so the transform
   here can't capture a fixed descendant (the stream and the header are both
   body-level, deliberately). */
main, .site-foot { transition: opacity 0.4s var(--ease), transform 0.4s var(--ease); }
.nav-out main, .nav-out .site-foot {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  transition-duration: 0.17s;
  transition-timing-function: var(--ease-io);
}

/* Running text in an EXTENDED face. Three compensations, all of them for the
   same fact — every glyph is wider, so a line holds fewer words and the eye
   travels further per word:
     · Book (300), not Regular. The lightest cut is the one that stays quiet
       at paragraph length; Regular at 16px in a wide design reads as bold.
     · 15px, not 16. HK Grotesk Wide has a generous x-height, so it holds
       its size at 15 the way a smaller-eyed face would not.
     · 1.55 leading and a hair of negative tracking. Wide letterforms already
       supply the horizontal air that default tracking is there to add. */
body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 300;
  line-height: 1.55;
  letter-spacing: -0.008em;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

::selection { background: var(--yellow); color: #1a1a1a; }

/* GRAIN. taste.md lists five depth tools and the site was using two: shadow,
   sparingly, and overlap. Grain is the one that matters most here, because a
   flat dark ground BANDS on a real display — and this page is 100vw of #333
   and #1a1a1a with soft radial scrims over it, which is precisely the case
   that bands. 4.5% over the whole viewport is the difference between a dark
   page and a lit room.

   Above the header (z 60) and the docked player (z 70) so the frame is one
   image rather than a grainy page under clean chrome. pointer-events:none,
   so it is invisible to every interaction. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 80;
  pointer-events: none;
  opacity: 0.045;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
  background-size: 160px 160px;
}

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

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 clamp(20px, 4vw, 48px); }
/* The one screen-reader-only rule. It was `class="visually-hidden"` at three
   call sites with the declarations hand-copied into a style attribute beside
   it, and the class itself defined nowhere. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The instrument-label tier. Tracking is down from 0.14em to 0.07em and the
   size from 11px to 10px, because the face itself is now doing half that
   job: an extended design at caps already reads spaced, and 0.14em on top of
   it ran the nav off the end of the bar. */
.mono {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  /* the clock ticks once a second — proportional figures make it twitch */
  font-variant-numeric: tabular-nums;
}

/* ---------- display type — HK Grotesk Wide Black ---------- */

/* One weight. Black across the display tier is the identity, not an oversight
   — headings set in the Regular cut read as a different brand. (Tried it on
   2026-08-25 and reverted: the stylesheet header cites the brand book as "H1
   Black, H2 Regular", but the built design has never worked that way and the
   coherence of one voice beats the citation. pelikan-regular.woff2 therefore
   stays unused; leave it in the repo, it costs nothing unrequested.)

   Tracking tightens as size grows — optical correction, not decoration. A
   face set at 88px with the same -0.025em as one set at 22px reads loose at
   the top and cramped at the bottom. The ramp runs -0.05em at hero scale to
   -0.008em at caption scale. */
.display {
  font-family: var(--display);
  font-weight: 900;
  letter-spacing: -0.035em;
  line-height: 0.96;
  color: #e4e4e4;
}
h1, h2, h3 { font-weight: inherit; }

.page-title { letter-spacing: -0.032em; }
.page-title--sm { letter-spacing: -0.028em; }

/* ---------- section scaffold + the chip marker ----------
   Every section opens like a brand book page: the 3-ellipse chip glyph with
   the running title laid across it, plus a section index. */

/* opaque + positioned on purpose: the fixed .stage-video sits at z-index 0
   behind the whole document, and only a positioned, non-transparent section
   is painted over it. A transparent .sec would show the stream through it. */
.sec { padding: clamp(72px, 10vw, 132px) 0; position: relative; z-index: 1; background: var(--bg); }
.sec + .sec { border-top: 1px solid var(--hair); }
.site-foot { position: relative; z-index: 1; }

.sec-mark {
  display: flex;
  align-items: center;
  gap: 0;
  margin-bottom: clamp(32px, 5vw, 56px);
}
.sec-mark img { height: clamp(40px, 5vw, 56px); width: auto; opacity: 0.9; }
.sec-mark .sec-title {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(22px, 3vw, 34px);
  letter-spacing: -0.012em;
  color: var(--acc);
  margin-left: calc(clamp(40px, 5vw, 56px) * -1.6); /* title overlaps the chip, brand book style */
  position: relative;
  z-index: 1;
  white-space: nowrap;
}
.sec-mark .sec-idx {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.2em;
  color: var(--dim);
}
/* The title is nowrap and the index is pushed to the far end of the same
   row, so below roughly 600px the two meet and overprint each other. The
   index is a running head — it goes rather than shrinking the title. */
@media (max-width: 620px) { .sec-mark .sec-idx { display: none; } }

/* ---------- THE BACKGROUND GLYPH ----------
   "Scaled up as a background watermark", per the brand book's own pattern
   rules — and using the book's own render of the mark
   (assets/photos/glyph-soft.png, lifted from PRES_LOONE_1607.pdf): greys
   rather than yellow, and partially out of focus, sharp and hot along the
   top-right arcs and dissolving everywhere else. The flat chip SVG it
   replaced could never carry that, and reconstructing it from CSS masks
   and blurs only ever approximated the thing the dossier already contains.
   The artwork is white-on-black, so `screen` drops the black and leaves
   the glyph. */
.sec-watermark { position: relative; overflow: hidden; }
.sec-watermark::before {
  content: "";
  position: absolute;
  inset: -12% -18%;
  background: url("../assets/photos/glyph-soft.png") no-repeat center / contain;
  mix-blend-mode: screen;
  opacity: 0.28;
  pointer-events: none;
}

/* ---------- THE NEST, BEHIND THE STREAMS ----------
   `EDIT NID.png` out of the brand book — *nid* is French for nest, and it is
   literally one: a dark textured sphere on near-black. It is the strongest
   argument in the dossier for what the word means, and the archive is the
   thing the nest has actually produced, so it is the section that gets it.

   Cover-fit and pinned right, because the sphere sits centre-frame in the
   source and the left two thirds of this section carry the lead card and its
   caption. Two layers over it: a left-to-right gradient so the type side
   stays flat --bg, and the section's own ground underneath, so the image is
   an atmosphere and never a thing to read text off. It is `aria-hidden` by
   construction (a pseudo-element) and carries no information. */
.sec-nest { position: relative; overflow: hidden; isolation: isolate; }
.sec-nest::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Height-sized, not `cover`. The source is portrait (750x937) and this
     section is landscape, so `cover` upscaled it ~1.9x and left nothing in
     frame but the sphere's outer edge. At `auto 130%` the nest itself is in
     the picture, sitting to the right where the rows are lightest. */
  background: url("../assets/photos/nest.jpg") no-repeat right -6% center / auto 130%;
  opacity: 0.55;
  pointer-events: none;
}
/* the readable side. Opaque at the left edge, gone by the time it reaches
   the image's own centre of mass */
.sec-nest::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(90deg, var(--bg) 0%, rgba(51, 51, 51, 0.88) 38%, rgba(51, 51, 51, 0.44) 72%, rgba(51, 51, 51, 0.58) 100%),
    linear-gradient(180deg, var(--bg) 0%, transparent 22%, transparent 78%, var(--bg) 100%);
}
/* one column on a phone, so there is no "type side" to protect — the image
   goes back to being a faint ground under everything */
@media (max-width: 900px) {
  .sec-nest::before { background-position: center; opacity: 0.32; }
  .sec-nest::after { background: linear-gradient(180deg, var(--bg) 0%, rgba(51, 51, 51, 0.82) 30%, rgba(51, 51, 51, 0.82) 70%, var(--bg) 100%); }
}

/* ---------- the venue pill ----------
   Which room a night comes from, in one token. One colour per room and the
   same one everywhere: Kreuzberg yellow, Mitte orange. */
.pill {
  justify-self: start;
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 4px 9px;
  border-radius: 999px;
  color: #1a1a1a;
  background: var(--yellow);
  white-space: nowrap;
}
.pill[data-venue="amt"] { background: var(--orange); }

/* ---------- UPCOMING — the slides ----------
   A full-bleed row of 3:4 plates. These ARE the Instagram flyers: the
   weekly renderer on the Hetzner box generates a poster and a timetable per
   night, and `flyer` in schedule.json points at it.

   (Briefly folded into a two-column text board on 2026-08-25 and put back
   the same day. The rows were more compact and that was the wrong trade:
   the posters are real artwork arriving on a real pipeline, and a date in
   a table cannot stand in for one. The typographic fallback below is what
   makes the strip safe to ship before that pipeline is wired.) */
.strip {
  position: relative;   /* like .sec — must paint over the fixed stage video */
  z-index: 1;
  background: var(--bg);
  display: grid;
  grid-auto-flow: column;
  /* CAPPED, not 1fr. At 1fr the cards divide whatever width there is, so a
     week with two nights rendered two 720px-wide posters — and 3:4 made them
     960px tall, a full screen each. A poster has a size; the strip centres
     however many there are and scrolls once they outgrow the row. */
  grid-auto-columns: clamp(230px, 30vw, 400px);
  justify-content: center;
  gap: 2px;
  overflow-x: auto;
  scrollbar-width: none;
}
.strip::-webkit-scrollbar { display: none; }
/* below ~3 cards' worth of width the row stops being a composition and
   becomes a squeeze — let it scroll from the left instead */
@media (max-width: 720px) {
  .strip { grid-auto-columns: clamp(230px, 76vw, 320px); justify-content: start; }
}

/* ---------- an upcoming night ---------- */
.flyer {
  position: relative;
  display: block;
  /* the renderer's own size — 1080×1350, the tallest Instagram takes. A 3:4
     plate cropped six percent off the sides of real artwork. */
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--stage);
  text-decoration: none;
  color: inherit;
}
.flyer-art { position: absolute; inset: 0; }
.flyer-art img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.9s var(--ease); }

/* No flyer yet. Not a placeholder box — a poster in the brand's own type,
   built from the one thing we always know about a night: its date. It reads
   as designed, so an empty `flyer` field is safe to ship right up until the
   renderer starts filling it. */
.flyer-art-gen {
  display: grid;
  place-items: center;
  background: var(--stage);
  transition: transform 0.9s var(--ease);
}
.flyer-art-gen::before {
  content: "";
  position: absolute;
  inset: -8%;
  background: url("../assets/loone-chip-dim.svg") no-repeat center / 74%;
  opacity: 0.45;
}
.gen-date {
  position: relative;
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(30px, 3.8vw, 46px);
  letter-spacing: -0.035em;
  line-height: 0.92;
  color: var(--acc);
  text-align: center;
  margin-bottom: 18%;   /* the caption owns the lower third */
}
.gen-date small {
  display: block;
  margin-top: 12px;
  font-family: var(--mono);
  font-weight: 400;
  font-size: 10px;
  letter-spacing: 0.2em;
  text-indent: 0.2em;   /* letter-spacing pads the right edge; recentre it */
  color: var(--dim);
}

/* A night with its poster drawn needs no caption: the flyer already carries
   the date, the act and the room, and printing them again over the artwork
   is a worse copy of it. js/app.js drops .has-art when the image 404s, and
   the caption comes back. */
.flyer.has-art .flyer-cap { display: none; }

.flyer-cap {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: grid;
  gap: 7px;
  padding: 16px;
  background: linear-gradient(180deg, transparent, rgba(26, 26, 26, 0.9) 48%);
}
.flyer-when { font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink); }
.flyer-act { font-family: var(--display); font-weight: 900; font-size: clamp(14px, 1.4vw, 17px); letter-spacing: -0.012em; color: #f2f2f2; }
.flyer-kind { font-size: 12px; color: var(--dim); margin-top: -3px; }
.flyer-go {
  justify-self: start;
  margin-top: 4px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--yellow);
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
}

/* ---------- the poem ----------
   Author-broken lines, so the text needs room to keep them: the measure is
   wider than .lede and the leading is looser. text-wrap:balance would fight
   the <br>s, hence pretty. */
.poem {
  max-width: 52ch;
  font-size: clamp(17px, 1.8vw, 21px);
  line-height: 1.85;
  color: var(--ink);
  text-wrap: pretty;
}
.poem + .poem { margin-top: clamp(20px, 2.6vw, 32px); }
.poem b, .poem strong { color: #f2f2f2; font-weight: 600; }
/* The gap before the invitation. NOT `.poem:last-of-type { margin-bottom }`:
   :last-of-type picks whichever <p> happens to end the band, which is not
   reliably the poem. */
.poem + .cta-link { margin-top: clamp(46px, 6vw, 86px); }

.label {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 18px;
}
.label .idx { color: var(--acc); margin-right: 10px; }

.lede { max-width: 620px; font-size: clamp(16px, 1.6vw, 19px); color: var(--ink); }
/* was a hand-typed `style="margin-top: 20px"` on fourteen separate paragraphs */
.lede + .lede { margin-top: 20px; }
.lede b, .lede strong { color: #e9e9e9; }
.about-body b, .about-body strong { color: #e9e9e9; font-weight: 600; }

/* ---------- page openers ----------
   Every page but the home page starts the same way: one section clearing the
   fixed header, one display headline. Both were inline styles repeated across
   eight and twelve elements, in two near-identical variants each. */
.sec-top { padding-top: calc(var(--head-h) + clamp(56px, 8vw, 100px)); }
.sec-top--deep { padding-top: calc(var(--head-h) + clamp(64px, 9vw, 120px)); background: var(--stage); }
.page-title { font-size: clamp(30px, 5vw, 56px); }
.page-title--sm { font-size: clamp(28px, 4.5vw, 48px); }

.sec-top--tall { min-height: 70vh; }

/* Three steps, because the markup had eight: 18, 20, 28, 32, 34, 36, 42 and
   44px hand-typed onto individual elements. Eight near-identical values is
   not a rhythm, it is the absence of one — nothing reads as a break because
   every gap is roughly the same size as every other. */
.mt-s { margin-top: 20px; }
.mt-m { margin-top: 32px; }
.mt-l { margin-top: clamp(36px, 5vw, 56px); }

/* the blocks the imprint and privacy links jump to */
.anchored { scroll-margin-top: 90px; }

/* ---------- header ---------- */

.site-head {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 22px;
  height: var(--head-h);
  padding: 0 clamp(16px, 3vw, 36px);
  background: rgba(26, 26, 26, 0.72);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid rgba(204, 204, 204, 0.08);
  transition:
    height 0.45s var(--ease),
    transform 0.5s var(--ease),
    top 0.5s var(--ease),
    background-color 0.45s var(--ease),
    border-color 0.45s var(--ease);
}
/* Past the fold the bar gains weight — it stops being part of the hero and
   becomes chrome. It no longer changes HEIGHT: several pages reserve their
   top padding from --head-h, and the bar is now hidden over the hero
   anyway, so the tall variant was a state nobody ever saw. */
html.scrolled .site-head {
  background: rgba(26, 26, 26, 0.9);
  border-bottom-color: rgba(204, 204, 204, 0.14);
}

/* THE HOME PAGE OPENS WITHOUT CHROME.
   Over the hero the bar is simply not there: the mark in the middle of the
   frame IS the logo, and a second, smaller copy of it 30px above reads as a
   duplicate. The bar arrives on the first scroll — or the moment the mark
   flies into it, which is what .tuned means. Scoped to the hero page: every
   other page needs its nav on arrival. */
body:has([data-lockup-hero]) .site-head { transform: translate3d(0, -100%, 0); }
html.scrolled body:has([data-lockup-hero]) .site-head,
html.tuned body:has([data-lockup-hero]) .site-head { transform: none; }

.wordmark { display: flex; align-items: center; }
/* The bar carries the LOCKUP — rings, word and © — because that is what the
   hero flies into it, and a wordmark-only bar meant the ellipses had to
   dissolve on the way. Taller than the old 20px wordmark by necessity: the
   word is only ~64% of the lockup's width, so at 20px it was unreadable.
   38px in a 56px bar leaves 9px of air either side. */
.wordmark img { height: 38px; width: auto; transition: opacity var(--t) var(--ease); }
.wordmark:hover img { opacity: 0.72; }
/* On the hero page the header mark is the LANDING SITE of the hero
   animation, not a logo of its own: it stays invisible until the flying mark
   has arrived on top of it (.mark-home), and the two swap over ~4% of the
   flight, where they are geometrically coincident. Scrolling back up runs it
   in reverse. */
body:has([data-lockup-hero]) .wordmark img { opacity: 0; transition-duration: 0.14s; }
html.mark-home body:has([data-lockup-hero]) .wordmark img { opacity: 1; }
.head-clock {
  color: var(--dim);
  display: none;
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease);
}
.head-clock b { color: var(--ink); font-weight: 600; }
@media (min-width: 900px) { .head-clock { display: block; } }
/* The clock used to fade out on .scrolled — it was hero furniture, and the
   bar had a tall state over the hero to hold it. Both of those are gone: the
   bar is one height and, on the home page, .scrolled is the only state in
   which it exists at all. Keeping that rule meant a Berlin clock nobody
   could ever see. It stays. */

.site-nav { display: flex; gap: clamp(14px, 2vw, 28px); margin-left: auto; }
.site-nav a {
  position: relative;
  text-decoration: none;
  color: var(--ink);
  padding: 6px 2px 8px;
  transition: color var(--t) var(--ease);
}
/* the rule draws in from the left on hover and retracts to the right on the
   way out — a swap of transform-origin, so it reads as one continuous stroke
   rather than a bar that grows and shrinks in place */
.site-nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  background: var(--yellow);
  transform: scaleX(0);
  transform-origin: 100% 50%;
  transition: transform 0.42s var(--ease);
}
.site-nav a[aria-current="page"]::after { transform: scaleX(1); transform-origin: 0 50%; }
.site-nav a[aria-current="page"] { color: var(--yellow); }
/* The blinking dot survives only where it means "this is happening right
   now" — the room status on /visit. It used to ride the nav's Live link and
   the hero's Next line, where it blinked at a date three days away. */
.on-air .dot {
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--orange);
  margin-right: 7px;
  animation: pulse 2s infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }

/* ---------- THE MENU GLYPH ----------
   Was the word "Menu" in a pill, which is the one place on the site where
   the brand said nothing. Three stacked marks still read as "menu" to
   everyone — but they are drawn as ELLIPSE OUTLINES, which is the logo's own
   construction language (four rings, hollow, fused where they meet) rather
   than three solid bars off a UI kit. Open, the outer two swing together
   into a cross and the middle one collapses.
   The label lives in aria-label, not in a text node: JS swaps it, so the
   button is still announced correctly with nothing visible to read. */
.nav-toggle {
  display: none;
  margin-left: auto;
  width: 42px;
  height: 42px;
  place-items: center;
  background: none;
  border: 1px solid var(--edge);
  color: var(--ink);
  border-radius: 50%;
  cursor: pointer;
  transition: border-color var(--t) var(--ease), color var(--t) var(--ease);
}
.nav-toggle:hover { border-color: var(--yellow); color: var(--yellow); }
.nav-toggle[aria-expanded="true"] { border-color: var(--yellow); color: var(--yellow); }
.nav-glyph { display: grid; gap: 4px; width: 20px; }
.nav-glyph i {
  display: block;
  height: 6px;
  border: 1.5px solid currentColor;
  border-radius: 50%;
  transition: transform 0.42s var(--ease), opacity 0.24s var(--ease);
}
/* rows sit on a 10px pitch (6px tall + 4px gap), so ±10px brings the outer
   two onto the middle one's centreline before they rotate */
.nav-toggle[aria-expanded="true"] .nav-glyph i:nth-child(1) { transform: translateY(10px) rotate(38deg); }
.nav-toggle[aria-expanded="true"] .nav-glyph i:nth-child(2) { opacity: 0; transform: scaleX(0.15); }
.nav-toggle[aria-expanded="true"] .nav-glyph i:nth-child(3) { transform: translateY(-10px) rotate(-38deg); }

@media (max-width: 860px) {
  .nav-toggle { display: grid; }
  /* backdrop-filter makes an element the containing block for its
     position:fixed descendants, so the drawer's `inset: var(--head-h) 0 0 0`
     was resolving inside the header's own 64px box and collapsing to
     nothing — the menu opened as bare text floating over the page. Drop the
     blur at this breakpoint and the drawer measures against the viewport. */
  .site-head {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgba(26, 26, 26, 0.94);
  }
  html.scrolled .site-head { background: rgba(26, 26, 26, 0.97); }
  /* Same retreat, moved off `transform` on purpose. A non-none transform makes
     the header the containing block for the fixed drawer inside it — the exact
     collapse the comment above describes — and it would apply for the whole
     0.5s the property animates back to none. `top` can never do that. */
  body:has([data-lockup-hero]) .site-head { transform: none; top: calc(var(--head-h) * -1); }
  html.scrolled body:has([data-lockup-hero]) .site-head,
  html.tuned body:has([data-lockup-hero]) .site-head { top: 0; }
  .site-nav {
    position: fixed;
    inset: var(--head-h) 0 0 0;
    /* opaque. At 0.97 the page underneath ghosted through — a yellow
       section heading floating behind the nav reads as a rendering bug,
       and 3% of transparency buys nothing */
    background: var(--stage);
    flex-direction: column;
    padding: 40px clamp(20px, 4vw, 48px);
    gap: 8px;
    display: none;
  }
  .site-nav.open { display: flex; }
  .site-nav a {
    font-family: var(--display);
    font-size: 30px;
    font-weight: 900;
    letter-spacing: -0.02em;
    padding: 12px 0;
    border-bottom: 1px solid var(--hair);
  }
}

/* ---------- hero: the orbit ---------- */

/* transparent: the stream (.stage-video, fixed at z-index 0) shows THROUGH
   the hero. The dark stage colour moves onto the page canvas so it is still
   right before the video loads, and on file:// where it never does. */
body:has([data-lockup-hero]) { background: var(--stage); }

/* /live is gone (2026-08-25). It was the home page with the mark taken off:
   same stream, same iframe, one fewer click to reach it from the header —
   and a nav item whose blinking dot promised something live at three in the
   afternoon on a Tuesday. The home page already plays the stream full-bleed
   and now hands you YouTube's own controls once you tune in, so the page had
   nothing left of its own. .live-stage and the [data-stage-full] rules went
   with it. /live 301s to /. */
.hero {
  position: relative;
  min-height: 100svh;
  background: transparent;
  overflow: hidden;
  display: flex;
  align-items: flex-end;
}

/* Cursor-following blur disc — blurs the ellipses in a ~150px radius around
   the pointer.  Driven by --bx/--by custom properties set from JS. Hidden
   until the pointer enters the hero (.ring-blur-active). */
.ring-blur-cursor {
  position: absolute;
  inset: 0;
  z-index: 1;       /* above the SVG (z-auto) but below lockup-hit (z:3) */
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s var(--ease);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  -webkit-mask-image: radial-gradient(var(--blur-r, 200px) var(--blur-r, 200px) at var(--bx, -999px) var(--by, -999px), rgba(0,0,0,0.85) 0%, transparent 100%);
  mask-image: radial-gradient(var(--blur-r, 200px) var(--blur-r, 200px) at var(--bx, -999px) var(--by, -999px), rgba(0,0,0,0.85) 0%, transparent 100%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
.ring-blur-active .ring-blur-cursor { opacity: 1; }
/* A full-hero backdrop-filter, composited over a decoding video, on a layer
   that exists only to follow a cursor. There is no cursor on a phone, so it
   is pure cost there — and the hero already carries the SVG goo filter plus
   .hero-blur. This is the cheapest layer to take off the mobile budget. */
@media not all and (hover: hover) and (pointer: fine) {
  .ring-blur-cursor { display: none; }
}

/* ---------- the stage video ----------
   ONE body-level iframe for the whole session. It is never reparented and
   never lives inside <main>, because both of those reload an iframe — that
   was the "the stream restarts when I scroll / change page" bug. Docking is
   nothing but a class flip: the box animates from full-viewport to a corner
   thumbnail, and the frame inside it keeps playing.
   Sized with container query units so cover-fit is correct at BOTH sizes
   (viewport units would blow the video up to screen size inside the 220px
   corner box). */
.stage-video {
  position: fixed;
  right: 0; bottom: 0;
  width: 100vw; height: 100svh;
  z-index: 0;
  pointer-events: none;
  container-type: size;
  /* THE BOX DOES NOT TRAVEL ANY MORE, AND NOTHING HERE ANIMATES SIZE.
     It used to: right/bottom/width/height all transitioned from full-bleed
     to the corner card, so a viewport-sized video visibly collapsed in on
     itself every time you scrolled past the hero. Two rounds of softening
     the curve did not fix it, because the problem was never the curve — a
     100vw box shrinking to 340px is a big, central motion however gently it
     is eased, and by that point the page content has already covered most of
     it, so the thing collapsing was mostly hidden anyway.

     The geometry snaps now, and the card slides in from the right edge
     instead (see pip-in / pip-out below). The snap is never seen: at that
     instant the card is still translated off screen and fully transparent.
     One small object entering from an edge, rather than the whole frame
     folding up in the middle.

     Only opacity transitions here — it carries the full-bleed backdrop back
     in when the card has slid out and the hero is returning. */
  transition: opacity 0.5s var(--ease-io);
}
/* only paints the stage backdrop when it actually holds a stream, otherwise
   an empty fixed layer would cover every other page in --stage */
.stage-video.on:not(.pip) { background: var(--stage); z-index: 1; }
.stage-video .player { position: absolute; inset: 0; border: 0; background: transparent; }
.stage-video .player iframe {
  position: absolute;
  top: 50%; left: 50%;
  width: max(100cqw, 177.78cqh);    /* cover-fit against the CONTAINER */
  height: max(100cqh, 56.25cqw);
  /* the extra 1.35 crops YouTube's own chrome (title bar, watch-later,
     end-screen grid) outside the frame — this is an ambient backdrop, not a
     player, so nothing interactive should ever be visible */
  transform: translate(-50%, -50%) scale(1.35);
  border: 0;
  pointer-events: none;   /* clicks belong to .lockup-hit, never the iframe */
  /* held at 0 until the embed is past YouTube's own centred loading spinner
     — see loadPlayer(). That spinner is what read as a grey coin behind the
     second o. */
  opacity: 0;
  filter: grayscale(1) contrast(1.08);  /* brand B&W treatment */
  transition: opacity 1.1s var(--ease), filter 0.8s var(--ease);
}
.stage-video.ready .player iframe { opacity: 0.5; }
/* TUNED IN → this stops being a backdrop and becomes a player.
   The room comes into colour, and — the part that matters — the frame stops
   being cover-fitted. A cover fit on a 16:9 embed in a taller-than-16:9
   viewport pushes YouTube's control bar below the fold, and the 1.35 crop
   that hides YouTube's chrome while it is wallpaper hides the controls too.
   Contain-fit and no crop, so the whole player is on screen and clickable.
   That is the entire reason the blur band and the standing copy clear out at
   the same moment: they were sitting exactly where the scrubber lands. */
.tuned .stage-video.ready .player iframe { opacity: 1; filter: grayscale(0) contrast(1); }
/* scoped to the hero page: .tuned rides on <html> and survives a soft
   navigation, and a full-viewport fixed layer that eats pointer events on
   /apply would swallow clicks meant for the form under it */
.tuned body:has([data-lockup-hero]) .stage-video:not(.pip) { pointer-events: auto; }
.tuned body:has([data-lockup-hero]) .stage-video:not(.pip) .player { background: #000; }
.tuned body:has([data-lockup-hero]) .stage-video:not(.pip) .player iframe {
  width: min(100cqw, 177.78cqh);
  height: min(100cqh, 56.25cqw);
  transform: translate(-50%, -50%);
  pointer-events: auto;
  transition: opacity 1.1s var(--ease), filter 0.8s var(--ease),
              width 0.7s var(--ease), height 0.7s var(--ease), transform 0.7s var(--ease);
}
.stage-video .yt-poster img { opacity: 0.5; filter: grayscale(1); }

/* ---------- DOCKED — same node, same iframe, smaller box ----------
   340px, not 240. The docked frame is a real player now (YouTube's controls
   are live in it, see below) and a scrubber, a volume slider and a fullscreen
   button do not fit in 240px — YouTube starts dropping them.

   It ENTERS FROM THE RIGHT EDGE. The geometry is applied instantly and the
   animation only moves and fades, which is why there is no size transition
   above: at frame zero the card is already 340px, already in the corner, and
   already off screen, so the jump from full-bleed happens behind a fully
   transparent element. `both` holds frame zero before the animation starts
   and the end state after it finishes. */
.stage-video.pip {
  right: 20px; bottom: 20px;
  width: min(340px, calc(100vw - 32px));
  height: calc(min(340px, calc(100vw - 32px)) * 9 / 16);
  z-index: 70;
  pointer-events: auto;
  animation: pip-in 0.6s var(--ease) both;
}
/* 28px past its own right edge, so the shadow clears the viewport too and
   nothing bleeds at the start */
@keyframes pip-in {
  from { opacity: 0; transform: translateX(calc(100% + 28px)); }
  to   { opacity: 1; transform: translateX(0); }
}
/* Leaving is the same move backwards, and quicker — you are scrolling back
   to the hero and the room should be there when you arrive, not still
   catching up. app.js holds .pip until this ends, then drops both classes
   and the base opacity transition above fades the full frame back in. */
.stage-video.undocking {
  animation: pip-out 0.42s var(--ease-io) both;
  pointer-events: none;
}
@keyframes pip-out {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(calc(100% + 28px)); }
}
/* The radius, border and shadow arrive with the card rather than settling in
   behind it — there is no travel left for them to lag. Kept as a transition
   only so undocking releases them softly instead of stripping them mid-slide. */
.stage-video .player {
  border-radius: 0;
  transition:
    border-radius 0.3s var(--ease),
    box-shadow 0.3s var(--ease),
    border-color 0.3s var(--ease);
  border: 1px solid transparent;
}
.stage-video.pip .player {
  border-radius: 10px;
  border-color: var(--hair);
  background: #000;
  box-shadow: 0 14px 40px -16px rgba(0, 0, 0, 0.62);
  overflow: hidden;   /* the radius has to cut the iframe, not just the box */
}
@media (prefers-reduced-motion: reduce) {
  .stage-video.pip, .stage-video.undocking { animation-duration: 0.01ms; }
  .stage-video { transition-duration: 0.01ms; }
}
/* THE CONTROLS ARE LIVE. The base rule takes pointer events off the iframe
   because on the hero the click belongs to .lockup-hit — but once it is a
   240×135 object in the corner there is no lockup and no hit target, and a
   video you cannot pause is a video you can only close. */
.stage-video.pip .player iframe {
  opacity: 1;
  filter: none;
  transform: translate(-50%, -50%);
  pointer-events: auto;
  transition: opacity 0.4s var(--ease), filter 0.5s var(--ease);
}
.stage-video.pip .pip-close { display: block; }

.hero-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  transition: opacity 1s var(--ease);
  background:
    radial-gradient(120% 65% at 50% 0%, rgba(26, 26, 26, 0.6), transparent 60%),
    linear-gradient(180deg, rgba(26, 26, 26, 0.5) 0%, rgba(26, 26, 26, 0.1) 30%, rgba(26, 26, 26, 0.72) 100%);
}
.hero-blur {
  transition: opacity 0.8s var(--ease);
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 38%;
  z-index: 1;
  pointer-events: none;
  -webkit-backdrop-filter: blur(22px);
  backdrop-filter: blur(22px);
  -webkit-mask-image: linear-gradient(to bottom, transparent, black 55%);
  mask-image: linear-gradient(to bottom, transparent, black 55%);
}

/* fixed 1200×800 artboard, scaled to the viewport by app.js */
.lockup-fly {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  will-change: transform, opacity;
}
.lockup-stage {
  position: absolute;
  top: 50%; left: 50%;
  width: 1200px;
  height: 800px;
  transform-origin: center;
  transform: translate(-50%, -50%);
}
/* one live vector for the whole mark — rings, word and © are all real brand
   geometry, animated by attribute. Nothing is rasterised, so it stays crisp
   at any stage scale (the old canvas + <img> pair went soft when the stage
   scaled above 1 on large displays). */
.lockup-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }

/* THE LOCK LANDING. The ring geometry is the real logo, so nothing may
   overshoot in the SVG itself — the impact is spent on the layer above it
   instead: one camera-sized push on .lockup-fly (the only wrapper JS does
   not drive) and a bloom that decays. Both are shorter than HOLD_MS, so
   they are finished before the construction rushes the camera. */
.hero.resolved .lockup-fly { animation: lock-land 460ms var(--ease); }
.hero.resolved .lockup-svg { animation: lock-bloom 760ms ease-out; }
/* the individual `scale` property, NOT `transform` — app.js owns .lockup-fly's
   transform (it is the flight into the header) and an animation on the same
   property would win over the inline style for as long as it ran */
@keyframes lock-land {
  0%   { scale: 1.028; }
  100% { scale: 1; }
}
@keyframes lock-bloom {
  0%   { filter: drop-shadow(0 0 0 rgba(255, 214, 10, 0)); }
  14%  { filter: drop-shadow(0 0 30px rgba(255, 214, 10, 0.5)); }
  100% { filter: drop-shadow(0 0 0 rgba(255, 214, 10, 0)); }
}

/* THE STRAPLINE lives inside the SVG now, as one <text> sitting 10 artboard
   px under the wordmark and tracked to the wordmark's EXACT width by
   textLength — which is a native SVG guarantee, not a hand-tuned
   letter-spacing that drifts the moment the string or the font changes.
   Sentence case: it is a sentence, and setting it in caps made a line of
   quiet copy shout louder than the logo above it.
   It rides OUTSIDE the goo group (like the ©) so it is never melted, and it
   travels with .lockup-fly, so it flies into the header with the mark. */
.lockup-tagline {
  fill: var(--yellow);
  font-family: var(--sans);
  /* Small on purpose. textLength stretches this line to the wordmark's exact
     width by ADDING space between glyphs, so the natural width has to stay
     comfortably under it — and HK Grotesk Wide is a wide design, which eats
     that headroom fast. At 15px the line sets to about two-thirds of the
     target and the rest becomes tracking. Raise it and lengthAdjust starts
     removing space instead, i.e. overlapping the letters. */
  font-size: 15px;
  font-weight: 400;
  opacity: 0;
  filter: blur(6px);
  transition: opacity 0.9s var(--ease), filter 0.9s var(--ease);
}
/* .armed, not .resolved: the mark no longer resolves unless you click it,
   so the strapline and the invitation belong to the drifting state */
.hero.armed .lockup-tagline {
  opacity: 1;
  filter: none;
  animation: broadcast 3s infinite ease-in-out 1.1s;
}
@keyframes broadcast {
  0%, 100% { opacity: 0.62; }
  50% { opacity: 1; }
}
/* THE INVITATION. Was a filled yellow pill reading "CLICK TO TUNE IN" — a
   button drawn on top of a composition that is already one big button, and
   the loudest object in a frame whose subject is the mark. It is now a word
   and a cursor: the smallest thing that still says "this is waiting for
   you". The caret does the attracting, so the type doesn't have to. */
.lockup-hint {
  position: absolute;
  left: 50%;
  top: 544px;
  transform: translateX(-50%);
  color: var(--yellow);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-indent: 0.16em;   /* tracking pads the right edge; recentre it */
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0;
  /* 0.25s of delay, not 0.5: this line now comes back after every breath,
     not just once on load, and the rest phase between two gathers is only
     ~1.3s. Half of that spent waiting to fade in read as a flicker. */
  transition: opacity 0.5s var(--ease) 0.25s;
}
.lockup-hint::after {
  content: "";
  display: inline-block;
  width: 0.5em;
  height: 1em;
  margin-left: 0.1em;
  vertical-align: -0.14em;
  background: currentColor;
  animation: caret 1.5s var(--ease-io) infinite;
}
@keyframes caret {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.12; }
}
@media (prefers-reduced-motion: reduce) {
  .lockup-hint::after { animation: none; }
}
.hero.armed .lockup-hint { opacity: 1; }
/* job done — and `animation: none` is not decoration here: the strapline's
   infinite `broadcast` keyframes own `opacity`, and a running animation beats
   a transition on the same property. Without this the line faded its blur in
   and then sat there at 0.62, which is the smear the client reported. */
/* .gathered is the top of the idle breath. The strapline and the invitation
   belong to the DRIFTING state: the moment the rings assemble, the outer
   ellipse runs straight through both lines. They clear for the hold and come
   back on the release, on the same beat they already use for the real lock. */
.hero.sound .lockup-hint,
.hero.gathered .lockup-hint,
.hero.departing .lockup-hint { opacity: 0; transition-delay: 0s; transition-duration: 0.35s; }
.hero.sound .lockup-tagline,
.hero.gathered .lockup-tagline,
.hero.departing .lockup-tagline {
  animation: none;
  opacity: 0;
  filter: blur(4px);
  transition-duration: 0.4s;
}
/* Tuned in, the room is the point and the furniture goes: the bottom blur
   band, the scrim and the standing copy all clear out so YouTube's own
   controls are reachable at the foot of the frame. */
.hero.sound .hero-scrim { opacity: 0; }
.hero.sound .hero-blur { opacity: 0; }
.hero.sound .hero-inner { opacity: 0; }
.hero.sound .lockup-hit { display: none; }

/* The "Sound off" pill lived here. Removed 2026-08-25: tuning in now hands
   the visitor YouTube's own controls, which carry a mute button — ours was a
   second, worse copy of it, parked in the middle of the frame. */

/* clicks land here, not on the hero — once the video iframe exists it swallows
   every click that reaches it (cross-origin iframes don't bubble) */
.lockup-hit {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -54%);
  width: min(78vw, 820px);
  height: min(58vh, 460px);
  z-index: 3;
  background: none;
  border: 0;
  cursor: pointer;
}
.lockup-hit:focus-visible { outline: 2px dashed var(--yellow); outline-offset: -30px; }

/* THE FLIGHT PASSES OVER THE BAR, NOT UNDER IT.
   .lockup-fly sits at z-index 2 inside .hero, and .site-head is fixed at 60,
   so the mark spent its whole departure sliding UNDERNEATH the header it was
   flying into — it vanished behind the bar and reappeared as the <img>. This
   competes with the bar directly (.hero is `position: relative` with no
   z-index, so it opens no stacking context), and it is raised only while
   something is in flight: at rest the bar belongs on top of the page. */
.hero.sound .lockup-fly,
.hero.departing .lockup-fly { z-index: 61; }

/* NOT .wrap. The standing copy is the only thing at the foot of a
   full-bleed frame, and centring it in a 1200px measure parked it a long way
   in from the edge on a wide screen — it read as floating rather than
   anchored. It now takes the HEADER's padding, so the room line sits on the
   same left rule as the wordmark above it and the frame breathes. */
.hero-inner {
  position: relative;
  z-index: 4;
  width: 100%;
  padding: 0 clamp(16px, 3vw, 36px) clamp(28px, 5vh, 56px);
  pointer-events: none;
  transition: opacity 0.6s var(--ease);
}
.hero-inner a, .hero-inner .live-next { pointer-events: auto; }
.hero-eyebrow { color: var(--ink); margin-bottom: 14px; }
.hero-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 34px;
  color: var(--dim);
}
.hero-meta a { color: var(--dim); }
.hero-meta .live-next { color: var(--yellow); }

/* ---------- the consent gate ----------
   Bottom-right, balancing the standing copy bottom-left. z-index 5 clears
   .lockup-hit (3) and .hero-inner (4) — the full-bleed hit target would
   otherwise swallow every click meant for the button and the policy link. */
.hero-consent {
  position: absolute;
  z-index: 5;
  right: clamp(16px, 3vw, 36px);
  bottom: clamp(28px, 5vh, 56px);
  width: min(370px, calc(100vw - 32px));
  display: grid;
  gap: 12px;
  padding: 20px 22px;
  background: rgba(26, 26, 26, 0.92);
  backdrop-filter: blur(10px);
  border: 1px solid var(--hair);
  border-radius: 8px;
}
.hero-consent .label { color: var(--yellow); }
.hero-consent-body { color: var(--dim); font-size: 13px; line-height: 1.55; }
.hero-consent-act { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 18px; }
.hero-consent-more {
  color: var(--dim); font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; text-decoration: underline; text-underline-offset: 3px;
}
.hero-consent-more:hover { color: var(--ink); }
/* it has done its job the moment the room opens, and it leaves on the same
   beat as the hint and the strapline rather than blinking out */
.hero-consent { transition: opacity 0.35s var(--ease); }
.hero.sound .hero-consent,
.hero.departing .hero-consent { opacity: 0; pointer-events: none; }
/* Narrow frames: the standing copy owns the bottom-left corner outright, so
   the panel stops competing for it and takes the top of the frame instead. */
@media (max-width: 860px) {
  .hero-consent {
    top: 78px; bottom: auto;
    left: 16px; right: 16px; width: auto;
  }
}

/* The scroll cue lived here: the word "Scroll" set vertically with a hairline
   falling away beneath it on a 2.6s loop. It was nicely made and it was still
   a scroll cue, which taste.md refuses outright — the visitor is looking at a
   full-viewport hero with a video behind it and knows what to do. Deleting it
   also takes one infinite animation off the hero's frame budget. */

/* ---------- RECENTLY ON AIR — the log ----------
   Was THE DIAL: one 60vw card at a time on a scroll-snap track, under a
   frequency ruler with a needle and a "TRANSMISSION 03 / 10" readout. The
   ruler went because it was decoration pretending to be an instrument — it
   reported a position in a list you could already see — and the one-card
   carousel went with it, because a carousel that shows one thing is a worse
   way to browse ten than showing ten.

   What replaces it is a broadcast log: the most recent set large on the
   left, the rest as a dense column beside it. That says "this is the latest,
   and here is what came before" in the layout itself, which is what the
   section is for, and it fits ten sets in less height than the dial fitted
   one. */

.onair {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: clamp(22px, 3vw, 44px);
  align-items: start;
}
@media (max-width: 900px) { .onair { grid-template-columns: 1fr; } }
/* Nine rows stack ~240px taller than one 16:9 lead plus its caption, which
   left a hole under the lead. The way into the archive lives in it — the
   link was sitting alone under the whole section anyway. */
.onair-main { display: grid; gap: clamp(24px, 4vw, 38px); align-content: start; }

.onair-card { text-decoration: none; color: inherit; display: block; }
.onair-frame {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  background: #000;
  border: 1px solid var(--hair);
  overflow: hidden;
}
.onair-frame img {
  width: 100%; height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.06);   /* brand B&W treatment */
  transition: filter 0.5s var(--ease), transform 0.9s var(--ease);
}

/* THE LEAD — the newest set, and the only element in the section that is
   allowed to be in colour at rest. "Keep one element in colour" is the brand
   book's own rule and this is the one that has earned it. */
.onair-lead .onair-frame img { filter: grayscale(0) contrast(1.02); }
.onair-lead .onair-meta { padding-top: 16px; }
.onair-lead .onair-name { font-family: var(--display); font-weight: 900; font-size: clamp(20px, 2.4vw, 28px); letter-spacing: -0.02em; color: #e9e9e9; }

.onair-live {
  position: absolute;
  left: 12px; top: 12px;
  padding: 5px 10px;
  border-radius: 999px;
  background: var(--yellow);
  color: #1a1a1a;
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.onair-meta { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; }
.onair-name { display: block; font-weight: 700; font-size: 15px; color: #e4e4e4; }
.onair-tag { display: block; font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim); margin-top: 4px; }
.onair-date { font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; color: var(--yellow); white-space: nowrap; }

/* THE REST — rows, not thumbnails-with-captions. Each carries a small 16:9
   still so the column still reads as video, but the row is the unit: it
   scans as a list of dates, which is what a log is. */
.onair-rest { display: flex; flex-direction: column; }
.onair-row {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr);   /* still + the meta line */
  gap: 14px;
  align-items: center;
  padding: 9px 0;
  border-bottom: 1px solid var(--hair);
  text-decoration: none;
}
.onair-rest .onair-row:first-child { border-top: 1px solid var(--hair); }
.onair-row .onair-frame { border: 0; }
.onair-row .onair-name { font-size: 14px; }
.onair-row .onair-tag { margin-top: 2px; }
/* the date is the first thing to go when the row runs out of width — the
   name is what you are scanning for */
@media (max-width: 620px) {
  .onair-row { grid-template-columns: 76px minmax(0, 1fr); }
  .onair-row .onair-date { display: none; }
}


/* ---------- reels (archive grids) ---------- */

.reel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
  gap: clamp(18px, 2.5vw, 30px);
}
.reel { text-decoration: none; color: inherit; display: block; }
.reel-frame {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  background: #000;
  border: 1px solid var(--hair);
  overflow: hidden;
}
.reel-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.06);
  transition: filter 0.4s var(--ease), transform 0.6s var(--ease);
}
.reel-play {
  position: absolute;
  right: 10px;
  bottom: 10px;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: rgba(26, 26, 26, 0.75);
  opacity: 0;
  transition: opacity var(--t) var(--ease);
}
.reel-play::after {
  content: "";
  position: absolute;
  left: 13px; top: 10px;
  border-left: 10px solid var(--yellow);
  border-top: 7px solid transparent;
  border-bottom: 7px solid transparent;
}
.reel-meta { display: block; padding: 12px 2px 0; }
.reel-name { display: block; font-weight: 700; font-size: 15px; color: #e4e4e4; }
.reel-tag { display: block; font-family: var(--mono); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--dim); margin-top: 4px; }

/* schedule rows (upcoming tab — no video to thumbnail) */
.row {
  display: grid;
  grid-template-columns: 110px 1fr auto;
  gap: 8px 26px;
  align-items: baseline;
  padding: 20px 0;
  border-bottom: 1px solid var(--hair);
  text-decoration: none;
}
.row .when { font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; color: var(--yellow); }
.row .who { font-weight: 700; color: #e4e4e4; }
.row .what { font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--dim); display: flex; align-items: center; gap: 8px; }
/* the room, in the site's one room token — yellow Kreuzberg, orange Mitte,
   the same pill the flyers carried */
.row .what .pill { flex: none; }
.row:hover .who { color: var(--yellow); }
@media (max-width: 520px) {
  .row { grid-template-columns: 1fr auto; }
  .row .when { grid-column: 1 / -1; }
}

/* tabs + filters */
.tabs { display: flex; gap: 10px; margin-bottom: 28px; }
.tab, .filter {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  background: none;
  border: 1px solid var(--edge);
  color: var(--ink);
  padding: 9px 18px;
  border-radius: 999px;
  cursor: pointer;
  transition:
    background-color var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease),
    transform var(--t-fast) var(--ease-snap);
}
.tab:hover, .filter:hover { border-color: var(--yellow); color: var(--yellow); }
.tab:active, .filter:active { transform: scale(0.96); }
.tab.active, .filter.active { background: var(--yellow); border-color: var(--yellow); color: #1a1a1a; }
.tab.active:hover, .filter.active:hover { color: #1a1a1a; }
.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 34px; }

/* ---------- buttons + CTA ---------- */

.btn {
  display: inline-block;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 15px 30px;
  border-radius: 999px;
  border: 1px solid transparent;
  cursor: pointer;
  /* named properties, not `all`: `all` also animates layout properties the
     moment anyone adds one, which is how buttons start reflowing on hover */
  transition:
    background-color var(--t) var(--ease),
    color var(--t) var(--ease),
    border-color var(--t) var(--ease),
    box-shadow var(--t) var(--ease),
    transform var(--t) var(--ease-snap);
}
.btn:active { transform: translate3d(0, 0, 0); transition-duration: 0.06s; }
.btn-solid { background: var(--orange); color: #1a1a1a; }
.btn-solid:hover { background: var(--yellow); color: #1a1a1a; box-shadow: 0 12px 28px -10px rgba(255, 214, 10, 0.55); }
.btn-ghost { border-color: var(--edge); color: var(--ink); background: none; }
.btn-ghost:hover { border-color: var(--yellow); color: var(--yellow); box-shadow: 0 12px 28px -14px rgba(255, 214, 10, 0.4); }

.cta-band {
  background: var(--stage);
  padding: clamp(80px, 11vw, 150px) 0;
  position: relative;
  overflow: hidden;
}
.cta-band::before {
  content: "";
  position: absolute;
  right: -14%;
  top: -30%;
  width: 58%;
  aspect-ratio: 1;
  background: url("../assets/photos/glyph-soft.png") no-repeat center / contain;
  mix-blend-mode: screen;
  opacity: 0.3;
  pointer-events: none;
}
.cta-link {
  font-size: clamp(38px, 7vw, 88px);
  letter-spacing: -0.05em;   /* the largest type on the site, so the tightest */
  text-decoration: none;
  display: inline-block;
  /* ROOM FOR THE ARROW TO MOVE INTO. The arrow's ink ends exactly on this
     element's right edge, and the reveal below puts a mask on it — whose
     `mask-clip` defaults to `border-box`, so ANYTHING painted outside the
     box is clipped no matter how wide mask-size is. Sliding the arrow 16px
     on hover therefore sheared the point off its head.
     Padding rather than `mask-clip: no-clip`: same result, and it does not
     depend on a value Safari still doesn't take. */
  padding-right: 26px;
}
.cta-link .cta-arrow { color: var(--acc); transition: transform var(--t) var(--ease); display: inline-block; }

/* ---------- fused frame — circle ∪ rectangle image motif ---------- */

.fuse {
  position: relative;
  display: inline-block;
}
.fuse img { filter: grayscale(1) contrast(1.06); }
.fuse::after {
  content: "";
  position: absolute;
  left: -9%;
  top: -14%;
  width: 42%;
  aspect-ratio: 322/141;
  border: 2px solid var(--acc);
  border-radius: 50%;
  pointer-events: none;
}

/* ---------- AMT page: arches, levels, ritual ---------- */

/* the whole opening section is one viewport tall so the arch band (and the
   tower) are on screen the instant the page loads — no scroll required */
.nest-hero {
  position: relative;
  overflow: hidden;
  min-height: 100svh;
  display: flex;
  /* `safe` matters here: the section clips (the tower overflows the skyline
     box upward and must not spill past the section), and plain `center` on a
     too-tall item overflows BOTH ends — so on a short laptop the heading
     would get cropped off the top. `safe` falls back to start alignment the
     moment the content stops fitting; browsers that don't know the keyword
     drop to `stretch`, which lands in the same place. */
  align-items: safe center;
  padding-top: calc(var(--head-h) + clamp(20px, 3vw, 40px));
  padding-bottom: clamp(20px, 3vw, 40px);
}
.nest-hero .wrap { position: relative; z-index: 1; width: 100%; display: flex; flex-direction: column; }

/* ---- the skyline: viaduct + Fernsehturm as ONE line drawing ----
   Same hairline weight, same colour, one shared ground line. The tower
   rises from behind the arches (its base masked out where they meet) so
   the two read as a single elevation, not a decal parked beside one. */
.nest-skyline {
  position: relative;
  margin-top: clamp(30px, 5vw, 58px);
  border-bottom: 1.5px solid var(--hair);   /* the ground both stand on */
}
/* The tower STANDS ON the same ground line as the arches (bottom: 0) rather
   than being parked above them, and is scaled off the arch band: 2.86 × the
   arch height, so the sightline reads as a real elevation. The bottom ~third
   — everything level with the viaduct — is masked away, which is what makes
   it sit BEHIND the arches instead of in front of them. One hairline lighter
   than the arches for the same reason: distance. */
.nest-tower {
  position: absolute;
  right: clamp(28px, 11vw, 190px);
  bottom: 0;
  height: clamp(258px, 40vw, 486px);
  width: auto;
  color: rgba(204, 204, 204, 0.1);
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to top, transparent 0 30%, #000 47%);
  mask-image: linear-gradient(to top, transparent 0 30%, #000 47%);
}
.nest-tower .tip { color: var(--yellow); }   /* the antenna is broadcasting too */
@media (max-width: 620px) {
  .nest-tower { right: 14px; height: 250px; }
}

/* AMT Club credit — their real mark (assets/amt-logo.svg, from
   club-amt.berlin) linking back to them. Sits on a hairline like a
   caption rather than floating as a badge. */
.nest-amt {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 16px;
  margin-top: clamp(26px, 4vw, 40px);
  padding-bottom: 10px;
  border-bottom: 1px solid var(--hair);
  text-decoration: none;
  transition: border-color var(--t-fast) var(--ease);
}
.nest-amt:hover { border-color: var(--yellow); }
.nest-amt-label {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--dim);
}
.nest-amt-logo { height: 26px; width: auto; color: var(--ink); transition: color var(--t-fast) var(--ease); }
.nest-amt:hover .nest-amt-logo { color: var(--yellow); }
.nest-amt-go { font-size: 13px; color: var(--dim); transition: color var(--t-fast) var(--ease); }
.nest-amt:hover .nest-amt-go { color: var(--yellow); }

.arch-band {
  display: flex;
  gap: clamp(10px, 1.6vw, 22px);
  align-items: flex-end;
  height: clamp(90px, 14vw, 170px);
}
.arch {
  flex: 1;
  height: 100%;
  border: 1.5px solid var(--hair);
  border-bottom: 0;
  border-radius: 999px 999px 0 0;
}
.arch.lit { border-color: var(--yellow); box-shadow: 0 0 24px rgba(255, 214, 10, 0.12) inset; position: relative; }
.arch.lit::after {
  content: "spätkauf";
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--yellow);
  white-space: nowrap;
}

.ritual { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: clamp(20px, 3vw, 40px); counter-reset: rit; }
.rit {
  border: 1px solid var(--hair);
  padding: clamp(22px, 3vw, 34px);
  position: relative;
  counter-increment: rit;
}
.rit::before {
  content: "0" counter(rit);
  display: inline-grid;
  place-items: center;
  min-width: 58px;
  height: 40px;
  border: 1.5px solid var(--acc);
  border-radius: 50%;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--acc);
  margin-bottom: 18px;
}
.rit h3 { font-size: 18px; font-weight: 700; color: #e4e4e4; margin-bottom: 8px; }
.rit p { color: var(--ink); font-size: 15px; }

/* ==========================================================================
   THE DESCENT — the two rooms
   Both rooms have the same shape (street → the way in → the floor the signal
   comes from), so the section IS that descent, drawn twice. It replaced two
   bordered cards with an illustration each: the cards read as generic panels,
   and the elevation drawings said nothing the sentence beside them didn't.

   The staircase is built out of the rows themselves, not laid over them. Each
   step owns one corner — a tread across the top (::before) and a riser down
   its full height (::after) — and `--k`, set per <li> in the markup, offsets
   both by one tread width. Step k's riser lands exactly where step k+1's
   tread begins, so the corners chain into one continuous flight. Because the
   riser is `top:0; bottom:0` rather than a fixed height, a step can be any
   height its text needs and the line still closes.

   Full width and stacked, NOT two columns: a staircase has to travel sideways
   about as far as it drops, and half a page of column doesn't give it the
   room — at column width the same construction reads as a ladder.
   ========================================================================== */

.rooms { display: grid; gap: clamp(44px, 6.5vw, 88px); }
.room { border-top: 1px solid var(--hair); padding-top: clamp(24px, 3.2vw, 38px); }
.room-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px clamp(20px, 3vw, 40px);
}
.room-name {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(28px, 4.4vw, 52px);
  letter-spacing: -0.026em;
  line-height: 0.95;
  color: #e4e4e4;
}
.room-where { display: flex; align-items: center; color: var(--dim); }
.room-where .dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--dim);
  margin-right: 9px;
  flex: none;
}

.descent {
  /* the tread is the unit everything else is measured in: the horizontal
     travel per step, and therefore the indent of that step's text */
  --tread: clamp(26px, 7vw, 104px);
  list-style: none;
  margin-top: clamp(26px, 3.4vw, 44px);
}
.step {
  --line: var(--hair);
  position: relative;
  min-height: clamp(92px, 11vw, 136px);
  padding: 15px 0 clamp(22px, 2.6vw, 34px)
           calc((var(--k) + 1) * var(--tread) + clamp(14px, 1.8vw, 26px));
}
.step:last-child { padding-bottom: 0; }
/* the tread — you land on it and walk right to the next drop */
.step::before {
  content: "";
  position: absolute;
  top: 0;
  left: calc(var(--k) * var(--tread));
  width: var(--tread);
  border-top: 1.5px solid var(--line);
  transform-origin: left center;
}
/* the riser — straight down the full height of the row it opens */
.step::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc((var(--k) + 1) * var(--tread));
  border-left: 1.5px solid var(--line);
  transform-origin: center top;
}
/* the last flight lights up: the step down into the room the broadcast comes
   from is the one piece of the drawing allowed to hold colour — the same rule
   as the viaduct's single lit arch */
.step-signal { --line: var(--yellow); }

.step-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 3px clamp(12px, 2vw, 22px);
}
.step-name {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(17px, 2.1vw, 24px);
  letter-spacing: -0.008em;
  line-height: 1.1;
  color: #e9e9e9;
}
.step-note {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dim);
}
.step-signal .step-name,
.step-signal .step-note { color: var(--yellow); }
/* --dim carries the label; --ink carries the sentence. Both clear AA now
   (see the token block), so this is a hierarchy choice, not a contrast one. */
.step-desc { color: var(--ink); font-size: 15px; margin-top: 8px; max-width: 62ch; }

.room .nest-amt { margin-top: clamp(26px, 3.4vw, 40px); }

.coords {
  font-family: var(--mono);
  font-size: clamp(12px, 1.4vw, 14px);
  letter-spacing: 0.18em;
  color: var(--dim);
  text-transform: uppercase;
}
.coords b { color: var(--yellow); font-weight: 600; }

/* ---------- forms ---------- */

.form-grid { display: grid; gap: 18px; max-width: 760px; }
.form-row { display: grid; gap: 18px; grid-template-columns: 1fr; }
@media (min-width: 640px) { .form-row.cols-2 { grid-template-columns: 1fr 1fr; } .form-row.cols-3 { grid-template-columns: 1fr 1fr 1fr; } }

.field label {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 8px;
}
.field label em { color: var(--yellow); font-style: normal; } /* required / hint */
.field input, .field textarea, .field select {
  width: 100%;
  background: var(--well);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 15px;
  padding: 13px 16px;
  border-radius: 4px;
  transition: border-color var(--t-fast) var(--ease);
}
.field input:focus, .field textarea:focus, .field select:focus {
  outline: none;
  border-color: var(--yellow);
}
.field textarea { min-height: 120px; resize: vertical; }
.field input[type="date"] { color-scheme: dark; }
.field-hint { font-size: 12.5px; color: var(--dim); margin-top: 6px; }

/* radio / checkbox pills */
.pills { display: flex; flex-wrap: wrap; gap: 10px; }
.pills label {
  display: inline-flex;
  align-items: center;
  /* The radio inside is position:absolute. Without a positioned label it
     resolved against the initial containing block instead, landing one full
     viewport-width to the right and stretching the document 424px wider than
     the screen on both apply forms. Invisible (opacity:0) and masked by
     body{overflow-x:hidden}, so it only showed up as a scrollWidth that never
     matched clientWidth. */
  position: relative;
  border: 1px solid var(--edge);
  border-radius: 999px;
  padding: 10px 20px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink);
  cursor: pointer;
  transition:
    background-color var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease),
    transform var(--t-fast) var(--ease-snap);
}
.pills label:hover { border-color: var(--yellow); color: var(--yellow); }
.pills label:active { transform: scale(0.96); }
.pills label:has(input:checked):hover { color: #1a1a1a; }
.pills input { position: absolute; opacity: 0; pointer-events: none; }
.pills label:has(input:checked) { background: var(--yellow); border-color: var(--yellow); color: #1a1a1a; }
.pills label:has(input:focus-visible) { outline: 2px dashed var(--yellow); outline-offset: 2px; }

/* ---------- THE PICKER — the question that opens a form ----------
   These were .pills: 11px tracked caps in a 36px lozenge, the same control
   the genre checkboxes use. But this one is not a tag, it is the FORK — it
   decides which of two forms you are about to fill in, and it is the first
   thing on the page. It gets the weight that matches the decision. */
.picker {
  display: grid;
  /* 260, not 230: at 230 a 760px form fits three across, which broke the
     four-option picker into a 3 + 1 orphan. Two columns on both forms. */
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 12px;
}
.picker-card {
  position: relative;
  display: block;
  padding: 20px 22px;
  border: 1px solid var(--edge);
  border-radius: 4px;
  background: var(--well);
  cursor: pointer;
  transition: border-color var(--t) var(--ease), background-color var(--t) var(--ease), transform var(--t) var(--ease);
}
.picker-card input { position: absolute; opacity: 0; pointer-events: none; }
/* `.field label` is tracked uppercase and this card IS a <label>, so both the
   title and its note inherited caps — a sentence turned into shouting. Needs
   to out-specify `.field label` (0,1,1), hence the tag on the class. */
.field label.picker-card { text-transform: none; letter-spacing: normal; }
.picker-title {
  display: block;
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(17px, 1.7vw, 21px);
  letter-spacing: -0.018em;
  color: #e9e9e9;
}
.picker-note { display: block; margin-top: 6px; font-size: 13px; line-height: 1.45; color: var(--dim); }
.picker-card:has(input:checked) {
  border-color: var(--acc);
  background: color-mix(in srgb, var(--acc) 12%, var(--well));
}
.picker-card:has(input:checked) .picker-title { color: var(--acc); }
.picker-card:has(input:focus-visible) { outline: 2px dashed var(--acc); outline-offset: 3px; }

.form-branch { display: none; }
.form-branch.visible { display: grid; gap: 18px; }

/* ---------- the foot of a form ----------
   Ships hidden. There is nothing to send until a branch is chosen, and a
   submit button under a form that does not exist yet is an invitation to
   click it and be told off. setBranch() reveals it.
   (The [hidden] override this used to need is global now, up by the reset.) */
[data-submit-wrap] { display: grid; gap: 10px; justify-items: start; margin-top: 6px; }

/* Greyed while the form is incomplete — but NOT `disabled`. A disabled
   button cannot be clicked, so it cannot tell you what is missing, and the
   visitor is left guessing which of fourteen fields it is unhappy about.
   aria-disabled says the same thing to assistive tech, keeps the button
   focusable and clickable, and the click runs validation and walks you to
   the first problem. */
.btn[aria-disabled="true"] {
  background: var(--well);
  color: var(--dim);
  border-color: var(--edge);
  box-shadow: none;
}
.btn[aria-disabled="true"]:hover { background: var(--well); color: var(--dim); border-color: var(--edge); box-shadow: none; transform: none; }

/* ---------- inline field errors ----------
   reportValidity() shows ONE native bubble, on one field, and it vanishes on
   the next click — on a form this long that is a scavenger hunt. Every
   invalid field says what is wrong, under itself, and stays saying it. */
.field-error {
  display: none;
  margin-top: 6px;
  font-size: 12.5px;
  /* #ff5a4d measures 3.9:1 on --bg — fine for the BORDER (WCAG wants 3:1 for
     a control boundary) and short of the 4.5:1 this sentence needs. Two
     values, two jobs. */
  color: #ff7a68;
}
.field.invalid .field-error { display: block; }
.field.invalid input,
.field.invalid textarea,
.field.invalid select { border-color: #ff5a4d; }
.field.invalid > label { color: #ff8a80; }

.form-success,
.form-error {
  display: none;
  border: 1px solid var(--yellow);
  color: var(--yellow);
  padding: 18px 22px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.form-error { border-color: #ff5a4d; color: #ff5a4d; }

/* apply landing choice cards */
/* ---------- the other door ----------
   Everything that is not a stream request, on one quiet line at the foot of
   the form. It replaced a whole chooser page whose only content was the fork
   — and it is deliberately not a button: the overwhelming majority of people
   here want the form above it, and this is for the few who don't. */
.other-ask {
  margin-top: clamp(40px, 6vw, 68px);
  padding-top: clamp(22px, 3vw, 30px);
  border-top: 1px solid var(--hair);
  max-width: 760px;
}
.other-ask a {
  display: inline-block;
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(16px, 1.7vw, 20px);
  letter-spacing: -0.018em;
  color: var(--ink);
  text-decoration: none;
  transition: color var(--t) var(--ease);
}
.other-ask a:hover { color: var(--yellow); }
.other-ask > span { display: block; margin-top: 6px; font-size: 13.5px; color: var(--dim); }
.other-ask a span { display: inline; }   /* the arrow rides the line, not below it */

/* ---------- 404 ----------
   Four ways out rather than one "go home" button: somebody who lands here
   wanted a specific thing, and the four things this site does are a better
   guess at it than the front door. Deliberately reuses the card language
   from .choice-card rather than inventing a fifth kind of box. */
.nf-grid {
  display: grid;
  gap: clamp(14px, 2vw, 20px);
  /* 280 + a 760 cap gives two columns, so four cards read as 2x2 rather
     than three across with an orphan underneath */
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  max-width: 760px;
}
.nf-card {
  display: block;
  padding: clamp(20px, 2.4vw, 28px);
  border: 1px solid var(--hair);
  border-radius: 4px;
  background: var(--well);
  text-decoration: none;
  transition: border-color var(--t) var(--ease), transform var(--t) var(--ease), background-color var(--t) var(--ease);
}
.nf-card h2 {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(19px, 2vw, 24px);
  letter-spacing: -0.018em;
  color: #e9e9e9;
  margin: 10px 0 8px;
}
.nf-card p { font-size: 13.5px; color: var(--dim); }
.nf-card .label { margin-bottom: 0; }
.nf-card .go {
  display: inline-block;
  margin-top: 16px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--acc);
}

.choice-grid { display: grid; gap: clamp(18px, 3vw, 32px); grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); }
.choice-card {
  display: block;
  border: 1px solid var(--edge);
  padding: clamp(28px, 4vw, 46px);
  text-decoration: none;
  position: relative;
  overflow: hidden;
  transition: border-color var(--t) var(--ease);
}
.choice-card::before {
  content: "";
  position: absolute;
  right: -22%;
  bottom: -34%;
  width: 70%;
  aspect-ratio: 322/141;
  border: 2px solid var(--hair);
  border-radius: 50%;
  transition: border-color 0.3s, transform 0.5s var(--ease);
}
.choice-card h3 { font-family: var(--display); font-weight: 900; font-size: clamp(22px, 2.6vw, 30px); letter-spacing: -0.012em; color: #e4e4e4; margin: 14px 0 10px; }
.choice-card p { color: var(--dim); font-size: 15px; max-width: 40ch; }
.choice-card .go { display: inline-block; margin-top: 22px; color: var(--acc); font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; }

/* branch choice buttons (in-page switcher, ported from v7) */
.choice {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  background: none;
  border: 1px solid var(--edge);
  color: var(--ink);
  padding: 12px 24px;
  border-radius: 999px;
  cursor: pointer;
  transition:
    background-color var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease),
    transform var(--t-fast) var(--ease-snap);
}
.choice:hover { border-color: var(--yellow); color: var(--yellow); }
.choice:active { transform: scale(0.96); }
.choice.active { background: var(--yellow); border-color: var(--yellow); color: #1a1a1a; }
.choice.active:hover { color: #1a1a1a; }

/* ---------- about ----------
   The press fact table and the brand-asset plate lived here; both were cut
   with the press page. Prose is capped by .lede on its own — this only
   carries the imprint block, which is not a .lede. */

.about-body { max-width: 660px; }
.about-body a { color: var(--yellow); }

/* ---------- players / consent / pip (machinery, proven in v7) ---------- */

.player-head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px 24px;
  padding: 16px 0;
  color: var(--dim);
}
.player-head .on { color: var(--yellow); }

.player-slot { position: relative; aspect-ratio: 16 / 9; }
.player {
  position: absolute;
  inset: 0;
  background: #000;
  border: 1px solid var(--hair);
}
.player iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.consent {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 18px;
  padding: clamp(20px, 4vw, 44px);
  background:
    radial-gradient(120% 90% at 50% 110%, rgba(255, 214, 10, 0.07), transparent 60%),
    linear-gradient(180deg, transparent 20%, rgba(26, 26, 26, 0.92));
}
.consent p { max-width: 54ch; color: var(--ink); font-size: 14px; }
.consent .btn { align-self: flex-start; }

.yt-poster { position: absolute; inset: 0; display: block; }
.yt-poster img { width: 100%; height: 100%; object-fit: cover; opacity: 0.6; transition: opacity var(--t) var(--ease); }
.yt-poster span {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  background: var(--orange);
  color: #1a1a1a;
  padding: 14px 26px;
  border-radius: 999px;
  white-space: nowrap;
}
.yt-poster small {
  position: absolute;
  left: 0; right: 0; bottom: 14px;
  text-align: center;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  color: var(--dim);
}

.pip-close {
  display: none;
  position: absolute;
  top: -30px;
  right: 0;
  z-index: 2;
  background: rgba(26, 26, 26, 0.9);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 6px 12px;
  cursor: pointer;
}


/* ---------- footer ---------- */

.site-foot {
  border-top: 1px solid var(--hair);
  background: var(--stage);
  padding: clamp(56px, 8vw, 110px) 0 40px;
  overflow: hidden;
}
.foot-giant { width: 100%; margin-bottom: clamp(40px, 6vw, 80px); }
.foot-giant img { width: 100%; height: auto; opacity: 0.9; }
.foot-grid {
  display: grid;
  gap: clamp(28px, 5vw, 70px);
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  margin-bottom: clamp(40px, 6vw, 70px);
}
.foot-nav { display: grid; gap: 9px; }
.foot-nav a { text-decoration: none; color: var(--ink); font-size: 15px; }
.foot-nav a:hover { color: var(--yellow); }
.foot-addr { color: var(--dim); font-size: 15px; font-style: normal; }
.fine {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 26px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--dim);
  border-top: 1px solid var(--hair);
  padding-top: 26px;
}
.fine a { color: var(--dim); }
.fine a:hover { color: var(--yellow); }
.fine .spacer { flex: 1; }

/* ==========================================================================
   MICRO-INTERACTIONS
   Everything a pointer can touch answers on the same two curves. The rule
   throughout: hover states move a few pixels and change one colour — never
   more. The site's loud motion belongs to the mark and the stream.
   ========================================================================== */

:focus-visible { outline: 2px solid var(--yellow); outline-offset: 3px; }
/* the real control is the label; a ring on the hidden input paints nowhere */
.pills input:focus-visible { outline: none; }

/* ritual cards — the numbered disc is the logo motif, so it turns */
.rit {
  transition:
    border-color var(--t) var(--ease),
    background-color var(--t) var(--ease),
    transform var(--t) var(--ease);
}

/* archive tiles */
.reel-frame { transition: border-color var(--t) var(--ease), box-shadow var(--t) var(--ease); }
.reel-name { transition: color var(--t) var(--ease); }
.rit::before { transition: transform 0.55s var(--ease-snap), box-shadow 0.55s var(--ease); }
.choice-card { transition: border-color var(--t) var(--ease), transform var(--t) var(--ease); }
.foot-nav a { justify-self: start; transition: color var(--t) var(--ease), transform var(--t) var(--ease); }


/* ---- POINTER-ONLY HOVER ----
   Every hover that MOVES something, reveals something, or recolours a photo
   lives in here. A tap on a touchscreen fires a hover that then STAYS fired
   until you tap somewhere else, so ungated these left a lifted card, a stuck
   "Rewatch" overlay and a photo in colour behind every tapped tile. Press
   feedback (:active) and focus (:focus-visible) are deliberately outside:
   both are correct on touch. Colour-only hovers are outside too — a stuck
   yellow link is not worth a media query. */
@media (hover: hover) and (pointer: fine) {
  .site-nav a:hover { color: var(--yellow); }
  .site-nav a:hover::after { transform: scaleX(1); transform-origin: 0 50%; }

  .reel:hover .reel-frame img { filter: grayscale(0); transform: scale(1.035); }
  .reel:hover .reel-play { opacity: 1; }
  .reel:hover .reel-frame {
    border-color: color-mix(in srgb, var(--yellow) 45%, transparent);
    box-shadow: 0 20px 44px -22px rgba(0, 0, 0, 0.95);
  }
  .reel:hover .reel-name { color: var(--yellow); }

  .onair-card:hover .onair-frame img { filter: grayscale(0) contrast(1.02); transform: scale(1.03); }
  .onair-card:hover .onair-name { color: var(--yellow); }
  .flyer:hover .flyer-art img, .flyer:hover .flyer-art-gen { transform: scale(1.045); }
  .flyer:hover .flyer-go { color: #fff; }

  .nf-card:hover { border-color: var(--yellow); background: var(--panel); transform: translate3d(0, -3px, 0); }
  .btn:hover { transform: translate3d(0, -2px, 0); }
  .cta-link:hover .cta-arrow { transform: translateX(16px); }

  .choice-card:hover { border-color: var(--acc); transform: translate3d(0, -4px, 0); }
  .choice-card:hover::before { border-color: var(--acc); transform: scale(1.06); }

  .rit:hover {
    border-color: color-mix(in srgb, var(--acc) 40%, transparent);
    background: color-mix(in srgb, var(--acc) 3%, transparent);
    transform: translate3d(0, -4px, 0);
  }
  .rit:hover::before { transform: rotate(-8deg) scale(1.07); box-shadow: 0 0 24px -5px currentColor; }

  .foot-nav a:hover { color: var(--yellow); transform: translate3d(5px, 0, 0); }
  .yt-poster:hover img { opacity: 0.8; }
  .cs-pin:hover { background: var(--yellow); transform: translateX(-50%) rotate(45deg) scale(1.35); }
}

/* ==========================================================================
   REVEAL ON SCROLL
   One base move for everything, then three overrides where a component has a
   better entrance of its own. `--d` is the stagger step: bindReveal() sets it
   from each element's index among its revealing siblings, so grids deal
   themselves out instead of landing as a single slab.
   ========================================================================== */

.js .rv {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  filter: blur(6px);
  transition:
    opacity 0.85s var(--ease),
    transform 0.95s var(--ease),
    filter 0.7s var(--ease);
  transition-delay: calc(var(--d, 0) * 75ms);
}
.js .rv.in { opacity: 1; transform: none; filter: none; }

/* photography settles in rather than arriving already at rest */
.js .rv:not(.in) .reel-frame img,
.js .rv:not(.in) .onair-frame img,
.js .rv:not(.in) .flyer-art img { transform: scale(1.08); }

/* ---- section headers are a sequence, not a block ----
   The chip swings in, the title wipes out from behind it (the negative
   margin already overlaps them, so the chip reads as the title's origin),
   and the index lands last. The row opts out of the base move so the parts
   are the only thing travelling. */
.js .sec-mark.rv { opacity: 1; transform: none; filter: none; }

.js .sec-mark.rv img {
  opacity: 0;
  transform: scale(0.7) rotate(-14deg);
  transition: opacity 0.6s var(--ease), transform 0.85s var(--ease-snap);
}
.js .sec-mark.rv.in img { opacity: 0.9; transform: none; }

.js .sec-mark.rv .sec-title {
  clip-path: inset(-30% 100% -30% 0);
  transform: translate3d(-10px, 0, 0);
  transition: clip-path 0.95s var(--ease) 0.14s, transform 0.95s var(--ease) 0.14s;
}
.js .sec-mark.rv.in .sec-title { clip-path: inset(-30% -10% -30% 0); transform: none; }

.js .sec-mark.rv .sec-idx { opacity: 0; transition: opacity 0.7s var(--ease) 0.38s; }
.js .sec-mark.rv.in .sec-idx { opacity: 1; }

/* ---- display type rises out of its own line ----
   The mask opens downward while the words travel up: a mask reveal without
   wrapping every line in a spare element.

   This MUST be a mask, never clip-path. IntersectionObserver computes its
   intersection rect after clipping, so an element that clip-paths itself to
   zero area reports ratio 0, never trips the threshold, never receives .in —
   and therefore stays clipped forever. That deadlock is exactly what a
   clip-path version of this rule caused: every display headline on the site
   was invisible. Masking is a paint operation and leaves the rect alone.

   135% tall so descenders stay covered.
   NOTE the width is NOT what protects the CTA arrow on hover — `mask-clip`
   defaults to `border-box` and clips paint to the box whatever mask-size
   says. The arrow is kept inside the box by padding on .cta-link instead. */
.js .display.rv, .js .cta-link.rv {
  opacity: 1;
  filter: none;
  transform: translate3d(0, 0.4em, 0);
  -webkit-mask-image: linear-gradient(to bottom, #000, #000);
  mask-image: linear-gradient(to bottom, #000, #000);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 150% 0%;
  mask-size: 150% 0%;
  transition:
    transform 1.05s var(--ease),
    -webkit-mask-size 1.05s var(--ease),
    mask-size 1.05s var(--ease);
  transition-delay: calc(var(--d, 0) * 75ms);
}
.js .display.rv.in, .js .cta-link.rv.in {
  transform: none;
  -webkit-mask-size: 150% 135%;
  mask-size: 150% 135%;
}

/* ---- the viaduct builds itself ----
   Arches rise out of the ground line they stand on, left to right, and the
   tower — further away — arrives slower and later. Same opt-out as the
   section header: the container holds still, the parts move. */
.js .nest-skyline.rv { opacity: 1; transform: none; filter: none; }
.arch-band { overflow: hidden; }          /* the arches start below the line */
.js .nest-skyline.rv .arch {
  opacity: 0;
  transform: translate3d(0, 100%, 0);
  transition: transform 1s var(--ease), opacity 0.6s var(--ease), border-color var(--t) var(--ease);
}
.js .nest-skyline.rv.in .arch { opacity: 1; transform: none; }
.js .nest-skyline.rv .arch:nth-child(2) { transition-delay: 0.07s; }
.js .nest-skyline.rv .arch:nth-child(3) { transition-delay: 0.14s; }
.js .nest-skyline.rv .arch:nth-child(4) { transition-delay: 0.21s; }
.js .nest-skyline.rv .arch:nth-child(5) { transition-delay: 0.28s; }
.js .nest-skyline.rv .arch:nth-child(6) { transition-delay: 0.35s; }
.js .nest-skyline.rv .nest-tower {
  opacity: 0;
  transform: translate3d(0, 30px, 0);
  transition: opacity 1.5s var(--ease) 0.3s, transform 1.7s var(--ease) 0.3s;
}
.js .nest-skyline.rv.in .nest-tower { opacity: 1; transform: none; }

/* ==========================================================================
   SCROLL-DRIVEN DECORATION
   Progressive enhancement, and deliberately limited to elements nothing can
   hover: a scroll-timeline animation outranks normal declarations, so putting
   one on anything with a :hover transform would silently kill the hover.
   Browsers without view() timelines simply get the static composition.
   ========================================================================== */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* the watermark glyph drifts against the scroll — the page moves past a
       mark that is standing still behind it */
    .sec-watermark::before, .cta-band::before {
      animation: wmDrift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
    @keyframes wmDrift {
      from { transform: translate3d(0, 8%, 0) rotate(-3deg) scale(1.05); }
      to   { transform: translate3d(0, -8%, 0) rotate(3deg) scale(1.05); }
    }

    /* ---- the stairs draw themselves as you go down the page ----
       Each step carries its OWN view timeline, so the flight draws top to
       bottom simply because the lower steps enter the viewport later — no
       shared clock and nothing to sequence by hand. Within a step the tread
       runs out first and the riser drops after it, which is the order you'd
       draw it in. The timeline is named on .step rather than left as `view()`
       because view() on a pseudo-element resolves against the pseudo's own
       box: the tread is a 1.5px-tall line, so its entry range would be a
       couple of pixels of scroll and the whole thing would snap. */
    .step { view-timeline-name: --step; }
    .step::before {
      animation: treadDraw linear both;
      animation-timeline: --step;
      animation-range: entry 22% entry 78%;
    }
    .step::after {
      animation: riserDraw linear both;
      animation-timeline: --step;
      animation-range: entry 68% cover 16%;
    }
    @keyframes treadDraw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
    @keyframes riserDraw { from { transform: scaleY(0); } to { transform: scaleY(1); } }

    /* the giant footer wordmark surfaces as you reach the bottom */
    .foot-giant img {
      animation: footRise linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 55%;
    }
    @keyframes footRise {
      from { opacity: 0; transform: translate3d(0, 44px, 0) scale(0.965); }
      to   { opacity: 0.9; transform: none; }
    }
  }
}

/* ---------- reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .js .rv,
  .js .sec-mark.rv img,
  .js .sec-mark.rv .sec-title,
  .js .sec-mark.rv .sec-idx,
  .js .display.rv, .js .cta-link.rv,
  .js .nest-skyline.rv .arch,
  .js .nest-skyline.rv .nest-tower {
    opacity: 1;
    transform: none;
    filter: none;
    clip-path: none;
    transition: none;
  }
  /* drop the mask outright rather than resize it — nothing should be
     mid-reveal when motion is off */
  .js .display.rv, .js .cta-link.rv { -webkit-mask-image: none; mask-image: none; }
  .js .rv:not(.in) .reel-frame img,
  .js .rv:not(.in) .onair-frame img,
  .js .rv:not(.in) .flyer-art img { transform: none; }
  .lockup-tagline, .lockup-hint::after, .on-air .dot,
  .hero.resolved .lockup-fly, .hero.resolved .lockup-svg { animation: none; }
  .strip { scroll-behavior: auto; }
  /* Was `* { transition-duration: 0.01ms !important }`, which took out the
     opacity and colour changes as well — and those are the ones that carry
     comprehension. A tab that swaps state with no transition at all is harder
     to follow, not easier. Everything that MOVES or changes shape goes;
     opacity, colour and shadow stay, quickly. */
  * {
    transition-property: opacity, color, background-color, border-color, box-shadow !important;
    transition-duration: 0.15s !important;
  }
}

/* ---------- print (press page mostly) ---------- */

@media print {
  .site-head, .site-foot, .stage-video { display: none; }
  body { background: #fff; color: #333; }
}

/* ==========================================================================
   CLIP STUDIO — the private post-stream link an artist gets by email.
   Reuses .player / .player-slot / .consent as-is for the video box (the only
   new piece is that this page drives it via the YouTube IFrame API instead
   of app.js's plain-iframe loadPlayer(), since it needs getCurrentTime()).
   ========================================================================== */

.cs-head { padding-top: calc(var(--head-h) + clamp(48px, 7vw, 88px)); background: var(--stage); }
.cs-greet { max-width: 640px; }
.cs-greet b { color: #e9e9e9; }

.cs-transport {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px 20px;
  margin-top: 18px;
}
.cs-time { color: var(--yellow); min-width: 6ch; }
.cs-manual { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.cs-manual-input {
  width: 104px;
  background: var(--well);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 12px;
  padding: 9px 10px;
  border-radius: 4px;
}
.cs-manual-input:focus { outline: none; border-color: var(--yellow); }
.cs-mark[disabled] { opacity: 0.4; cursor: not-allowed; }

.cs-ruler {
  position: relative;
  height: 40px;
  margin-top: 26px;
  background: repeating-linear-gradient(to right, var(--hair) 0 1px, transparent 1px calc(100% / 24));
  border-top: 1px solid var(--hair);
  border-bottom: 1px solid var(--hair);
}
.cs-playhead {
  position: absolute;
  top: 0; bottom: 0;
  left: 0;
  width: 2px;
  background: var(--yellow);
  box-shadow: 0 0 10px rgba(255, 214, 10, 0.6);
  pointer-events: none;
}
.cs-pin {
  position: absolute;
  top: 5px;
  width: 12px; height: 12px;
  transform: translateX(-50%) rotate(45deg);
  background: var(--orange);
  border: 1.5px solid var(--stage);
  cursor: pointer;
  padding: 0;
  transition: transform var(--t-fast) var(--ease-snap), background-color var(--t-fast) var(--ease);
}
.cs-pin:focus-visible { background: var(--yellow); transform: translateX(-50%) rotate(45deg) scale(1.35); }

.cs-clips { border: 1px solid var(--hair); margin-top: 14px; }
.cs-row {
  display: grid;
  grid-template-columns: auto auto 1fr auto auto;
  align-items: center;
  gap: 14px;
  padding: 14px 18px;
}
.cs-row + .cs-row { border-top: 1px solid var(--hair); }
.cs-row-time {
  background: none; border: 1px solid var(--edge); color: var(--yellow);
  font-family: var(--mono); font-size: 12px; padding: 7px 12px; border-radius: 999px; cursor: pointer;
  transition: border-color var(--t-fast) var(--ease);
}
.cs-row-time:hover { border-color: var(--yellow); }
.cs-row-nudge { display: flex; gap: 4px; }
.cs-row-nudge button {
  width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--edge);
  background: none; color: var(--dim); cursor: pointer; font-family: var(--mono); line-height: 1;
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.cs-row-nudge button:hover { border-color: var(--yellow); color: var(--yellow); }
.cs-row-label {
  background: var(--well); border: 1px solid var(--edge); color: var(--ink);
  font-family: var(--sans); font-size: 14px; padding: 9px 12px; border-radius: 4px; width: 100%;
}
.cs-row-label:focus { outline: none; border-color: var(--yellow); }
.cs-row-end { color: var(--dim); font-size: 11px; font-family: var(--mono); white-space: nowrap; }
.cs-row-remove {
  background: none; border: 0; color: var(--dim); font-size: 17px; cursor: pointer; line-height: 1; padding: 4px;
}
.cs-row-remove:hover { color: var(--orange); }

.cs-empty { color: var(--dim); padding: 34px 18px; text-align: center; font-size: 14px; }
.cs-counter { margin-top: 14px; color: var(--dim); }
.cs-counter b { color: var(--yellow); }
.cs-counter.full b { color: var(--orange); }

.cs-upload { margin-top: 10px; }
.cs-upload-btn {
  display: inline-block;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
  border: 1px dashed var(--edge); color: var(--dim); padding: 10px 18px; border-radius: 999px; cursor: pointer;
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.cs-upload-btn:hover { border-color: var(--yellow); color: var(--yellow); }

.cs-success { text-align: center; padding: clamp(50px, 8vw, 90px) 0; }
.cs-success img { width: 64px; margin: 0 auto 22px; opacity: 0.9; }
.cs-success h2 { font-family: var(--display); font-weight: 900; font-size: clamp(28px, 4vw, 44px); letter-spacing: -0.026em; color: var(--yellow); margin-bottom: 14px; }
.cs-success p { color: var(--ink); max-width: 46ch; margin: 0 auto; }

@media (max-width: 700px) {
  .cs-row {
    grid-template-columns: auto 1fr auto;
    grid-template-areas: "time label remove" "nudge label ." "end end end";
    row-gap: 8px;
  }
  .cs-row-time { grid-area: time; }
  .cs-row-nudge { grid-area: nudge; }
  .cs-row-label { grid-area: label; }
  .cs-row-end { grid-area: end; }
  .cs-row-remove { grid-area: remove; }
  .cs-manual { margin-left: 0; width: 100%; }
  .cs-mark { width: 100%; order: -1; }
}

/* ==========================================================================
   CREW LINEUP ROWS (/apply) + NIGHT SHEET (/night/{token})
   ========================================================================== */

.lineup-rows { display: grid; gap: 8px; }
.lineup-row { display: grid; grid-template-columns: 112px 1fr 116px 40px; gap: 8px; align-items: center; }
.lineup-row input,
.lineup-row select {
  width: 100%; background: var(--well); border: 1px solid var(--edge); color: var(--ink);
  font-family: var(--sans); font-size: 15px; padding: 13px 16px; border-radius: 4px;
}
.lineup-row select {
  font-family: var(--mono); letter-spacing: 0.08em;
  padding: 13px 10px; appearance: none; cursor: pointer;
}
.lineup-row select[name="set_start"] { font-size: 13px; }
.lineup-row select[name="set_format"] { font-size: 12px; }
.lineup-remove {
  height: 100%; background: none; border: 1px solid var(--edge); border-radius: 4px;
  color: var(--dim); font-size: 18px; line-height: 1; cursor: pointer;
}
.lineup-remove:hover { color: var(--ink); border-color: var(--ink); }
.lineup-add { margin-top: 10px; }

.night-slots { display: grid; gap: 10px; }
.night-slot {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  background: var(--panel); border: 1px solid var(--hair); border-radius: 6px;
  padding: 16px 18px;
}
.night-slot.has-pic { border-color: rgba(255, 214, 10, 0.35); }
.night-slot.ns-error { border-color: #ff5a4d; }
.ns-meta { min-width: 0; }
.ns-time { font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; color: var(--dim); }
.ns-artist { font-weight: 600; margin: 2px 0; overflow-wrap: anywhere; }
.ns-status { font-size: 13px; color: var(--dim); }
.night-slot.has-pic .ns-status { color: var(--yellow); }
.night-slot.ns-error .ns-status { color: #ff5a4d; }
.ns-up { flex: none; cursor: pointer; }
@media (max-width: 560px) {
  .night-slot { flex-direction: column; align-items: stretch; text-align: left; }
  /* three columns won't take a fourth at 320px: `order` sends the format
     past the remove button so auto-placement drops it onto a second row,
     where it spans the whole width. Scoped by name — the start hour is a
     <select> too now, and it belongs in column one with the artist beside
     it, not thrown onto its own line as well. */
  .lineup-row { grid-template-columns: 84px 1fr 36px; }
  .lineup-row select[name="set_format"] { order: 1; grid-column: 1 / -1; }
}
