@font-face {
  font-family: "Inter";
  src: url("/assets/fonts/InterVariable-4.1-subset.woff2") format("woff2");
  font-style: normal;
  font-weight: 400 850;
  font-display: swap;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

:root {
  /* TYPOGRAPHY FLOOR ---------------------------------------------------------------------
     Nine elements on this page sat between 8.8px and 9.9px, every one of them a FIXED rem, so
     a 1440px desktop rendered them at exactly the size a 375px phone did. The floor is the
     first number in each clamp; the vw term is what gives a desktop back some of the headroom
     it was never getting.

     THE FLOOR IS THE TARGET, NOT THE MINIMUM. The first pass set each floor at the smallest
     size still defensible and let a desktop climb away from it -- which is backwards for a page
     whose entire audience is on a phone. A 375px phone was getting 12px labels and 10px stamps
     while a 1470px laptop got 13.4 and 11.1. These floors are now the sizes the phone is meant
     to have, and the vw term only adds the small amount a wide screen can carry: every token
     lands within ~1.5px of its floor at 1440, so desktop stays roughly where it was while the
     small end comes up to meet it.

     The minimums are in rem, not px, deliberately: someone who has raised their browser's base
     size for accessibility should get a bigger floor, not be pinned to ours. At the default
     16px root -- which is what the audit measured -- 0.875rem is 14px and 0.75rem is 12px.

       --fs-label         text that carries meaning. Never below 14px.
       --fs-label-strong  the 18+ consent label. A floor ABOVE the plain label, because it is
                          the one piece of small print on the page that exists to be read.
       --fs-stamp         decoration only (REC, PLAYER ONE). Never below 12px.
       --fs-social        the social links -- meaningful, and also a tap target.
       --fs-body-sm       the connection line. Held at 13px through the first re-pitch, which
                          left it the smallest meaningful text on the page -- below the label
                          floor it sits next to. Same 14px floor as the rest now.
       --fs-bio           the bio, which was small AND dimmed. Both are fixed below. */
  --fs-label: clamp(0.875rem, 0.844rem + 0.132vw, 1rem);
  --fs-label-strong: clamp(0.9375rem, 0.906rem + 0.132vw, 1.0625rem);
  --fs-stamp: clamp(0.75rem, 0.734rem + 0.066vw, 0.8125rem);
  --fs-social: clamp(0.875rem, 0.848rem + 0.113vw, 1rem);
  --fs-body-sm: clamp(0.875rem, 0.85rem + 0.1vw, 0.95rem);
  --fs-bio: clamp(0.96875rem, 0.947rem + 0.094vw, 1.0625rem);

  /* The smallest thing a finger is expected to hit. WCAG 2.5.5 AAA. */
  --tap: 44px;

  color-scheme: dark;
  font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
}

html {
  min-height: 100%;
  background: var(--bg, #07080c);
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  overflow-x: hidden;
  color: var(--ink, #f5f5f7);
  background:
    radial-gradient(circle at 82% 8%, rgba(57, 197, 187, 0.13), transparent 30rem),
    radial-gradient(circle at 8% 82%, rgba(255, 102, 170, 0.08), transparent 26rem),
    var(--bg, #07080c);
}

a {
  color: inherit;
}

a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--cyan, #39c5bb);
  outline-offset: 4px;
}

.texture,
.texture::before,
.texture::after {
  position: fixed;
  inset: 0;
  pointer-events: none;
}

.texture {
  z-index: 0;
  overflow: hidden;
  opacity: 0.52;
}

.texture::before {
  content: "";
  background:
    repeating-linear-gradient(0deg, transparent 0 7px, rgba(255, 255, 255, 0.012) 7px 8px),
    repeating-linear-gradient(90deg, transparent 0 11px, rgba(57, 197, 187, 0.012) 11px 12px);
  mix-blend-mode: screen;
}

.texture::after {
  content: "";
  inset: -30%;
  background: conic-gradient(from 120deg at 50% 50%, transparent, rgba(255, 102, 170, 0.05), transparent 28%, rgba(57, 197, 187, 0.045), transparent 56%);
  animation: texture-drift 24s linear infinite;
}

main,
body > footer {
  position: relative;
  z-index: 1;
  width: min(100% - 2rem, 1240px);
  margin-inline: auto;
}

.connection i,
.rec i {
  display: inline-block;
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  background: #7bf1c5;
  box-shadow: 0 0 0 4px rgba(123, 241, 197, 0.08);
}

.hero-stage {
  position: relative;
  display: grid;
  gap: 0.95rem;
  padding: 0.9rem 0 1.6rem;
}

.portrait {
  position: relative;
  overflow: hidden;
  aspect-ratio: 6 / 5;
  background: #11131a;
  box-shadow: inset 5px 0 0 rgba(255, 102, 170, 0.22), inset -5px 0 0 rgba(57, 197, 187, 0.22);
  animation: lift-in 800ms 80ms ease both;
}

.portrait::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border: 1px solid rgba(245, 245, 247, 0.18);
  background: linear-gradient(180deg, transparent 52%, rgba(7, 8, 12, 0.75));
  pointer-events: none;
}

.hero {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  filter: saturate(0.9) contrast(1.04);
}

.hero-slide {
  opacity: 0;
  transition: opacity 700ms ease;
}

.hero-slide.is-active {
  opacity: 1;
}

.status {
  position: absolute;
  top: 0.8rem;
  left: 0.8rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.62rem;
  border: 1px solid rgba(245, 245, 247, 0.22);
  color: rgba(245, 245, 247, 0.8);
  background: rgba(7, 8, 12, 0.72);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-label);
  letter-spacing: 0.06em;
}

.portrait-stamp {
  position: absolute;
  right: 0.8rem;
  bottom: 0.8rem;
  left: 0.8rem;
  z-index: 2;
  display: flex;
  justify-content: space-between;
  color: rgba(245, 245, 247, 0.78);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-stamp);
  letter-spacing: 0.13em;
}

.portrait-stamp span:first-child {
  color: var(--pink, #ff66aa);
}

.hero-copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* As a grid item this defaults to min-width:auto, i.e. its own min-content width, and that
     sizes the TRACK. .ticker-inline holds `width: max-content; white-space: nowrap`, so at 375px
     the single mobile column measured 1451.77px, .hero-copy rendered 1451.8px inside a 343px
     stage, and the whole page scrolled sideways. Measured, not reasoned: min-width:0 on the
     TICKER changes nothing -- the clamp has to be here, on the grid item. The >=760px rule below
     already sets min-height:0 on this element for the same family of reasons. */
  min-width: 0;
  padding: 0.2rem 0 0;
  animation: lift-in 800ms 170ms ease both;
}

.kicker,
.social-dock > span,
.player-cam p span {
  color: var(--cyan, #39c5bb);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-label);
  font-weight: 760;
  letter-spacing: 0.16em;
}

.kicker {
  margin: 0 0 0.65rem;
}

.hero-copy h1 {
  display: flex;
  flex-direction: column;
  margin: 0;
  color: var(--ink, #f5f5f7);
  font-size: clamp(4.15rem, 22vw, 5.3rem);
  font-weight: 850;
  letter-spacing: -0.1em;
  line-height: 0.77;
}

.hero-copy h1 .outline {
  margin-left: 0.6em;
  color: transparent;
  -webkit-text-stroke: 1px rgba(57, 197, 187, 0.88);
}

.tagline {
  max-width: 36ch;
  margin: 0.9rem 0 0;
  color: rgba(245, 245, 247, 0.9);
  font-size: clamp(1.05rem, 2.4vw, 1.38rem);
  line-height: 1.45;
}

.bio {
  max-width: 43ch;
  margin: 1rem 0 0;
  /* Was 0.82rem at 54% opacity -- small AND dimmed, which compounds. Both moved. */
  color: rgba(245, 245, 247, 0.72);
  font-size: var(--fs-bio);
  line-height: 1.55;
}

.connection {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  /* Was 0.8rem 0 0 at 78% opacity, third of four lines under the CTA. With the bio gone it is the
     only sentence in that column, and it is the one that says she answers -- Shane: "That shows
     that she replies." It takes the space the bio left rather than the column simply closing up. */
  margin: 1.15rem 0 0;
  color: rgba(245, 245, 247, 0.92);
  font-size: var(--fs-body-sm);
}


.primary-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  width: 100%;
  max-width: 520px;
  min-height: 4.35rem;
  margin-top: 1.05rem;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--pink, #ff66aa);
  color: #09090d;
  background: var(--pink, #ff66aa);
  text-decoration: none;
  box-shadow: 5px 5px 0 rgba(57, 197, 187, 0.34);
  transition: transform 180ms ease, box-shadow 180ms ease, background 180ms ease;
}

.primary-link:hover {
  color: #09090d;
  background: #ff7ab5;
  box-shadow: 2px 2px 0 rgba(57, 197, 187, 0.5);
  transform: translate(3px, 3px);
}

.primary-link span {
  /* Sized off its eyebrow, not off the viewport. --fs-label-strong now runs 15 -> 16.4px, and at
     the old 15.7 -> 17.9px this line was only ~1.5px bigger than the small print inside it. It
     holds a ~1.25x step at both ends instead. */
  font-size: clamp(1.16rem, 4.75vw, 1.28rem);
  font-weight: 780;
}

.primary-link small {
  display: block;
  margin-bottom: 0.2rem;
  /* THE PRIORITY. This is the consent framing on the outbound click; it exists to be read,
     and at 9.3px it was the least readable text on the page. Contrast lifted with it. */
  color: rgba(9, 9, 13, 0.82);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-label-strong);
  font-weight: 700;
  letter-spacing: 0.12em;
}

.primary-link b {
  color: #09090d;
  font-size: 1.25rem;
  font-weight: 400;
}

/* The cue between the connection line and the socials. Drawn from one empty <i>: two borders on a
   square, rotated 45deg, which is a chevron. No file, no request, nothing for script-src to refuse.

   The nudge is the CTA arrow's, borrowed on purpose -- same ease-out-cubic, same "moves a few
   pixels along its own diagonal" idea -- so the page has one motion vocabulary rather than two.
   translate(3px, 3px) is applied AFTER the 45deg rotation, so it resolves to straight down on
   screen: the local x and y axes point down-right and down-left, and their sum is vertical.

   Motion lives only inside `prefers-reduced-motion: no-preference`. It is deliberately NOT also
   listed in the `reduce` block below -- an override there could never fire, and a guard that
   cannot fire is worse than no guard, because the next reader trusts it. */
.scroll-cue {
  display: flex;
  align-items: center;
  height: 1.05rem;
  margin: 0.75rem 0 0;
  padding-left: 0.28rem; /* centres the rotated square on the dot above it, not on its own box */
}

.scroll-cue i {
  display: block;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 1.5px solid rgba(57, 197, 187, 0.9);
  border-bottom: 1.5px solid rgba(57, 197, 187, 0.9);
  border-radius: 0 0 2px 0;
  opacity: 0.5;
  transform: rotate(45deg);
  filter: drop-shadow(0 0 5px rgba(57, 197, 187, 0.28));
}

@media (prefers-reduced-motion: no-preference) {
  .scroll-cue i {
    animation: cue-drift 2.6s cubic-bezier(0.33, 1, 0.68, 1) infinite;
  }
}

.social-dock {
  display: flex;
  /* flex-start, not center, and this only matters once there is more than one handle. The nav
     wraps; the label does not. Centred, five handles put ELSEWHERE 103px down the block, floating
     beside the FOURTH one -- measured at 390px, and it reads as a bug rather than a label. At the
     top it reads as a heading for the list at any length. One handle renders identically either
     way, so this costs nothing today and is correct on the day the other four arrive. */
  align-items: flex-start;
  gap: 1rem;
  margin-top: 0.9rem;
}

.social-dock > span {
  color: rgba(245, 245, 247, 0.55);
  white-space: nowrap;
}

.social-dock nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 0.75rem;
}

.social {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0;
  border-bottom: 1px solid rgba(245, 245, 247, 0.18);
  color: rgba(245, 245, 247, 0.82);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-social);
  text-decoration: none;
  transition: border-color 180ms ease, color 180ms ease;
}

.social::after {
  content: "";
  position: absolute;
  top: 50%;
  right: 0;
  left: 0;
  height: var(--tap);
  transform: translateY(-50%);
}

.social:hover {
  border-color: var(--cyan, #39c5bb);
  color: var(--cyan, #39c5bb);
}

.social svg {
  width: 0.9rem;
  height: 0.9rem;
  fill: currentColor;
}

.player-cam {
  position: relative;
  /* The card is queried on its OWN width, not the viewport's. It has to be: between 760px and
     ~815px the hero grid squeezes this card to 283-290px -- NARROWER than it gets on a 375px
     phone -- because the copy column next door holds a 360px floor. A viewport media query would
     have made the 768px case worse while looking correct at 375. */
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  /* Vertically centre the caption against the photo. flex-end pinned it to the photo's bottom
     edge, which measured 65px below the photo's centre at 1440px -- the caption read as sitting
     in the bottom of the empty space rather than in the middle of it. Raised three times as a
     bug before the axis was measured rather than eyeballed.

     The photo is the tallest item, so it defines the line's height: centring moves the caption
     and cannot move the photo. Below the stacking threshold each item is alone on its own
     full-width line, so this has no effect there -- checked at 375px, not assumed. */
  align-items: center;
  max-width: 480px;
  margin-top: 0.2rem;
  margin-left: 0;
  padding: 0.85rem;
  border: 1px solid rgba(245, 245, 247, 0.15);
  background: rgba(15, 17, 24, 0.66);
  animation: lift-in 850ms 320ms ease both;
}

.player-cam > img,
.player-cam > .player-media {
  /* Wants 9.5rem, gives it up to the caption before the caption is allowed to wrap. The floor
     stops the photo collapsing to a sliver on a 320px phone. */
  flex: 0 1 9.5rem;
  min-width: 4.75rem;
}

.player-cam > img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  filter: saturate(0.72) contrast(1.08);
}

/* Side-by-side cannot give the photo 9.5rem AND the caption a single line below this width, so
   stacking gives the photo the whole card width instead -- bigger than either arrangement, and
   the caption gets a full-width line.

   THE THRESHOLD IS DERIVED, NOT CHOSEN. It is photo (9.5rem = 152px) + gap (1rem = 16px) +
   the caption's own width, and the caption's width is what changed: "yes, I actually play"
   measures 199.2px at its 23px ceiling, against 154px before. 152 + 16 + 199.2 = 367.2px, and
   24.5rem (392px) clears that by ~25px so the boundary is not decided by a rounding error.

   Leaving it at 19.5rem opened a third arrangement between the two: wide enough not to stack,
   too narrow to sit side by side, so the caption wrapped onto its own row and left a 152px
   photo pinned to the left of a wide empty gap. Every phone width lands in that band. */
@container (max-width: 24.5rem) {
  .player-cam > img,
  .player-cam > .player-media,
  .player-cam p {
    flex-basis: 100%;
  }
}

/* The gradient IS the loading state. The poster is deferred (server.js, motion.js), so from first
   paint until it lands this is the whole card -- and #11131a read as a hole where a photo failed.
   The two stops are the poster's own top and bottom halves, sampled at 1x8 and then run through
   the same `saturate(0.72) contrast(1.08)` the photo below is drawn with, because the raw pixels
   are not what a visitor sees: raw #a489ac/#796052 draw as #87748e/#584a44.

   `aspect-ratio` was already here and is what keeps this honest -- the box is the poster's exact
   size before the poster exists, so nothing reflows when it arrives. */
.player-media {
  position: relative;
  overflow: hidden;
  width: 100%;
  aspect-ratio: 4 / 5;
  background: linear-gradient(180deg, #87748e 0%, #584a44 100%);
}

.player-media img,
.player-media video {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.72) contrast(1.08);
}

.player-media video {
  opacity: 0;
  transition: opacity 220ms ease;
}

.player-media video.is-playing {
  opacity: 1;
}

/* An <img> with no `src` paints its own alt text, which over the gradient is precisely the broken
   state this was supposed to avoid. Hidden until motion.js has both set the src AND had the load
   event back -- revealing on the src assignment alone would fade in an undecoded image and pop.
   The <noscript> copy ships with `is-loaded` already on it, since no script will ever add it. */
.player-media img.player-fallback {
  opacity: 0;
  transition: opacity 260ms ease;
}

.player-media img.player-fallback.is-loaded {
  opacity: 1;
}

.player-cam .rec {
  position: absolute;
  top: 0.95rem;
  left: 0.95rem;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  color: #ff8eae;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-stamp);
  letter-spacing: 0.1em;
}

.player-cam p {
  /* TWO axes, and only one of them was ever the complaint. text-align handles the HORIZONTAL
     and was already right -- measured at 1440px, the caption's centre and the empty space beside
     the photo agree to within a pixel. Shane's "not centered", asked three times, was the
     VERTICAL: the block sat 65px below the photo's centre, at the bottom of the space.
     That axis is the container's align-items, not anything in this rule. */
  text-align: center;
  /* No min-width override, deliberately. A flex item's automatic minimum size is its min-content
     width, and because the caption below is `nowrap` that min-content width IS the whole string:
     the browser will shrink the photo rather than break the line. The old rule was a max-width in
     ch that HOPED the column was wide enough; this one cannot be wrong. */
  flex: 1 1 auto;
  /* No align-self. It used to say flex-end, which SILENTLY BEAT the container's align-items --
     changing the container alone moved nothing, which is the likeliest reason this survived two
     fixes. One source of truth for the vertical, and it is .player-cam. */
  margin: 0 0 0.25rem;
}

.player-cam p span {
  display: block;
  /* Overrides the shared .kicker/.social-dock size above: this label reads against a 22px
     caption, so it needs to be bigger than a label sitting on its own. */
  font-size: clamp(0.9375rem, 0.92rem + 0.06vw, 1rem);
  margin-bottom: 0.6rem;
}

.player-cam p strong {
  display: block;
  color: rgba(245, 245, 247, 0.9);
  /* "yes, I actually play" is 191.4px wide at 22px in Inter 700 -- measured, not estimated.
     Every arrangement below has to give the text column at least that much, or the nowrap
     below turns into an overflow instead of a wrap. */
  font-size: clamp(1.375rem, 1.353rem + 0.094vw, 1.4375rem);
  line-height: 1.06;
  white-space: nowrap;
}

.ticker {
  overflow: hidden;
  width: 100%;
  margin: 0;
  border-top: 1px solid rgba(245, 245, 247, 0.12);
  border-bottom: 1px solid rgba(245, 245, 247, 0.12);
  color: var(--pink, #ff66aa);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: clamp(0.6875rem, 0.66rem + 0.12vw, 0.8125rem);
  letter-spacing: 0.18em;
  white-space: nowrap;
}

/* MOBILE ONLY. Kai asked for the ticker under the OF button as a divider; below 760px this is
   the one that renders and .ticker-band is display:none. display:none rather than visibility or
   opacity on purpose -- it stops .ticker > div's infinite 24s animation dead, so the hidden copy
   costs no compositing. Margins: 1.4rem above the rule, and 0.35rem here plus .bio's own 1rem
   below it, which lands at 22.4px above / 21.6px below -- a divider, not a fourth paragraph. */
.ticker-inline {
  margin: 1.4rem 0 0.35rem;
}

/* Full-width band under both hero columns; see the comment on the node in views/index.html for
   why it cannot be the same node as .ticker-inline. Below 760px the inline copy replaces it. */
.ticker-band {
  display: none;
}

.ticker > div {
  display: flex;
  width: max-content;
  animation: ticker 24s linear infinite;
  will-change: transform;
}

.ticker-set {
  display: flex;
  flex: none;
  align-items: center;
  gap: 1.5rem;
  padding: 0.85rem 1.5rem 0.85rem 0;
}

.ticker b {
  color: var(--cyan, #39c5bb);
  font-weight: 400;
}

.signal-note {
  max-width: 720px;
  padding: 2.35rem 0 2.8rem;
  border-bottom: 1px solid rgba(245, 245, 247, 0.16);
}

/* MOTION, WITHOUT THE LIBRARY -----------------------------------------------------------------
   A fade-in and a 4px arrow nudge were the only two things on this page that pulled in Anime.js:
   116KB minimum, and because motion.js static-imported it, twenty sequential module requests had
   to resolve before the slideshow could even start -- 12.8s of that on a Slow 3G connection.
   Both effects are a transform and an opacity. Both are CSS.

   `.will-reveal` is added by motion.js rather than living in this file: if the script fails to
   load, the note has to stay readable rather than sit invisibly at opacity 0. */
.signal-note.will-reveal {
  opacity: 0;
  transform: translateY(22px);
}

.signal-note.will-reveal.is-revealed {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: no-preference) {
  .signal-note.will-reveal {
    /* cubic-bezier(0.33, 1, 0.68, 1) is ease-out-cubic -- the curve Anime.js's `out(3)` produced. */
    transition:
      opacity 720ms cubic-bezier(0.33, 1, 0.68, 1),
      transform 720ms cubic-bezier(0.33, 1, 0.68, 1);
  }
}

/* The `hover: hover` guard is new, and deliberate. The pointerenter/pointerleave pair this
   replaces never fired on touch; a bare :hover does, and would strand the arrow mid-nudge after
   a tap. Asymmetric durations are preserved: 220ms in, 260ms back. */
@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  [data-cta] b {
    transition: transform 260ms cubic-bezier(0.33, 1, 0.68, 1);
  }

  [data-cta]:hover b {
    transform: translate(4px, -4px);
    transition-duration: 220ms;
  }
}

.signal-kicker {
  margin: 0;
  color: var(--pink, #ff66aa);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
}

.signal-copy {
  max-width: 25ch;
  margin: 0.75rem 0 1rem;
  color: rgba(245, 245, 247, 0.9);
  font-size: clamp(1.35rem, 3.4vw, 2.25rem);
  font-weight: 650;
  letter-spacing: -0.04em;
  line-height: 1.08;
}

.signal-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem 1.2rem;
  color: rgba(245, 245, 247, 0.58);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.signal-meta span::before {
  content: "✦";
  margin-right: 0.45rem;
  color: var(--cyan, #39c5bb);
}

.signal-meta span {
  margin: 0;
}

body > footer {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.3rem 0 1.5rem;
  color: rgba(245, 245, 247, 0.62);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-label);
  letter-spacing: 0.08em;
}

body > footer p {
  display: none;
  margin: 0;
}

body > footer nav {
  display: flex;
  gap: 1rem;
}

body > footer a {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  text-decoration: none;
}

/* The footer row would otherwise grow by the full 44px of the links inside it. */
body > footer nav {
  margin-block: calc((var(--tap) - 1.4em) / -2);
}

.sticky-cta {
  position: fixed;
  right: 0.75rem;
  bottom: calc(0.75rem + env(safe-area-inset-bottom));
  left: 0.75rem;
  z-index: 20;
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0.7rem;
  align-items: center;
  min-height: 3.6rem;
  padding: 0.65rem 0.8rem;
  color: #09090d;
  background: var(--pink, #ff66aa);
  border: 1px solid #ff9ac7;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.42);
  font-size: 0.84rem;
  font-weight: 800;
  text-decoration: none;
  opacity: 0;
  pointer-events: none;
  transform: translateY(calc(100% + 2rem));
  transition: opacity 180ms ease, transform 220ms ease;
}

.sticky-cta small {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-label-strong);
  letter-spacing: 0.08em;
}

.sticky-cta b {
  font-size: 1.15rem;
  font-weight: 500;
}

.sticky-cta.is-visible {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}

/* DESKTOP-ONLY TYPE. Above 1304px `main` is pinned at 1240px, so every screen from a 13"
   laptop to a 5K renders the identical layout and type scale is the only variable left. Two
   tokens had stopped moving long before that point and were drifting out of proportion with
   the h1, which keeps growing to 1509px.

   Everything here is inside min-width:1024 so it cannot reach a phone. The floors are chosen
   against the values the shared clamps already produce at 1024, so crossing the boundary is a
   no-op for the tagline and a single +2px step for the CTA (see below). */
@media (min-width: 1024px) {
  /* Frozen at 22.08px since 920px wide while the h1 climbed to 160px: the ratio ran from 4.9x
     at 1024 to 7.25x at 1920, and the tagline stopped reading as the hero's second line and
     started reading as a caption under a poster. 1.38rem floor IS the value the shared clamp
     gives at 1024, so nothing moves at the boundary. */
  .tagline {
    font-size: clamp(1.38rem, 0.9rem + 0.75vw, 1.6rem);
  }

  /* The main line capped at 431px -- the earliest of any token on the page -- so every desktop
     showed the same 20.48px while its own eyebrow kept climbing to 17px, compressing the step
     from 1.27x to 1.20x. It also left the supporting sentence LARGER than the call to action,
     and the tagline lift above would have widened that to 25%.

     THE TWO CONSTRAINTS HERE CANNOT BOTH BE MET. Sitting at or above the tagline forces the
     eyebrow step to at least 25.2/16.4 = 1.54x at 1440; pinning the step to ~1.4x instead puts
     the main line at 23.0px, back under the tagline. The size relationship is the one that was
     wrong, so it wins: the step lands at 1.42x at 1024 and widens to 1.53x, recovering from
     1.20x rather than compressing further.

     This is the one value that steps at the boundary: 20.48 -> 22.5 at exactly 1024px. Made
     continuous it would sit under the tagline through 1400px, which is the defect being fixed. */
  .primary-link span {
    font-size: clamp(1.40625rem, 0.958rem + 0.7vw, 1.625rem);
  }
}

@media (min-width: 760px) {
  main,
  body > footer {
    width: min(100% - 4rem, 1240px);
  }

  .hero-stage {
    grid-template-columns: minmax(0, 1.04fr) minmax(360px, 0.96fr);
    grid-template-areas:
      "portrait copy"
      "player copy";
    column-gap: clamp(2.5rem, 7vw, 7rem);
    /* "Moved up": this row-gap IS the photo-to-card gap, and has been since height:100% made
       the portrait fill row 1. Measured constant at 20px across 760/768/800/830/900/1440/1920
       before this change, and re-measured constant at the new value after it. */
    row-gap: 0.8rem;
    padding-top: 2rem;
    padding-bottom: 3rem;
  }

  .portrait {
    grid-area: portrait;
    aspect-ratio: 4 / 3;
    /* LAYOUT: fill the row, don't just sit at the top of it. .hero-copy spans both rows, so
       when its content is taller than portrait + gap + player-cam the grid pushes that height
       into row 1. The portrait's aspect-ratio pins its own height, so the surplus opened up as
       dead space BELOW the photo and the player-cam drifted away from it -- 37px instead of the
       intended 20px at 760px wide, and unbounded as the copy grows. height:100% makes the photo
       absorb the surplus instead, so the card stays attached at every width. */
    height: 100%;
  }

  .hero-copy {
    grid-area: copy;
    min-height: 0;
    justify-content: flex-start;
    padding: 1.3rem 0 0;
  }

  .hero-copy h1 {
    font-size: clamp(5.5rem, 10.6vw, 10rem);
  }

  .player-cam {
    grid-area: player;
    z-index: 2;
    width: min(100%, 480px);
    /* Centred beneath the photo. Below ~900px the card is already as wide as the portrait, so
       this changes nothing there; above it the card was hanging off the left edge of a much
       wider photo. */
    margin: 0 auto;
    transform: none;
  }

  /* The two tickers swap. At this width .hero-copy is the RIGHT COLUMN of the grid, so the
     inline copy would be 360px at 768 and 546.8px at 1440 against the band's 704 and 1240 --
     a marquee the width of one column instead of a page-wide divider. Kai only looked at
     mobile; this keeps the desktop band exactly as it was. */
  .ticker-band {
    display: block;
  }

  .ticker-inline {
    display: none;
  }

  .sticky-cta {
    display: none;
  }

  body > footer p {
    display: block;
  }
}

@media (min-width: 1100px) {
  .hero-copy {
    padding-top: 2.2rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .texture::after,
  .portrait,
  .hero-copy,
  .player-cam,
  .hero-slide,
  .player-media video,
  .ticker > div {
    animation: none !important;
    transition: none !important;
  }

  .sticky-cta {
    transition: none;
  }
}

@keyframes cue-drift {
  0%, 70%, 100% { opacity: 0.42; transform: rotate(45deg) translate(0, 0); }
  35% { opacity: 1; transform: rotate(45deg) translate(3px, 3px); }
}

@keyframes lift-in {
  from { opacity: 0; transform: translateY(18px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes ticker {
  to { transform: translateX(-50%); }
}

@keyframes texture-drift {
  to { transform: rotate(360deg); }
}
