/* ============ Life's Work — Alex van Rooyen ============ */
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --ink: #dfe4ee;
  --whisper: #8b93a3;
  --faint: #565d6b;
  --glass-edge: rgba(190, 205, 235, .55);
  --glass-fill: rgba(150, 165, 195, .10);
  --serif: 'Gelasio', Georgia, 'Times New Roman', serif;
  --sans: 'Arimo', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
}

html, body {
  height: 100%;
  overflow: hidden;
  background: #070910;
  font-family: var(--sans);
  color: var(--ink);
  /* Mobile Safari inflates text it decides is body copy, which is a real
     difference between a handset and the same width emulated on a desktop:
     a card measured to fit comes out taller on the phone. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* ---------- scenes ---------- */
.scene {
  position: fixed;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 1.2s ease;
}
.scene.active { opacity: 1; pointer-events: auto; }

.plate, .plate-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* The portrait plates are 9:16, but phones are nearer 0.46, so cover fills the
   height and slices ~9% off each side. Alex and Chino stand left of centre, so
   a centred crop cuts the dog. Biasing the crop left keeps them whole; what it
   costs is a little more of the mountain's right flank, which reads as sky and
   rock rather than subject. */
@media (orientation: portrait) {
  .plate, .plate-video { object-position: 12% center; }
  /* Video 2 opens at this same 12% for an exact cut from video 1, then
     main.js pans it to 82% almost immediately -- while he's still walking
     and the source footage is already moving -- so it lands on 82% well
     before the sit, matching video 3/hub for the rest of the clip. Panned
     via transform (compositor-only, see main.js) rather than
     object-position, and timed against motion that's already happening
     rather than the still seated shot, both of which is what read as shaky
     when tried before. */
  #hub-plate, #hub-video { object-position: 82% center; }
  #walk-video {
    top: 0; left: 0; right: auto; bottom: auto; width: auto; max-width: none; height: 100%;
    will-change: transform;
  }
}
/* Past this aspect ratio (tablets, some foldables in portrait) the source is
   noticeably squarer than a phone, and the 12%/82% bias above no longer
   reserves enough width to keep Alex and Chino in frame -- cover would start
   cropping them instead of just the mountain. Contain guarantees the full
   frame is always visible; the night sky either side reads as letterboxing,
   not a bug. walk-video is excluded: main.js drives its width/transform
   directly for the pan and would just override this. */
@media (orientation: portrait) and (min-aspect-ratio: 3/4) {
  #landing-plate, #landing-video, #hub-plate, #hub-video, #seat-plate, #seat-video {
    object-fit: contain; object-position: center;
  }
}
.plate-video { opacity: 0; transition: opacity 1.5s ease; }
.plate-video.visible { opacity: 1; }

.plate { animation: hover-drift 14s ease-in-out infinite alternate; }
@keyframes hover-drift {
  from { transform: scale(1.02) translate(0, 0); }
  to   { transform: scale(1.045) translate(-0.6%, 0.5%); }
}

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

/* ---------- landing UI ---------- */
.identity {
  position: absolute;
  top: 4.5vh;
  left: 4vw;
  opacity: 0;
  animation: fade-in 2s ease 0.8s forwards;
}
.identity h1 {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(17px, 2vw, 24px); letter-spacing: .04em;
  text-shadow: 0 1px 10px rgba(0, 0, 0, .85), 0 0 3px rgba(0, 0, 0, .6);
}
.identity p {
  font-size: clamp(11px, 1.1vw, 13px);
  color: #dde3ef;
  letter-spacing: .08em; margin-top: 5px;
  text-shadow: 0 1px 8px rgba(0, 0, 0, .9), 0 0 2px rgba(0, 0, 0, .7);
}
.identity .thesis {
  color: #b9c1d1; font-style: italic;
  letter-spacing: .06em; margin-top: 3px;
  text-shadow: 0 1px 8px rgba(0, 0, 0, .9), 0 0 2px rgba(0, 0, 0, .7);
}

@keyframes fade-in { to { opacity: 1; } }
/* The hub's copy must not depend on the landing's load-time fade -- that runs
   0.8s after the document opens, long before anyone gets here, and if it is
   ever missed the name is simply invisible. The scene carries its own fade,
   so this just needs to be present. */
#scene-hub .identity { opacity: .96; animation: none; }

.cta {
  position: absolute;
  bottom: 7vh;
  left: 50%;
  transform: translateX(-50%);
  padding: 13px 34px;
  font-family: var(--serif);
  font-size: clamp(14px, 1.3vw, 17px);
  letter-spacing: .08em;
  /* Android resolves this serif stack to a wider face than iOS does, which
     was enough to break "Take a look." onto a second line. */
  white-space: nowrap;
  color: var(--ink);
  background: rgba(150, 165, 195, .08);
  border: 1px solid var(--glass-edge);
  border-radius: 30px;
  cursor: pointer;
  opacity: 0;
  animation: fade-in 2s ease 2.2s forwards;
  transition: background .4s, transform .25s, box-shadow .4s;
  backdrop-filter: blur(2px);
}
.cta:hover {
  background: rgba(170, 185, 215, .18);
  box-shadow: 0 0 26px rgba(160, 180, 220, .25);
  transform: translateX(-50%) scale(1.04);
}
.cta:active { transform: translateX(-50%) scale(0.97); }

.tagline-settled {
  position: absolute;
  top: 13vh;
  right: 6vw;
  text-align: right;
  font-family: var(--serif);
  font-size: clamp(12px, 1.35vw, 17px);
  letter-spacing: .22em;
  color: rgba(195, 202, 216, 0);
  line-height: 2.1;
  pointer-events: none;
}
.tagline-settled span { display: block; opacity: 0; transition: opacity 3s ease; }
.tagline-settled span.lit { opacity: .75; color: #c3cad8; text-shadow: 0 0 14px rgba(180,200,240,.35); }

/* ---------- hub: tiles ---------- */
.sky {
  position: absolute;
  top: 6vh;
  left: 50%;
  transform: translateX(-50%);
  width: min(92vw, 1100px);
  height: 44vh;
  perspective: 900px;
}
.tile {
  position: absolute;
  top: 50%;
  left: 50%;
  width: clamp(180px, 24vw, 300px);
  aspect-ratio: 3/2;
  transform-style: preserve-3d;
  transition: transform 1s cubic-bezier(.22, 1, .36, 1), opacity 1s;
  cursor: pointer;
}
.tile-inner {
  position: absolute; inset: 0;
  border: 1px solid var(--glass-edge);
  border-radius: 10px;
  background: linear-gradient(160deg, rgba(170,185,215,.16), rgba(120,135,165,.06));
  backdrop-filter: blur(3px);
  box-shadow: 0 0 30px rgba(140,160,210,.12), inset 0 0 30px rgba(160,180,220,.06);
  overflow: hidden;
  animation: tile-bob 7s ease-in-out infinite alternate;
}
.tile:nth-child(2) .tile-inner { animation-duration: 8.5s; }
.tile:nth-child(3) .tile-inner { animation-duration: 6.2s; }
@keyframes tile-bob { from { transform: translateY(-4px) rotate(-.4deg);} to { transform: translateY(5px) rotate(.5deg);} }

.tile-preview {
  position: absolute; inset: 9% 7% 34% 7%;
  border-radius: 8px;
  background: rgba(14, 19, 30, .62);
  border: 1px solid rgba(150,170,210,.18);
  display: flex; align-items: center; justify-content: center;
  gap: clamp(6px, 1vw, 14px);
  overflow: hidden;
}
/* the platform mark, so the tile is recognised before the label is read */
.tile-logo {
  width: 46%; max-height: 58%; object-fit: contain;
  display: block;
  filter: drop-shadow(0 2px 10px rgba(0,0,0,.6));
  transition: transform .45s cubic-bezier(.22,1,.36,1), opacity .45s;
}
.tile.focused .tile-logo { transform: scale(1.04); }
/* the Klaviyo wordmark is far wider than the other two marks (about 3.4:1), so
   it needs a touch less width once the open cue joins it on the front tile */
.tile[data-room="email"].focused .tile-logo { width: 40%; }
/* click cue — only on the focused, front tile */
/* Only the front tile shows the cue. It must leave the flow entirely on the
   others — at opacity 0 it still took up flex width and shoved their logo
   off-centre. */
.tile-preview::after {
  display: none;
  content: "open";
  font-size: clamp(10px, 1vw, 13px);
  letter-spacing: .28em;
  color: #eaf0fb;
  padding: 6px 20px;
  border: 1px solid rgba(200,215,245,.5);
  border-radius: 20px;
  background: rgba(12,16,26,.4);
  opacity: 0;
  transition: opacity .5s, letter-spacing .4s, background .4s, box-shadow .4s;
  text-shadow: 0 1px 5px rgba(0,0,0,.6);
}
.tile.focused .tile-preview::after {
  display: block;
  opacity: 1;
  animation: cue-pulse 2.6s ease-in-out infinite;
}
@keyframes cue-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(200,220,255,0); border-color: rgba(200,215,245,.4); }
  50% { box-shadow: 0 0 20px 2px rgba(190,215,255,.3); border-color: rgba(220,232,255,.85); }
}
/* hover on the focused tile: gentle grow + glow, no lift */
.tile.focused:hover {
  transform: translate(-50%, -50%) scale(1.055) !important;
  transition: transform .45s cubic-bezier(.22,1,.36,1) !important;
}
.tile.focused:hover .tile-inner {
  border-color: rgba(225,236,255,.95);
  box-shadow: 0 0 60px rgba(170,195,245,.4), inset 0 0 40px rgba(180,200,240,.14);
}
.tile.focused:hover .tile-preview::after {
  letter-spacing: .34em;
  background: rgba(30,42,64,.6);
  box-shadow: 0 0 24px 3px rgba(200,220,255,.4);
}
/* Legibility here comes from shadow depth rather than a darker tile — stacked
   shadows hug the glyphs, so the glass keeps its transparency. */
.tile-label {
  position: absolute; bottom: 17%; left: 0; right: 0;
  text-align: center;
  font-size: clamp(11px, 1.1vw, 14px);
  letter-spacing: .28em;
  color: #ffffff;
  text-shadow:
    0 1px 2px rgba(0,0,0,1),
    0 0 6px rgba(0,0,0,.95),
    0 0 14px rgba(0,0,0,.85),
    0 0 26px rgba(0,0,0,.6);
}
/* the tagline stays legible on the side tiles too, the way the store cards do */
.tile-plaque {
  position: absolute; bottom: 6%; left: 0; right: 0;
  text-align: center;
  font-family: var(--serif); font-style: italic;
  font-size: clamp(9.5px, .95vw, 12.5px);
  color: #e4ebf7;
  text-shadow:
    0 1px 2px rgba(0,0,0,1),
    0 0 6px rgba(0,0,0,.95),
    0 0 14px rgba(0,0,0,.85),
    0 0 26px rgba(0,0,0,.6);
  opacity: .85;
  transition: opacity .8s, color .8s;
}
.tile.focused .tile-plaque { opacity: 1; color: #f6f9ff; }
.tile.focused .tile-inner {
  box-shadow: 0 22px 60px rgba(0,0,0,.5), 0 0 44px rgba(160,185,235,.34), inset 0 0 34px rgba(170,190,230,.10);
  border-color: rgba(225,238,255,.9);
}
/* Side tiles are only dimmed once, by the carousel's inline opacity. Dimming the
   inner element as well multiplied the two and made them almost invisible. */
.tile.dim .tile-inner {
  box-shadow: 0 16px 44px rgba(0,0,0,.45), 0 0 22px rgba(140,165,215,.14);
  border-color: rgba(190,208,240,.5);
}
.tile.dim .tile-logo { opacity: .82; }

/* ---------- southern cross / moon ---------- */
.southern-cross {
  position: absolute;
  top: 12vh; right: 12vw;
  width: 60px; height: 70px;
  cursor: pointer;
}
.sc-star {
  position: absolute;
  width: 4px; height: 4px;
  background: #eef2fb;
  border-radius: 50%;
  box-shadow: 0 0 8px 2px rgba(220,230,255,.6);
  animation: sc-pulse 3.6s ease-in-out infinite;
}
.sc-star.s1 { top: 0; left: 26px; width: 5px; height: 5px; }
.sc-star.s2 { top: 26px; left: 44px; }
.sc-star.s3 { top: 34px; left: 6px; }
.sc-star.s4 { top: 54px; left: 30px; }
@keyframes sc-pulse { 0%,100% { opacity: .75; } 50% { opacity: 1; box-shadow: 0 0 13px 4px rgba(220,230,255,.8);} }
.sc-label {
  position: absolute; top: 74px; left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: 10px;
  letter-spacing: .12em;
  color: var(--faint);
}
.southern-cross:hover .sc-star { animation-duration: 1.4s; }

.moon {
  position: absolute;
  top: 6vh; right: 4vw;
  width: clamp(34px, 4vw, 52px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 38% 35%, #a9b2c5, #6d7688 65%, #4a5163);
  cursor: pointer;
  transition: box-shadow .5s, transform .5s;
  animation: moon-breathe 4.2s ease-in-out infinite;
}
@keyframes moon-breathe {
  0%, 100% { box-shadow: 0 0 26px 4px rgba(190, 205, 240, .3), 0 0 60px 10px rgba(180, 200, 245, .1); }
  50% { box-shadow: 0 0 40px 8px rgba(205, 220, 255, .6), 0 0 90px 22px rgba(190, 210, 250, .28); }
}
.moon:hover {
  transform: scale(1.08);
  box-shadow: 0 0 52px 12px rgba(210, 225, 255, .7), 0 0 110px 30px rgba(195, 215, 255, .35);
  animation-play-state: paused;
}
.moon::after {
  content: "about me";
  position: absolute;
  top: 108%; left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: 11px;
  letter-spacing: .18em;
  color: #ccd3e2;
  opacity: 0;
  transition: opacity .4s;
  text-shadow: 0 1px 6px rgba(0,0,0,.85);
  pointer-events: none;
}
.moon:hover::after { opacity: .9; }

/* ---------- inscriptions ---------- */
/* Mirrors .corner-right exactly -- same anchor, same stack, same gap -- so
   the two rows line up across the screen. Left was plain block flow before,
   which put its second line on a different baseline to "Work with me". */
.corner-left {
  position: absolute; bottom: 3.4vh; left: 3vw;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
}
/* Not a .whisper-link, so it has to borrow that line box by hand or the row
   it shares with "Work with me" sits a couple of pixels off. */
.cred {
  font-size: 12.5px;
  line-height: 15px;    /* matches .whisper-link, see the note there */
  padding: 4px 0;
  letter-spacing: .1em;
  color: #e2e7f2;
  text-shadow: 0 1px 8px rgba(0, 0, 0, .9), 0 0 3px rgba(0, 0, 0, .75), 0 0 14px rgba(0, 0, 0, .6);
}
.whisper-link {
  appearance: none;
  -webkit-appearance: none;
  background: transparent !important;
  border: none;
  outline: none;
  color: #ccd3e2;
  font-size: 12.5px;
  /* Explicit, and matched on .cred: line-height:normal resolves differently
     for a button than for a block, which left the two corners' rows a couple
     of pixels out of step with each other. */
  line-height: 15px;
  letter-spacing: .12em;
  cursor: pointer;
  padding: 4px 0;
  animation: text-glow-pulse 3.4s ease-in-out infinite;
}
.whisper-link:hover { color: #ffffff; }
@keyframes text-glow-pulse {
  0%, 100% { text-shadow: 0 0 4px rgba(255,255,255,.15), 0 1px 6px rgba(0,0,0,.8); }
  50% { text-shadow: 0 0 12px rgba(255,255,255,.75), 0 0 26px rgba(200,215,250,.35), 0 1px 6px rgba(0,0,0,.8); }
}
.corner-right {
  position: absolute; bottom: 3.4vh; right: 3vw;
  display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
}

/* This is the only thing telling anyone the sky moves, and at 11px in the
   faintest grey on the palette it was competing with a starfield and losing.
   It gets the plaque the rooms and the results heading already wear, so it
   stays readable over the sky, the city lights or the grass alike. */
.swipe-hint {
  position: absolute;
  bottom: 12vh; left: 50%;
  transform: translateX(-50%);
  font-size: 12px; letter-spacing: .2em;
  white-space: nowrap;
  color: #eaf0fb;
  padding: 9px 20px;
  border-radius: 20px;
  border: 1px solid rgba(190,210,250,.22);   /* dimmed: the orbit is the edge now */
  background: radial-gradient(ellipse at 50% 35%, rgba(58,72,100,.72), rgba(18,24,38,.68));
  box-shadow: 0 0 22px 3px rgba(150,180,240,.22), inset 0 0 14px rgba(160,185,235,.16);
  backdrop-filter: blur(3px);
  text-shadow: 0 1px 8px rgba(0,0,0,.85);
  opacity: 0;
  transition: opacity 1.2s;
  pointer-events: none;
}
.swipe-hint.lit { opacity: 1; }
/* Everything animated here is transform or opacity, so the compositor carries
   it and nothing repaints per frame -- which is what let the shadow-based
   pulses elsewhere cost Android so much.

   The chevrons do the talking: they drift outward together and ease back, so
   the pill mimes the gesture it is asking for rather than just glowing. A
   sheen crosses the glass on the same cycle and a beat later, which is the
   part that catches an eye that had stopped looking. */
.swipe-hint { display: inline-flex; align-items: center; gap: 9px; overflow: hidden; }
.sh-text { display: inline-block; }
.sh-arrow { display: inline-block; font-size: 14px; line-height: 1; opacity: .85; }
.swipe-hint.lit .sh-l { animation: sh-l 2.6s cubic-bezier(.4,0,.3,1) infinite; }
.swipe-hint.lit .sh-r { animation: sh-r 2.6s cubic-bezier(.4,0,.3,1) infinite; }
@keyframes sh-l {
  0%, 62%, 100% { transform: translateX(0); opacity: .6; }
  28% { transform: translateX(-5px); opacity: 1; }
}
@keyframes sh-r {
  0%, 62%, 100% { transform: translateX(0); opacity: .6; }
  28% { transform: translateX(5px); opacity: 1; }
}
/* A point of light travelling the border. The gradient's own start angle is
   animated rather than the element being spun -- spinning a pill-shaped box
   would drag the highlight through its corners at a different speed than its
   long edges, which reads as a wobble. @property is what makes an angle
   animatable at all; where it is not supported the ring simply sits still in
   colour, which is a fine place to land.

   Drawn into the 1.5px padding ring only: two full-size masks, one clipped to
   the content box, differenced so only the padding survives. */
@property --hint-ang {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
.swipe-hint::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  padding: 1.5px;
  background: conic-gradient(from var(--hint-ang),
    rgba(255,255,255,0) 0turn,
    rgba(255,255,255,0) .52turn,
    rgba(94,234,212,.55) .66turn,
    rgba(129,140,248,.95) .76turn,
    rgba(240,171,252,.85) .84turn,
    rgba(255,255,255,0) .95turn,
    rgba(255,255,255,0) 1turn);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  pointer-events: none;
}
.swipe-hint.lit::after { animation: hint-orbit 5.5s linear infinite; }
@keyframes hint-orbit { to { --hint-ang: 1turn; } }
}

/* ---------- rooms ---------- */
#scene-room { background: #0b0e16; }
/* the room name carries the same luminous halo as the ascension orb, so it
   reads as lit glass rather than flat type against the sky */
.room-title {
  position: absolute; top: 5vh; left: 50%;
  transform: translateX(-50%);
  letter-spacing: .42em;
  font-size: clamp(14px, 1.35vw, 18px);
  color: #eef4ff;
  text-indent: .42em;               /* balance the trailing letter-space */
  height: 42px;                     /* matches the back orb */
  display: inline-flex; align-items: center; justify-content: center;
  white-space: nowrap; text-align: center;
  padding: 0 30px;
  border-radius: 21px;
  border: 1px solid rgba(190,210,250,.4);
  background: radial-gradient(ellipse at 50% 35%, rgba(60,74,102,.5), rgba(20,26,42,.42));
  box-shadow: 0 0 26px 4px rgba(180,205,250,.28), inset 0 0 16px rgba(160,185,235,.2);
  backdrop-filter: blur(3px);
  text-shadow:
    0 0 14px rgba(190,215,255,.85),
    0 0 34px rgba(150,185,245,.5),
    0 2px 6px rgba(0,0,0,.75);
  animation: title-glow 4.6s ease-in-out infinite;
}
@keyframes title-glow {
  0%, 100% {
    box-shadow: 0 0 22px 3px rgba(180,205,250,.24), inset 0 0 14px rgba(160,185,235,.16);
    text-shadow: 0 0 12px rgba(190,215,255,.7), 0 0 30px rgba(150,185,245,.4), 0 2px 6px rgba(0,0,0,.75);
  }
  50% {
    box-shadow: 0 0 34px 7px rgba(195,218,255,.44), inset 0 0 20px rgba(175,200,245,.28);
    text-shadow: 0 0 18px rgba(210,228,255,.95), 0 0 44px rgba(165,196,250,.6), 0 2px 6px rgba(0,0,0,.75);
  }
}
.room-body {
  position: absolute;
  inset: 12vh 6vw 10vh;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 3vw;
  overflow-y: auto;
}
.gallery-pane {
  border: 1px solid rgba(140,155,185,.4);
  border-radius: 10px;
  background: #141824;
  width: min(30vw, 340px);
  aspect-ratio: 3/4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: var(--faint);
  font-size: 12px;
  letter-spacing: .08em;
  text-align: center;
  padding: 18px;
  gap: 10px;
}
.gallery-pane strong { color: var(--whisper); font-weight: 500; letter-spacing: .14em; }

.phone {
  width: min(30vw, 250px);
  aspect-ratio: 9/18;
  border: 1.5px solid rgba(150,165,195,.5);
  border-radius: 26px;
  overflow: hidden;
  background: #0d1018;
  position: relative;
}
.feed { position: absolute; inset: 0; overflow: hidden; }
.feed-scroll { animation: feed-roll 26s linear infinite; }
@keyframes feed-roll { from { transform: translateY(0); } to { transform: translateY(-50%); } }
.feed img { width: 100%; display: block; margin-bottom: 6px; border-radius: 2px; }

.constellation-svg { width: min(70vw, 700px); height: 50vh; }
.constellation-svg circle { fill: #dfe4ee; }
.constellation-svg polyline { stroke: #5b6478; stroke-width: 1.2; fill: none; }
.constellation-svg text { fill: #8b93a3; font-size: 12px; letter-spacing: .06em; }

/* ascension orb — the unique "back to the sky" control */
.back-orb {
  position: absolute;
  top: 4vh; left: 3vw;
  width: 42px; height: 42px;
  border: none;
  background: none;
  cursor: pointer;
  z-index: 6;
  animation: orb-bob 4s ease-in-out infinite;
}
.orb-core {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 36% 30%, rgba(60,72,98,.85), rgba(24,30,46,.9));
  border: 1px solid rgba(190,210,250,.55);
  box-shadow: 0 0 20px 3px rgba(180,205,250,.4), inset 0 0 12px rgba(160,185,235,.25);
  display: flex; align-items: center; justify-content: center;
  transition: transform .5s, box-shadow .5s;
}
/* upward chevron = clear "go back up" */
.orb-arrow {
  width: 12px; height: 12px;
  border-left: 2px solid #dfe7f8;
  border-bottom: 2px solid #dfe7f8;
  transform: translateX(2px) rotate(45deg);
}
/* rising spark above the orb, echoing the upward motion */
.back-orb::after {
  content: "";
  position: absolute;
  left: 50%; top: -12px;
  width: 3px; height: 3px;
  border-radius: 50%;
  background: #eaf1ff;
  transform: translateX(-50%);
  box-shadow: 0 7px 0 -0.7px rgba(220,232,255,.55);
  animation: orb-ascend 2.6s ease-in-out infinite;
}
.back-orb:hover { animation-play-state: paused; }
.back-orb:hover .orb-core {
  transform: translateY(-6px) scale(1.08);
  box-shadow: 0 0 32px 9px rgba(205,222,255,.75), inset 0 0 14px rgba(180,205,245,.4);
}
@keyframes orb-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-5px); }
}
@keyframes orb-ascend {
  0% { transform: translateX(-50%) translateY(6px); opacity: 0; }
  45% { opacity: 1; }
  100% { transform: translateX(-50%) translateY(-9px); opacity: 0; }
}

/* ---------- second seat ---------- */
.seat-inscription {
  position: absolute;
  top: 7vh; left: 6vw;
  font-family: var(--serif); font-style: italic;
  font-size: clamp(12px, 1.2vw, 15px);
  color: var(--whisper);
  opacity: 0;
  animation: fade-in 2.4s ease 1s forwards;
}
/* Sits above centre and runs wider than the portrait it covers. Overlapping
   the mountain is fine on a wide screen -- the form is the point of this
   scene, and at 360px centred it read as an afterthought pinned to the edge. */
.empty-tile {
  position: absolute;
  top: 44%; right: 5vw;
  transform: translateY(-50%);
  width: min(88vw, 430px);
  border: 1px solid var(--glass-edge);
  border-radius: 16px;
  background: linear-gradient(160deg, rgba(20,26,40,.72), rgba(12,16,26,.66));
  backdrop-filter: blur(9px);
  padding: 32px 30px 27px;
  box-shadow: 0 10px 50px rgba(0,0,0,.4);
  transition: transform 2.6s cubic-bezier(.4, 0, .8, .4), opacity 2.4s;
}
.empty-tile.liftoff { transform: translate(0, -130vh) scale(.4) rotate(3deg); opacity: 0; }
/* Off-screen rather than display:none -- a bot reading the DOM should still
   find it and fill it in, which is the whole point of a honeypot. */
.hp-field {
  position: absolute; left: -9999px; width: 1px; height: 1px;
  overflow: hidden; opacity: 0;
}
/* No reserved space while it is empty. Holding a line open for a message
   that is usually never shown made the card ~22px taller on every phone,
   which is enough to put it over the face in the portrait behind it. */
.form-note {
  margin-top: 0;
  font-size: 11.5px; letter-spacing: .04em; line-height: 1.5;
  color: var(--whisper); text-align: center;
}
.form-note:not(:empty) { margin-top: 10px; }
.form-note.bad { color: #f0a6a6; }
.form-send:disabled { opacity: .6; cursor: default; }
.field { margin-bottom: 21px; }
.form-q {
  display: block;
  font-family: var(--serif); font-size: 17px;
  color: #e6ebf5;
  margin-bottom: 11px;
}
.toggles { display: flex; gap: 9px; }
.toggle {
  padding: 7px 18px;
  border-radius: 17px;
  border: 1px solid rgba(140,155,185,.5);
  background: none;
  color: var(--whisper);
  font-size: 13px;
  cursor: pointer;
}
.toggle.on { background: rgba(170,185,215,.2); color: var(--ink); border-color: var(--glass-edge); }
.empty-tile input, .empty-tile textarea {
  width: 100%;
  background: rgba(10, 14, 22, .55);
  border: 1px solid rgba(140,155,185,.3);
  border-radius: 8px;
  color: var(--ink);
  font-size: 14.5px;
  padding: 12px 14px;
  font-family: var(--sans);
  resize: none;
  transition: border-color .3s;
}
.empty-tile input:focus, .empty-tile textarea:focus {
  outline: none;
  border-color: var(--glass-edge);
}
.form-send {
  display: block;
  width: 100%;
  margin-top: 5px;
  padding: 13px 22px;
  border-radius: 11px;
  border: 1px solid var(--glass-edge);
  background: rgba(170,185,215,.14);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 15.5px;
  letter-spacing: .08em;
  cursor: pointer;
  transition: background .3s;
}
.form-send:hover { background: rgba(185,200,235,.24); }

/* Vertically centred (the desktop default) lands the form over Alex's face
   in the seated portrait crop. Pinning it to the top instead keeps it clear
   of him regardless of how tall the field stack grows. */
@media (orientation: portrait) {
  /* Narrower and pushed further down clears the back orb (top:4vh, 42px
     tall, left:3vw) instead of overlapping its right edge.

     Everything inside is em, off one viewport-relative size on the card, so
     the whole thing scales as a unit and takes the same SHARE of any phone.
     Sized in px it did not: the height was fixed by its content, so the
     shorter the phone the more of the screen it swallowed, and on a 667px
     handset it reached the face in the portrait behind it. Now it lands at
     roughly 8vh to 47vh whatever the handset, which keeps it clear of both
     the face and the mountain. The px bounds on the clamp stop the text
     going illegible on a very short screen or overblown on a tall one. */
  .empty-tile {
    top: 7vh; right: 4vw;
    width: min(78vw, 325px);
    font-size: clamp(9px, 1.7vh, 15px);
    padding: 1.55em 1.5em 1.4em;
    transform: none;
  }
  .empty-tile.liftoff { transform: translate(0, -130vh) scale(.4) rotate(3deg); }
  .empty-tile .field { margin-bottom: 1em; }
  .empty-tile .form-q { font-size: 1em; margin-bottom: .55em; }
  .empty-tile .toggles { gap: .55em; }
  .empty-tile .toggle {
    font-size: .85em; padding: .42em 1.05em; border-radius: 1.15em;
  }
  .empty-tile input, .empty-tile textarea {
    font-size: .92em; padding: .6em .8em; border-radius: .6em;
  }
  .empty-tile textarea { min-height: 3em; }
  .empty-tile .form-send {
    font-size: 1em; padding: .7em 1.4em; margin-top: .3em; border-radius: .7em;
  }
}
.sent-line {
  position: absolute;
  top: 50%; right: 5vw;
  transform: translateY(-50%);
  width: min(88vw, 360px);
  text-align: center;
  font-family: var(--serif);
  font-size: 15px;
  line-height: 1.8;
  color: var(--ink);
  opacity: 0;
  pointer-events: none;
  transition: opacity 2.5s ease 2s;
}
.sent-line.lit { opacity: .9; }
/* Bottom right, on the same footing as "Work with me" everywhere else, so
   the one action on this screen sits where the eye already looks for it. */
.seat-whispers {
  position: absolute;
  bottom: 3.4vh; right: 3vw;
  text-align: right;
}
.seat-whispers a {
  display: inline-block;
  color: #ccd3e2;
  font-size: 12.5px;
  letter-spacing: .12em;
  text-decoration: none;
  padding: 4px 0;
  animation: text-glow-pulse 3.4s ease-in-out infinite;
}
.seat-whispers a:hover { color: #ffffff; }

/* ---------- results: floating gallery in space ---------- */
.results-space {
  position: fixed; inset: 0;
  background: radial-gradient(ellipse at 50% 40%, #0d1120 0%, #060810 70%, #04060c 100%);
  opacity: 0; pointer-events: none;
  transition: opacity 1s ease;
  z-index: 30;
  overflow: hidden;
  perspective: 1400px;
}
.results-space.open { opacity: 1; pointer-events: auto; }
/* The plate carries this room; the gradient underneath it stays as the
   backstop for the moment before it decodes. Sits behind the starfield so
   the canvas adds a little live twinkle over the photographed stars. */
.results-plate {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: 0; pointer-events: none;
}
.results-heading {
  position: absolute; top: 6vh; left: 0; right: 0;
  text-align: center; z-index: 3;
  pointer-events: none;
}
/* Same plaque the rooms wear (see .room-title) so this reads as one of them
   rather than a page heading that wandered in. */
.results-heading h2 {
  letter-spacing: .42em;
  font-size: clamp(14px, 1.35vw, 18px);
  font-weight: 400; color: #eef4ff;
  text-indent: .42em;               /* balance the trailing letter-space */
  height: 42px;                     /* matches the back orb */
  display: inline-flex; align-items: center; justify-content: center;
  white-space: nowrap;
  padding: 0 30px;
  border-radius: 21px;
  border: 1px solid rgba(190,210,250,.4);
  background: radial-gradient(ellipse at 50% 35%, rgba(60,74,102,.5), rgba(20,26,42,.42));
  box-shadow: 0 0 26px 4px rgba(180,205,250,.28), inset 0 0 16px rgba(160,185,235,.2);
  text-transform: uppercase;
}
.results-heading p {
  font-size: 13px; color: var(--whisper);
  margin-top: 8px; letter-spacing: .04em;
}
.float-field {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform .6s ease-out;
}

/* ---------- results on a phone: the same deck the ads room uses ----------
   The field does not survive one column wide -- the packer drives every card
   to its floor and the tables end up too small to read. This is the ad
   stack's shape instead (see .as-deck): current card centred and full width,
   the next couple behind it peeking in from the right and pushed back in Z,
   dragged rather than scrolled. Same gesture in both rooms, so the phone
   only has one thing to learn. */
.float-field.carousel {
  position: absolute;
  top: 17vh; bottom: 7vh; left: 0; right: 0;
  --fc-t: .32s;
  perspective: 1200px;    /* real depth behind, not a flat shrink */
  touch-action: none; cursor: grab;   /* same reasoning as .as-deck */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  transform: none !important;
}
.float-field.carousel .float-card {
  position: absolute; top: 50%; left: 50%;
  width: var(--fc-side, 86vw);
  animation: none;        /* the drift fights a drag */
  transform-origin: center center;
  /* The overshoot curve is what makes a landed swipe feel like a snap into
     place rather than a glide to a stop. Opacity keeps a plain ease --
     overshooting it would flicker the card brighter than it should go. */
  transition: transform var(--fc-t) cubic-bezier(.34, 1.56, .64, 1),
              opacity var(--fc-t) ease;
  will-change: transform, opacity;
}
/* The stat block is the reason to stop on a card, so it gets more room here
   than it needs on a desktop where the whole field is legible at once. */
.float-field.carousel .fc-caption { padding: 12px 15px 14px; }
.float-field.carousel .fc-head { font-size: 15px; margin-bottom: 9px; }
.float-field.carousel .fc-stat { font-size: 14px; }
.float-field.carousel .fc-stat.roas .value { font-size: 1.25em; }

/* Shares the bottom offset and the line box with the "Work with me" button
   beside it, so the two read as one row rather than two things that happen
   to have landed near each other. */
.results-count {
  position: absolute; left: 0; right: 0; bottom: 3.4vh;
  text-align: center; z-index: 4; pointer-events: none;
  font-size: 11px; line-height: 22px; letter-spacing: .22em;
  color: rgba(200,214,238,.6);
}
/* Above the deck: a card in front carries z-index 100 while it is stacked. */
#results-contact { z-index: 120; }
.float-card {
  position: absolute;
  border-radius: 10px;
  overflow: hidden;
  cursor: default;
  border: 1px solid rgba(190,205,235,.35);
  background: rgba(20,26,40,.7);
  box-shadow: 0 10px 40px rgba(0,0,0,.5), inset 0 0 20px rgba(150,175,225,.06);
  backdrop-filter: blur(2px);
  transition: box-shadow .55s ease, border-color .55s ease;
  will-change: transform;
}
/* The drift stops dead on hover (an animation cannot be eased to a halt), so
   the lift is carried by the shadow instead -- it arrives over half a second
   and makes the stop read as the card settling rather than snagging. */
.float-card:hover {
  border-color: rgba(215,228,250,.8);
  box-shadow: 0 18px 60px rgba(120,150,220,.4), inset 0 0 24px rgba(170,195,240,.12);
  z-index: 20 !important;
}
.float-card .fc-caption { transition: background .55s ease; }
.float-card:hover .fc-caption { background: rgba(14,19,32,.72); }
/* Holding a card still under the cursor is done in the JS, not here: each
   card's animation is assigned with the `animation` shorthand, which writes
   play-state: running into its inline style and beats any rule in this file.
   See the delegated pointerover in main.js. */
.float-card img { width: 100%; display: block; }
.float-card .fc-caption {
  padding: 9px 12px;
  font-size: 11.5px; letter-spacing: .05em;
  color: #d7deec;
  background: rgba(10,14,24,.55);
}
.fc-head { font-weight: 600; font-size: 12px; letter-spacing: .04em; color: #eef2fb; margin-bottom: 5px; }
.fc-head .fc-month { color: #8f9ab2; font-weight: 500; }
.fc-stats { display: flex; flex-direction: column; gap: 3px; }
.fc-stat { display: flex; justify-content: space-between; gap: 10px; font-size: 11px; }
.fc-stat .label { color: #8f9ab2; }
.fc-stat .value { color: #f5f8ff; font-weight: 700; letter-spacing: .01em; }
.fc-stat.roas .value { color: #3ecf82; font-weight: 700; font-size: 1.2em; }

/* A growth card is the same anatomy widened by one column: two screenshots of
   one account side by side, and every figure repeated under its own month so
   the change between them is read across a row rather than reconstructed from
   two separate cards. */
.fc-pair { display: flex; align-items: flex-start; gap: 3px; }
.float-card .fc-pair-2 img { width: 50%; }
.float-card.growth .fc-stat {
  display: grid;
  grid-template-columns: minmax(48px, auto) 1fr 1fr;
  gap: 10px; align-items: baseline;
}
.float-card.growth .fc-stat .value { text-align: right; }
.fc-months .value {
  color: #8f9ab2 !important; font-weight: 500 !important;
  font-size: .95em !important; letter-spacing: .06em;
}
.fc-summary {
  margin-top: 7px; padding-top: 6px;
  border-top: 1px solid rgba(160,180,215,.16);
  font-size: 10.5px; letter-spacing: .03em; color: #9fb0c9;
}
.float-card.placeholder {
  display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 20px;
  color: var(--whisper); font-size: 12px; font-style: italic;
  min-height: 120px;
}
.result-focus {
  position: fixed; inset: 0;
  background: rgba(4,6,12,.9);
  display: none; align-items: center; justify-content: center;
  z-index: 40; cursor: zoom-out;
  padding: 5vh;
}
.result-focus.open { display: flex; }
/* The caption follows the image into the enlarged view. It carries the
   "Sample" marker on the illustrative sheets, and this is the view someone
   actually reads the numbers in -- so it is the one place the marker most
   needs to be. */
.focus-inner { display: flex; flex-direction: column; align-items: center; gap: 12px; max-width: 92vw; }
.focus-caption {
  font-size: 13px; letter-spacing: .05em; color: #cfd8e8; text-align: center;
}
.focus-caption .fc-caption { padding: 0; background: none; }
.focus-caption .fc-head { font-size: 17px; margin-bottom: 8px; }
.focus-caption .fc-stats { align-items: center; gap: 5px; width: 220px; margin: 0 auto; }
.focus-caption .fc-stat { font-size: 15px; justify-content: space-between; }
.focus-caption .fc-stat.roas .value { font-size: 1.3em; }
.result-focus img {
  max-width: 92vw; max-height: 82vh;
  border-radius: 10px;
  border: 1px solid rgba(190,205,235,.4);
  box-shadow: 0 20px 80px rgba(0,0,0,.7);
}
.results-back { top: 4vh; left: 3vw; z-index: 35; }

/* ---------- overlays ---------- */
.overlay {
  position: fixed; inset: 0;
  background: rgba(5, 7, 12, .82);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 40;
}
.overlay.open { display: flex; }
.overlay-pane {
  width: min(92vw, 520px);
  max-height: 86vh;
  overflow-y: auto;
  border: 1px solid var(--glass-edge);
  border-radius: 14px;
  background: linear-gradient(170deg, rgba(24, 30, 44, .96), rgba(13, 17, 26, .97));
  padding: 34px;
}
.overlay-pane h2 { font-family: var(--serif); font-weight: 400; font-size: 20px; margin-bottom: 12px; }
.results-note { font-size: 12.5px; color: var(--whisper); line-height: 1.7; margin-bottom: 20px; }
.results-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 22px; }
.result-slot {
  border: 1px solid rgba(140,155,185,.3);
  border-radius: 8px;
  padding: 14px;
  font-size: 12px;
  color: var(--whisper);
  line-height: 1.5;
}
.moon-pane {
  width: min(94vw, 640px);
  max-height: 88vh;
  padding: 0;
  overflow: hidden;
  background: linear-gradient(170deg, rgba(22,29,45,.97), rgba(11,15,25,.98));
  /* A hairline plus an inset highlight along the top edge, rather than one
     bright border all the way round -- it catches the light like a real edge
     instead of outlining the box. */
  border: 1px solid rgba(190,205,235,.22);
  border-radius: 20px;
  /* Column, so the reading area takes whatever the footer leaves rather than
     the two being reconciled by a hand-tuned calc() that goes stale the
     moment the footer's contents change. */
  display: flex; flex-direction: column;
  box-shadow:
    inset 0 1px 0 rgba(225,238,255,.14),
    0 0 90px rgba(120,150,215,.16),
    0 40px 100px rgba(0,0,0,.66);
  position: relative;
}
/* moonlight wash across the top of the pane */
.moon-pane::before {
  content: "";
  position: absolute; top: -60px; left: 50%;
  transform: translateX(-50%);
  width: 320px; height: 320px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(190,215,255,.16), rgba(190,215,255,0) 70%);
  pointer-events: none;
}
.moon-scroll {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto;
  /* The bottom padding only exists to keep the CTA out of the fade below it.
     A long fade therefore costs dead space under the button twice over, so
     the bottom one is short -- it still softens the last line of a scroll
     without pushing the footer that far off the floor. */
  padding: 44px 42px 26px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 28px, #000 calc(100% - 16px), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 28px, #000 calc(100% - 16px), transparent 100%);
  scrollbar-width: thin;
  scrollbar-color: rgba(190,210,245,.42) transparent;
}
/* liquid-glass scrollbar, matching the CTA button */
.moon-scroll::-webkit-scrollbar { width: 10px; }
.moon-scroll::-webkit-scrollbar-track {
  background: rgba(150,170,210,.05);
  border-radius: 20px;
  margin: 26px 0;
}
.moon-scroll::-webkit-scrollbar-thumb {
  border-radius: 20px;
  border: 1px solid rgba(200,216,248,.5);
  background:
    linear-gradient(180deg, rgba(226,236,255,.32), rgba(160,180,220,.16)),
    rgba(150,165,195,.12);
  box-shadow:
    inset 0 1px 2px rgba(255,255,255,.45),
    inset 0 -2px 6px rgba(120,150,210,.25),
    0 0 12px rgba(160,190,245,.25);
  backdrop-filter: blur(3px);
  transition: box-shadow .4s, border-color .4s;
}
.moon-scroll::-webkit-scrollbar-thumb:hover {
  border-color: rgba(225,238,255,.9);
  box-shadow:
    inset 0 1px 3px rgba(255,255,255,.6),
    inset 0 -2px 8px rgba(140,175,235,.35),
    0 0 22px rgba(180,205,255,.45);
}
.moon-photo {
  width: 104px; border-radius: 50%; aspect-ratio: 1; object-fit: cover;
  display: block; margin: 0 auto 22px;
  /* Two rings rather than a glow: the halo alone read soft, and a thin inner
     hairline against a wider outer wash is what gives a portrait an edge. */
  border: 1px solid rgba(215,230,255,.55);
  box-shadow:
    0 0 0 6px rgba(150,175,225,.07),
    0 0 30px 4px rgba(185,210,255,.22);
  animation: moon-breathe 5s ease-in-out infinite;
}
.moon-eyebrow {
  text-align: center;
  font-size: 10px; letter-spacing: .34em; text-transform: uppercase;
  color: rgba(160,178,212,.65);
  margin-bottom: 26px;
}
/* A measure, not the full pane width -- long serif lines are the single
   biggest thing making a page of text feel cheap to read. */
.moon-story { max-width: 54ch; margin: 0 auto; }
.moon-story p { font-family: var(--serif); letter-spacing: .005em; }
/* The opening line is an introduction, so it is set like one: larger, lighter
   and looser than the body it sits above. */
.ms-lede {
  font-size: 20px; line-height: 1.62;
  color: #f4f7fc;
  margin-bottom: 24px;
}
.ms-body {
  font-size: 14.5px; line-height: 1.95;
  color: #c8d0e0;
  margin-bottom: 17px;
}
/* Marked with a leading "> " in about.txt (see the parser in main.js). */
.ms-quote {
  font-size: 17px; line-height: 1.7;
  color: #eaf0fa;
  margin: 26px 0;
  padding-left: 20px;
  border-left: 1px solid rgba(180,205,250,.45);
  font-style: italic;
}
/* Sits at the end of the story rather than pinned over it -- it is the last
   thing you read, not a toolbar. Full-bleed against the scroll's own side
   padding, so the rule above it spans the pane. Nothing competes for the
   width now, which is what lets the credentials hold one line. */
.moon-foot {
  margin: 30px -42px 0;
  padding: 17px 42px 0;
  border-top: 1px solid rgba(150,170,210,.16);
  display: flex; flex-direction: column; align-items: center;
  gap: 12px;
}
/* Separated by drawn dots rather than typed middots, so they space evenly
   and the line can break between facts instead of mid-credential. */
/* One centred line on anything wide enough to hold it; wraps to two on a
   phone. Each credential is nowrap so a wrap can only ever fall between
   facts, never inside one. */
.moon-creds {
  display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
  gap: 5px 14px;
}
.moon-creds span {
  display: flex; align-items: center;
  font-size: 9.5px; letter-spacing: .12em; text-transform: uppercase;
  color: #8791a6; white-space: nowrap;
}
.moon-creds span + span::before {
  content: ""; flex: 0 0 auto;
  width: 3px; height: 3px; border-radius: 50%;
  background: rgba(135,145,166,.5);
  margin-right: 14px;
}
/* Reads as part of its own credential rather than a figure pulled out of it,
   so it inherits the row's face, size, weight and tracking entirely. */
.moon-creds b {
  font: inherit; letter-spacing: inherit; text-transform: inherit;
  color: inherit; margin-right: 4px;
}
.moon-cta {
  flex: 0 0 auto;
  padding: 9px 24px;
  font-family: var(--serif);
  font-size: 13.5px; letter-spacing: .08em;
  color: var(--ink);
  background: rgba(150,165,195,.08);
  border: 1px solid var(--glass-edge);
  border-radius: 30px;
  cursor: pointer;
  transition: background .4s, transform .25s, box-shadow .4s;
  backdrop-filter: blur(2px);
}
.moon-cta:hover {
  background: rgba(170,185,215,.18);
  box-shadow: 0 0 26px rgba(160,180,220,.25);
  transform: scale(1.04);
}
.moon-cta:active { transform: scale(.97); }
@media (max-width: 700px) {
  /* Tighter gutters, and the footer's bleed matches them. */
  .moon-scroll { padding: 36px 22px 26px; }
  .moon-foot { margin: 26px -22px 0; padding: 16px 22px 0; }
  .moon-cta { width: 100%; }
}
.moon-pane .overlay-close {
  position: absolute; top: 14px; right: 20px;
  margin: 0; z-index: 3;
}
.overlay-close {
  margin-top: 20px;
  background: none; border: none;
  color: var(--faint);
  font-size: 12px; letter-spacing: .14em;
  cursor: pointer;
}
.overlay-close:hover { color: var(--ink); }

/* ---------- mobile ---------- */
@media (max-width: 700px) {
  .sky { height: 40vh; top: 10vh; }
  .tile { width: 62vw; }
  .room-body { flex-direction: column; overflow-y: auto; }
  .room-body.orbit-mode { inset: 12vh 4vw 8vh; }
  /* the fan becomes a vine climbing the screen; JS switches the anchor points */
  .room-body.tree-mode { inset: 8vh 3vw 3vh; }
  .branch { width: clamp(96px, 30vw, 128px); gap: 5px; }
  .br-txt em { display: none; }
  .tree-root { width: 94%; padding: 9px 12px; gap: 10px; }
  .tree-root img { width: 48px; }
  .tr-blurb { display: none; }
  .tree-why { padding: 0 4vw; min-height: 54px; }
  .gallery-pane { width: 80vw; }
  .southern-cross { right: 6vw; top: 8vh; transform: scale(.8); }
  .identity { left: 6vw; }
  .tagline-settled { right: 6vw; top: 17vh; }
  /* the wide letter-spacing that reads fine on desktop pushes "EMAIL FLOWS"
     past the pill's own width on a phone, wrapping it to two lines */
  .room-title,
  .results-heading h2 { font-size: 12px; letter-spacing: .26em; text-indent: .26em; padding: 0 18px; }
}

@media (prefers-reduced-motion: reduce) {
  .plate, .tile-inner, .whisper-link, .sc-star { animation: none !important; }
  .plate-video { display: none; }
  .identity, .cta, .seat-inscription { animation: none !important; opacity: 1 !important; }
  .tagline-settled span { transition: none; }
  .tagline-settled span.lit { opacity: .75; }
}

@keyframes card-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}
/* The results cards are positioned by their centre (left/top are a percentage
   and translate(-50%,-50%) pulls them back onto it) and pushed into depth.
   An animation's transform REPLACES the inline one rather than adding to it,
   so animating with plain card-bob dropped both -- every card jumped right by
   half its width and drifted off the screen. This keyframe carries the
   centring and the depth through every step; the depth arrives per card as
   --fc-z so one rule serves them all. */
/* Every card runs this one keyframe but arrives at a different path: the
   amplitudes and the tilt come in per card as --fc-dx/--fc-dy/--fc-rot, and
   the stops are spaced so no two axes turn at the same moment. A single
   shared up-and-down had them all breathing in time, which read as one
   mechanism rather than a field of separate things drifting. */
@keyframes fc-bob {
  0%, 100% {
    transform: translate(-50%, -50%) translateZ(var(--fc-z, 0px))
               translate(0, 0) rotate(0deg);
  }
  18% {
    transform: translate(-50%, -50%) translateZ(var(--fc-z, 0px))
               translate(calc(var(--fc-dx, 5px) * .55), calc(var(--fc-dy, 10px) * -.3))
               rotate(calc(var(--fc-rot, .4deg) * .45));
  }
  37% {
    transform: translate(-50%, -50%) translateZ(var(--fc-z, 0px))
               translate(var(--fc-dx, 5px), calc(var(--fc-dy, 10px) * -.75))
               rotate(var(--fc-rot, .4deg));
  }
  58% {
    transform: translate(-50%, -50%) translateZ(var(--fc-z, 0px))
               translate(calc(var(--fc-dx, 5px) * .15), calc(var(--fc-dy, 10px) * -1))
               rotate(calc(var(--fc-rot, .4deg) * .3));
  }
  79% {
    transform: translate(-50%, -50%) translateZ(var(--fc-z, 0px))
               translate(calc(var(--fc-dx, 5px) * -.8), calc(var(--fc-dy, 10px) * -.45))
               rotate(calc(var(--fc-rot, .4deg) * -.6));
  }
}

/* Anyone who has asked the OS for less movement gets the field standing
   still; the cards are decoration and nothing here depends on the drift. */
@media (prefers-reduced-motion: reduce) {
  .float-card { animation: none !important; }
}

/* ---------- store viewer ---------- */
.store-viewer {
  position: fixed; inset: 0;
  background: #0a0d15;
  z-index: 60;
  display: none;
  flex-direction: column;
}
.store-viewer.open { display: flex; }
.store-frame-bar {
  position: relative;
  height: 46px;
  flex: 0 0 46px;
  display: flex; align-items: center;
  padding: 0 18px;
  background: rgba(14,18,28,.96);
  border-bottom: 1px solid rgba(150,170,210,.18);
}
/* centred on the bar itself, so the close button on the left can't shift it */
.store-frame-name {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  font-size: 13px; letter-spacing: .1em; color: #cfd6e3;
  white-space: nowrap; pointer-events: none;
}
.store-close {
  background: none; border: 1px solid rgba(190,205,235,.4);
  color: #ccd3e2; font-size: 12px; letter-spacing: .08em;
  padding: 6px 14px; border-radius: 16px; cursor: pointer;
  transition: background .3s, border-color .3s;
}
.store-close:hover { background: rgba(170,185,215,.15); border-color: var(--glass-edge); }
.store-viewer iframe { flex: 1 1 auto; width: 100%; border: none; background: #fff; }
.gallery-pane.live { cursor: pointer; transition: border-color .3s, box-shadow .3s, transform .3s; }
.gallery-pane.live:hover {
  border-color: rgba(215,228,250,.8);
  box-shadow: 0 10px 44px rgba(120,150,220,.3);
  transform: translateY(-4px);
}
.browse-cue { color: #aab8dd; font-style: normal; letter-spacing: .06em; }

/* ---------- store gallery cards (redesigned) ---------- */
#scene-room {
  background-color: #070a12;
  background-image: radial-gradient(ellipse at 50% 35%, #0e1220 0%, #070a12 70%, #04060c 100%);
  background-size: cover;
  background-position: center;
}
#scene-room.has-bg {
  background-image: var(--room-bg) !important;
  background-size: cover !important;
  background-position: center !important;
}
/* Twinkling star canvas stays visible over the still sky plate -- it's the
   "twinkling star here and there" motion, not covered up by the image. */
.room-body { gap: 2.4vw; }
.store-card-wrap {
  width: min(30vw, 360px);
  animation: card-bob 7s ease-in-out infinite;
  transition: transform .5s cubic-bezier(.22,1,.36,1);
}
.store-card-wrap:hover { animation-play-state: paused; }
.store-card {
  border-radius: 14px;
  overflow: hidden;
  background: linear-gradient(160deg, rgba(170,185,215,.16), rgba(120,135,165,.06));
  border: 1px solid var(--glass-edge);
  box-shadow: 0 14px 50px rgba(0,0,0,.45), 0 0 30px rgba(140,160,210,.12), inset 0 0 30px rgba(160,180,220,.06);
  backdrop-filter: blur(4px);
  cursor: default;
  transition: transform .4s cubic-bezier(.22,1,.36,1), box-shadow .4s, border-color .4s;
}
.store-card.live { cursor: pointer; }
.store-card-wrap:has(.store-card.live:hover) { z-index: 5; }
.store-card.live:hover {
  transform: scale(1.06);
  border-color: rgba(220,232,255,.9);
  box-shadow: 0 30px 80px rgba(120,150,220,.5), inset 0 0 34px rgba(170,195,240,.12);
}
.store-thumb {
  position: relative;
  /* Per store: the frame is as tall as that store's hero needs. Stores whose
     hero runs the full capture show all of it; one with a bar under the hero
     gets a shorter frame so the top-anchored image stops above it. */
  width: 100%; aspect-ratio: var(--thumb-ar, 1.6);
  overflow: hidden;
  background: #0d1018;
  border-bottom: 1px solid rgba(150,170,210,.18);
}
/* a still captured at a 1440x900 laptop viewport, so the store's whole hero
   section shows in the tile without mounting a live storefront */
.store-thumb img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
  display: block; pointer-events: none;
  user-select: none;
}
.store-thumb.empty {
  display: flex; align-items: center; justify-content: center;
  color: var(--faint); font-size: 12px; letter-spacing: .18em; font-style: italic;
}
.store-open {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  display: flex; align-items: center; justify-content: center;
  font-size: clamp(10px, 1vw, 13px); letter-spacing: .28em; color: #eaf0fb;
  text-indent: .28em;          /* offset the trailing letter-space so it sits true-centre */
  padding: 6px 20px;
  border: 1px solid rgba(200,215,245,.5);
  border-radius: 20px;
  background: rgba(12,16,26,.45);
  text-shadow: 0 1px 5px rgba(0,0,0,.6);
  opacity: 0;
  /* no letter-spacing or box-shadow transition here: widening the pill drags it
     off its centre anchor, and box-shadow is owned by the cue-pulse animation */
  transition: opacity .5s, background .4s, border-color .4s;
}
.room-body.orbit-mode .store-card-wrap.focused .store-open {
  opacity: 1;
  animation: cue-pulse 2.6s ease-in-out infinite;
}
/* hover brightens the pill only — the pulse keeps sole ownership of box-shadow */
.store-card.live:hover .store-open {
  background: rgba(34,48,72,.72);
  border-color: rgba(226,238,255,.95);
}
/* a darker base under the caption so the type isn't competing with whatever
   sky happens to sit behind the glass */
/* Fixed height regardless of caption length: the taglines wrap to different
   line counts, and letting the block grow changed each card's height as the
   ring turned, which made the preview above it look like it was shifting. */
.store-meta {
  padding: 12px 18px 14px; text-align: center;
  min-height: clamp(70px, 6.4vw, 86px);
  display: flex; flex-direction: column; justify-content: center;
  background: linear-gradient(to bottom, rgba(9,13,23,.42), rgba(9,13,23,.66));
}
.store-meta strong {
  display: block; color: #f7f9fd; font-weight: 400;
  letter-spacing: .24em; font-size: clamp(10.5px, 1vw, 13px);
  text-transform: uppercase;
  text-shadow: 0 1px 4px rgba(0,0,0,.85);
}
.store-meta span {
  display: block; margin-top: 5px; color: #e7edf8;
  font-family: var(--serif); font-style: italic;
  font-size: clamp(10px, .92vw, 12.5px);
  line-height: 1.4;
  text-shadow: 0 1px 4px rgba(0,0,0,.9), 0 0 14px rgba(0,0,0,.55);
}

/* ---------- room cards: slow orbit ring ----------
   JS writes .store-card-wrap transforms every frame, so the wrap must carry
   no keyframe animation and no transform transition — either would fight it.
   The hover-grow lives on the child .store-card, which JS never touches. */
.room-body.orbit-mode {
  position: absolute;
  inset: 12vh 6vw 9vh;
  display: block;
  overflow: visible;
  perspective: 1200px;
}
.room-body.orbit-mode .store-card-wrap {
  position: absolute;
  top: 50%; left: 50%;
  width: clamp(280px, 31vw, 440px);
  animation: none !important;
  /* the compositor carries the card between slots — see makeOrbit */
  transition: transform 1.35s cubic-bezier(.34, .9, .28, 1), opacity 1.1s ease;
  will-change: transform, opacity;
  backface-visibility: hidden;
  cursor: pointer;
}

/* Restated after the rule above, not up in the mobile block where it started:
   same specificity, and the earlier one simply lost. At the 280px floor the
   front card filled a phone and its neighbours sat entirely behind it, so the
   room looked like a single store with nothing either side. */
@media (max-width: 700px) {
  .room-body.orbit-mode .store-card-wrap { width: 58vw; }
}

/* --pop rises from 0 at the back of the ring to 1 out front, so the leading
   card's glass brightens and its glow blooms as it swings into view */
.room-body.orbit-mode .store-card {
  border-color: rgba(200,216,245,calc(.22 + var(--pop, 0) * .68));
  box-shadow:
    0 14px 50px rgba(0,0,0,.45),
    0 0 calc(18px + var(--pop, 0) * 46px) rgba(150,180,240,calc(.08 + var(--pop, 0) * .38)),
    inset 0 0 30px rgba(160,180,220,.06);
  transition: box-shadow 1.2s ease, border-color 1.2s ease;
}
/* only the card out front takes the grow-hover */
.room-body.orbit-mode .store-card-wrap.dim .store-card.live:hover {
  transform: none;
  box-shadow: 0 14px 50px rgba(0,0,0,.5);
}


/* ---------- meta ads room: the angle tree ---------- */
.room-body.tree-mode {
  /* clears the room title, which sits at 5vh and is 42px tall */
  position: absolute; inset: 13vh 3vw 3vh;
  display: flex; flex-direction: column; overflow: visible;
  /* the base .room-body sets flex-wrap:wrap for its own grid layouts and this
     never reset it -- when the column's content ran taller than the room,
     some browsers wrapped tree-why into a second column starting back near
     the top instead of overflowing, landing it right on top of the tree.
     Chromium happened to shrink tree-stage instead of wrapping, which is why
     this never showed up in testing here. */
  flex-wrap: nowrap;
  /* the base .room-body centres its items, which would collapse the stage to
     zero width — everything inside it is absolutely positioned */
  align-items: stretch; justify-content: flex-start;
}
/* the identical-looking rule for this in the max-width:700px block further up
   the file loses the cascade to this one (same specificity, later source
   wins) -- real mobile override goes here instead. Trims the room's own
   bottom margin so margin-top:auto on tree-why has more room to push into. */
@media (max-width: 700px) {
  .room-body.tree-mode { inset: 13vh 3vw 0; }
}
/* this line sits over the brightest part of the Milky Way, so it needs a soft
   dark halo of its own — a hard pill would fight the room title above it */
.tree-head {
  flex: 0 0 auto; text-align: center; margin-bottom: 6px;
  color: #f6faff; font-family: var(--serif); font-style: italic;
  font-size: clamp(13px, 1.45vw, 20px); letter-spacing: .03em; line-height: 1.4;
  max-width: 60ch; margin-inline: auto;
  padding: 12px clamp(20px, 5vw, 80px);
  /* No background plate. Any gradient sized larger than its box gets clipped at
     the element bounds and shows hard edges, so the darkening is done purely
     with stacked shadows, which hug the glyphs and have no box to reveal. */
  text-shadow:
    0 1px 2px rgba(0,0,0,1),
    0 0 7px rgba(0,0,0,.98),
    0 0 15px rgba(0,0,0,.92),
    0 0 28px rgba(0,0,0,.8),
    0 0 48px rgba(0,0,0,.62),
    0 0 80px rgba(0,0,0,.42);
}
/* one sentence per line */
.tree-head span { display: block; }
.tree-head span + span { margin-top: 2px; }
/* the factual line, deliberately smaller and plainer than the italic hook
   above it so it reads as a caption, not a third beat of the same pitch */
.tree-head .tree-sub {
  margin-top: 8px; font-family: var(--sans, inherit); font-style: normal;
  font-size: clamp(10.5px, 1.05vw, 13px); letter-spacing: .02em;
  color: #b9c4dc;
}
.tree-stage { position: relative; flex: 1 1 auto; min-height: 0; touch-action: pan-y; }
.tree-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.tree-svg .trunk, .tree-svg .limb {
  fill: none; stroke: rgba(178,202,246,.5); stroke-width: 1.6; stroke-linecap: round;
  transition: stroke .45s, stroke-width .45s;
}
.tree-svg .trunk { stroke: rgba(214,232,255,.8); stroke-width: 2.2; }
.tree-svg .limb.sel { stroke: rgba(238,248,255,.98); stroke-width: 2.6; filter: drop-shadow(0 0 7px rgba(190,218,255,.85)); }
.tree-svg .node, .tree-svg .fork { fill: #e6f0ff; filter: drop-shadow(0 0 6px rgba(180,210,255,.9)); }

/* the five arguments */
.branch {
  position: absolute; transform: translate(-50%, -50%) scale(.9);
  background: none; border: none; padding: 0; cursor: pointer;
  width: clamp(94px, 10.8vw, 152px);
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  opacity: 0;
}
.branch.shown { opacity: 1; transform: translate(-50%, -50%) scale(1); }
/* branches are position:absolute, so they take no part in the stage's own
   height -- shrinking them doesn't free space in the stage, it frees space
   between the cards themselves and whatever sits below the stage, which is
   the actual lever for closing the gap to tree-why on a short phone screen. */
@media (max-width: 700px) {
  .branch { width: clamp(78px, 24vw, 104px); gap: 4px; }
}
.br-card {
  position: relative; display: block; width: 100%;
  border-radius: 11px; overflow: hidden;
  border: 1px solid rgba(186,208,248,.5);
  box-shadow: 0 14px 36px rgba(0,0,0,.6);
  transition: border-color .4s, box-shadow .4s, transform .45s cubic-bezier(.22,1,.36,1);
}
.br-card img {
  display: block; width: 100%; aspect-ratio: 1; object-fit: cover;
  filter: saturate(.8) brightness(.72); transition: filter .4s;
}
/* top-right: these creatives almost all set their headline in the top-left,
   so a badge there lands straight on the first word */
.br-no {
  position: absolute; top: 6px; right: 6px;
  font-size: 8.5px; letter-spacing: .16em; color: #eaf1ff;
  padding: 2px 7px; border-radius: 9px;
  background: rgba(8,12,22,.62); border: 1px solid rgba(190,210,250,.3);
}
/* The one thing the room never said out loud: these open. Sits over the card
   on hover -- the same move the store tiles make with their "open" chip, so
   it is a signal the room has already taught. */
.br-open {
  position: absolute; left: 50%; bottom: 8px;
  transform: translate(-50%, 6px);
  font-size: 8.5px; letter-spacing: .18em; text-transform: uppercase; color: #eaf1ff;
  padding: 4px 11px; border-radius: 11px; white-space: nowrap;
  background: rgba(8,12,22,.78); border: 1px solid rgba(190,210,250,.45);
  backdrop-filter: blur(3px);
  opacity: 0; pointer-events: none;
  transition: opacity .3s, transform .3s;
}
.branch:hover .br-open { opacity: 1; transform: translate(-50%, 0); }
.br-txt { display: block; text-align: center; }
.br-txt strong {
  display: block; color: #dee7f8; font-weight: 400;
  font-size: clamp(8.5px, .8vw, 10.5px); letter-spacing: .18em; text-transform: uppercase;
  text-shadow: 0 1px 6px rgba(0,0,0,.85);
  transition: color .4s;
}
/* the hook lives in the panel below, not on every card at once — five names and
   five hooks on screen together is what made this read as noise */
.br-txt em { display: none; }
.branch:hover .br-card, .branch.sel .br-card {
  transform: translateY(-5px);
  border-color: rgba(236,246,255,.95);
  box-shadow: 0 20px 50px rgba(120,158,235,.55), 0 0 24px rgba(175,205,255,.4);
}
.branch:hover .br-card img, .branch.sel .br-card img { filter: none; }
.branch.sel .br-txt strong { color: #f4f8ff; }
.branch.sel .br-txt em { color: #dce6f8; }
/* immediate tactile response the instant a finger lands -- fast (.08s, not
   the hover transition's .4s) so it reads as a press, not a hover lingering
   in. This is the actual "you can tap this" signal; nothing moves until you
   touch it, so the first touch has to visibly answer back. */
.branch:active .br-card {
  transition: border-color .08s, box-shadow .08s, transform .08s;
  transform: scale(.95);
  border-color: rgba(236,246,255,.95);
  box-shadow: 0 8px 20px rgba(120,158,235,.5), 0 0 16px rgba(175,205,255,.4);
}
/* one quiet pulse, once the tree has finished growing in -- pure motion
   standing in for a "tap me" label. Keeps the entrance transform (translate
   to the card's own point, already centred by .branch.shown) so it pulses in
   place rather than jumping to the default 0,0 transform origin. */
@keyframes branch-pulse {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  45% { transform: translate(-50%, -50%) scale(1.08); }
}
.branch.pulse-once { animation: branch-pulse .6s ease-in-out; }

/* the product at the root */
.tree-root {
  position: absolute; left: 50%; bottom: -4vh; transform: translateX(-50%);
  display: flex; align-items: center; gap: 14px;
  width: min(88%, 440px);
  padding: 10px 16px 10px 10px; border-radius: 14px;
  border: 1px solid rgba(206,224,255,.55);
  background: radial-gradient(ellipse at 30% 20%, rgba(54,68,100,.72), rgba(14,20,34,.82));
  box-shadow: 0 0 42px 6px rgba(160,195,255,.28), inset 0 0 20px rgba(160,185,235,.16);
  backdrop-filter: blur(4px);
}
@media (max-width: 700px) {
  /* Alex's own coordinate: root at x:50, y:100 of the stage (same convention
     as the ?edit=1 HUD) -- centred exactly on the stage's bottom edge, so
     roughly the bottom half of the card sits past it. room-body's own
     overflow-y:auto (mobile) lets a short scroll reach the rest. */
  .tree-root { left: 50%; top: 100%; bottom: auto; transform: translate(-50%, -50%); }
}
/* a cut-out packshot: no frame, no crop, it should read as the object itself */
.tree-root img {
  width: clamp(56px, 6vw, 78px); aspect-ratio: 1; object-fit: contain; flex: 0 0 auto;
  filter: drop-shadow(0 6px 16px rgba(0,0,0,.55));
}
.tree-root.no-shot img { display: none; }
.tree-root.no-shot { justify-content: center; }
.tree-root.no-shot .tr-copy { text-align: center; }
.tr-copy { min-width: 0; text-align: left; }
.tr-brand {
  display: block; color: #b6c4e0;
  font-size: clamp(8px, .74vw, 9.5px); letter-spacing: .26em; text-transform: uppercase;
}
.tr-name {
  display: block; margin-top: 2px; color: #f6f9ff; font-weight: 400;
  font-size: clamp(13px, 1.3vw, 18px); letter-spacing: .12em; text-transform: uppercase;
  text-shadow: 0 2px 10px rgba(0,0,0,.7);
}
.tr-blurb {
  display: block; margin-top: 5px; color: #ccd7ec;
  font-family: var(--serif); font-style: italic;
  font-size: clamp(9.5px, .88vw, 12px); line-height: 1.45;
}

/* product paging — absolute, so they never take part in the column layout */
.pp-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 40px; height: 40px; border-radius: 50%; z-index: 8; cursor: pointer;
  background: radial-gradient(circle at 36% 30%, rgba(60,72,98,.85), rgba(24,30,46,.9));
  border: 1px solid rgba(190,210,250,.5);
  box-shadow: 0 0 18px 3px rgba(180,205,250,.28);
  color: #dfe7f8; font-size: 20px; line-height: 1;
  transition: opacity .4s, box-shadow .4s;
}
.pp-arrow:hover:not(:disabled) { box-shadow: 0 0 28px 6px rgba(205,222,255,.6); }
.pp-arrow:disabled { opacity: .18; cursor: default; }
.pp-prev { left: -8px; }
.pp-next { right: -8px; }
.pp-dots {
  position: absolute; left: 50%; bottom: -2px; transform: translateX(-50%);
  display: flex; gap: 8px; z-index: 8;
}
.pp-dot {
  width: 7px; height: 7px; border-radius: 50%; padding: 0; cursor: pointer;
  background: rgba(190,210,250,.3); border: 1px solid rgba(190,210,250,.5);
  transition: background .4s, box-shadow .4s;
}
.pp-dot.on { background: #dbe7ff; box-shadow: 0 0 12px 2px rgba(190,215,255,.7); }

/* top:50% centres these on the whole room, which on the tall mobile "vine"
   layout drops an arrow right beside whichever card sits at the vertical
   middle -- flanking the dots at the bottom instead keeps them off the tree.
   Uppercase + .18em letter-spacing on .br-txt strong also wraps "PERSONA
   TARGETING" and "UGC TESTIMONIAL" to two ragged lines while shorter labels
   stay on one, which is what read as messy -- tighter spacing here levels
   more of them out. */
@media (max-width: 700px) {
  .pp-arrow { top: auto; bottom: 2px; width: 34px; height: 34px; font-size: 17px; transform: none; }
  .pp-prev { left: calc(50% - 74px); }
  .pp-next { right: calc(50% - 74px); }
  .pp-dots { bottom: 14px; }
  .br-txt strong { letter-spacing: .09em; }
  /* Next specifically wanted back at the original right-edge, vertical-centre
     spot rather than flanking the dots down here. Prev now matches it on the
     left, once a room has more than one product and prev becomes reachable. */
  .pp-next { top: 50%; bottom: auto; right: -8px; width: 40px; height: 40px; font-size: 20px; transform: translateY(-50%); }
  .pp-prev { top: 50%; bottom: auto; left: -8px; width: 40px; height: 40px; font-size: 20px; transform: translateY(-50%); }
}

/* why this angle exists */
.tree-why {
  flex: 0 0 auto; text-align: center; opacity: 0;
  padding: 0 6vw; margin-top: 8px; min-height: clamp(88px, 13vh, 124px);
}
.tree-why.lit { animation: note-in .55s ease forwards; }
@media (max-width: 700px) {
  /* margin-top:auto in a column flex container eats all remaining space
     above it, pushing the box as far down as the room allows rather than a
     fixed nudge -- true "toward the bottom" instead of a guessed pixel gap. */
  .tree-why { margin-top: auto; }
}
/* the hook the ad actually opens with — the loudest thing in the panel */
.tree-why b {
  display: block; color: #f4f8ff; font-weight: 400;
  font-family: var(--serif); font-style: italic;
  font-size: clamp(14px, 1.5vw, 21px); line-height: 1.35;
  max-width: 46ch; margin-inline: auto;
  text-shadow: 0 2px 14px rgba(0,0,0,.7);
}
.tree-why strong {
  display: block; margin-top: 7px; color: #aebbd6; font-weight: 400;
  font-size: clamp(8.5px, .8vw, 10.5px); letter-spacing: .26em; text-transform: uppercase;
  text-shadow: 0 1px 8px rgba(0,0,0,.8);
}
.tree-why span {
  display: block; margin-top: 6px; color: #ccd6e8;
  font-size: clamp(10px, .92vw, 12.5px); line-height: 1.5;
  max-width: 68ch; margin-inline: auto;
  text-shadow: 0 1px 8px rgba(0,0,0,.75);
}

/* tap a card to read it properly */
.drum-focus {
  position: fixed; inset: 0; z-index: 40; cursor: pointer;
  display: none; flex-direction: column; align-items: center; justify-content: center; gap: 16px;
  background: rgba(5,8,15,.9); backdrop-filter: blur(7px);
}
.drum-focus.open { display: flex; animation: note-in .4s ease; }
/* The deck, not a single picture on a dark field: the angles either side sit
   behind and beside the current one, so it is visible that there are more.
   Same reading as the phone's ad stack. */
/* Square, because every ad is (1080x1080 or 1024x1024). It was 630x612, and
   object-fit:contain then letterboxed the image inside a box whose border was
   drawn at the box's edge -- which is the gap down either side. A square
   stage puts the border on the picture. */
.df-stage {
  position: relative;
  --df-side: min(68vh, 88vw);
  width: var(--df-side); height: var(--df-side);
}
.df-shot {
  position: absolute; top: 50%; left: 50%;
  width: 100%; height: 100%; object-fit: cover;
  border-radius: 14px; border: 1px solid rgba(215,232,255,.6);
  box-shadow: 0 28px 80px rgba(0,0,0,.75), 0 0 40px rgba(150,185,245,.3);
  transform-origin: center;
  transition: transform .38s cubic-bezier(.22,1,.36,1), opacity .38s ease;
  will-change: transform, opacity;
  cursor: pointer;
}
/* the fronted one is not a target -- a click on it falls through and closes */
.df-shot[data-off="0"] { cursor: default; }

.df-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; border-radius: 50%; z-index: 120; cursor: pointer;
  background: radial-gradient(circle at 36% 30%, rgba(60,72,98,.9), rgba(24,30,46,.94));
  border: 1px solid rgba(190,210,250,.5);
  box-shadow: 0 0 18px 3px rgba(180,205,250,.28);
  color: #dfe7f8; font-size: 24px; line-height: 1;
  transition: box-shadow .35s, transform .25s, background .35s;
}
.df-arrow:hover {
  box-shadow: 0 0 30px 7px rgba(205,222,255,.6);
  transform: translateY(-50%) scale(1.08);
}
.df-arrow:active { transform: translateY(-50%) scale(.94); }
.df-prev { left: max(2vw, 18px); }
.df-next { right: max(2vw, 18px); }

.drum-focus img.legacy-single {
  max-width: min(70vh, 88vw); max-height: 68vh; object-fit: contain;
  border-radius: 14px; border: 1px solid rgba(215,232,255,.6);
  box-shadow: 0 28px 80px rgba(0,0,0,.75), 0 0 40px rgba(150,185,245,.3);
}
.df-meta { text-align: center; }
.df-who {
  display: inline-block; margin-bottom: 9px;
  font-size: 9.5px; letter-spacing: .28em; text-transform: uppercase; color: #cdd8ef;
  padding: 4px 12px; border-radius: 13px;
  border: 1px solid rgba(190,210,250,.4); background: rgba(20,28,46,.5);
}
.df-angle {
  display: block; color: #f2f6ff; font-weight: 400;
  font-size: clamp(16px, 1.6vw, 23px); letter-spacing: .06em;
  text-shadow: 0 2px 12px rgba(0,0,0,.6);
}
.df-count {
  display: block; margin-top: 7px;
  font-size: 9.5px; letter-spacing: .22em; color: #8e9cba;
}
.df-close {
  font-size: 9.5px; letter-spacing: .22em; text-transform: uppercase; color: #8e9cba;
}
.df-close b { font-weight: 400; color: #cdd8ef; letter-spacing: .1em; }
/* Edges step, middle closes (see the pointerup handler), so the cursor should
   say which is which rather than leaving it to be discovered. */
.drum-focus { cursor: zoom-out; }   /* bare space closes; see the pointerup */

/* Sits under the description panel rather than up in the standfirst, so it
   is read after the room has been looked at rather than before. */
.tree-tip {
  position: absolute; left: 50%; bottom: 2.4vh; transform: translateX(-50%);
  z-index: 7; pointer-events: none; text-align: center; white-space: nowrap;
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: #8593b0;
  text-shadow: 0 1px 8px rgba(0,0,0,.85);
}
.tree-tip b { font-weight: 400; color: #d3ddf1; }

.room-body.flow-mode { position: absolute; inset: 0; display: block; }

/* ---------- email room, phone board ----------
   The same board as the desktop, but every card is placed in screen
   coordinates by main.js rather than laid out inside one huge scaled element.
   Nothing here is ever bigger than the screen, so there is no oversized layer
   for the browser to hold. */
.room-body.flow-mobile { position: absolute; inset: 0; display: block; padding: 0; overflow: hidden; }
.mb-stage {
  position: absolute; inset: 0; overflow: hidden;
  touch-action: none; cursor: grab;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
.mb-card {
  position: absolute; left: 0; top: 0; display: none;
  background: none; border: none; padding: 0; margin: 0;
  -webkit-tap-highlight-color: transparent; cursor: pointer;
}
.mb-card:focus { outline: none; }
/* No drop shadow. It used to be dropped only while a gesture was running,
   because a big blurred shadow on every visible card is the priciest thing
   on the board to repaint -- but the board reads brighter and cleaner
   without it, so it stays off, and the frames it was costing stay saved. */
.mb-shot {
  display: block; width: 100%; height: 100%; overflow: hidden; border-radius: 3px;
  border: 1px solid rgba(186,208,248,.4); background: rgba(12,17,28,.7);
}
.mb-shot img { display: block; width: 100%; height: 100%; object-fit: fill; }
/* Delays and titles stay the same size on screen however far the board is
   zoomed, so they read as annotation on the board rather than part of it.
   Hidden when the cards are too small for them to mean anything. */
.mb-when, .mb-label {
  position: absolute; left: 0; white-space: nowrap; display: none;
  text-shadow: 0 2px 8px rgba(0,0,0,.95); pointer-events: none;
}
.mb-card.with-caps .mb-when, .mb-card.with-caps .mb-label { display: block; }
.mb-when {
  bottom: 100%; margin-bottom: 2px;
  font-size: 8px; letter-spacing: .16em; text-transform: uppercase; color: #cfe0ff;
}
.mb-label {
  top: 100%; margin-top: 5px;
  font-family: var(--serif); font-style: italic; font-size: 9px; color: #a9b8d2;
}
/* Names sit above what they label and are clipped to its width, so a long one
   is cut short rather than running across the column beside it. */
.mb-tag { position: absolute; left: 0; top: 0; pointer-events: none; overflow: hidden; }
.mb-tag b, .mb-tag i {
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mb-tag b { text-shadow: 0 2px 14px rgba(0,0,0,.95); }
.mb-tag i { font-style: normal; color: #8fa4c6; }
/* zoomed right out there is only room for the brand itself */
.mb-stage.lod-bare .mb-tag-brand i { display: none; }
.mb-tag-brand b { font-family: var(--serif); font-weight: 400; font-size: 17px; color: #fbfdff; letter-spacing: .03em; }
.mb-tag-brand i { font-size: 7px; letter-spacing: .18em; text-transform: uppercase; margin-top: 2px; }
.mb-tag-flow b { font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: #cfe0ff; font-weight: 600; }
.mb-tag-flow i { font-family: var(--serif); font-style: italic; font-size: 8px; margin-top: 1px; }
.mb-zoom {
  position: absolute; z-index: 20; right: 12px; bottom: 10px; display: flex; gap: 5px;
}
.mb-zoom button {
  background: rgba(18,24,38,.86); border: 1px solid rgba(186,208,248,.34);
  color: var(--ink); font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  min-width: 34px; height: 30px; border-radius: 999px; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* Flex-centred rather than left to the button default, which pads
     asymmetrically. text-indent cancels the trailing letter-space the last
     character carries, which otherwise pulls the label a hair left. */
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; line-height: 1; text-indent: .1em;
}
/* Sits in the strip along the very bottom, opposite the zoom buttons, so it
   shares that band rather than covering an email. Carries its own backdrop
   since the board scrolls underneath it. */
.mb-hint {
  position: absolute; z-index: 20; left: 12px; bottom: 10px;
  width: max-content; white-space: nowrap;
  padding: 6px 12px; border-radius: 999px;
  background: rgba(8,11,20,.72); border: 1px solid rgba(186,208,248,.16);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  /* matches the zoom buttons beside it; the tracking is tighter than theirs
     because this is a whole sentence rather than a three-letter label, and
     at .16em it could not hold one line next to them */
  text-align: center; font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  color: #93a6c4; pointer-events: none;
}
/* On a phone the room's own "Work with me" link sits in the bottom right,
   where the zoom buttons and the hint were both landing on top of it. The
   link stays put; the board's own controls move out from under it -- buttons
   to the opposite corner, hint lifted clear above both. */
@media (max-width: 700px) {
  /* Tucked into the strip between that link and the bottom of the screen,
     right-aligned under it. The strip is only as deep as the link's own
     3.4vh offset, and a phone's usable height shrinks further whenever the
     browser bars are showing -- so these are sized to fit the shallowest
     strip a handset realistically gives, rather than relocating on short
     screens, which just moved them out from under the link they belong to. */
  /* Height is the one dimension the strip under the link actually limits,
     so the buttons grow sideways and in type size rather than upward. */
  .mb-zoom { right: 3vw; left: auto; bottom: 1px; gap: 5px; }
  .mb-zoom button { min-width: 32px; height: 20px; font-size: 10px; }
  /* The hint sits at the far left, clear of the link's column entirely, so
     nothing caps its height -- it can take the larger pill outright. */
  .mb-hint { bottom: 1px; left: 3vw; padding: 5px 11px; font-size: 9px; letter-spacing: 0; }
}
/* Small handsets (SE-class, 320-360 wide): the hint is one long unbroken
   line sharing a strip with the zoom buttons, so it is the thing that has
   to give before the two meet. */
@media (max-width: 380px) {
  .mb-hint { font-size: 7px; padding: 4px 8px; }
  /* an SE-class phone is short as well as narrow, so the strip under the
     link is at its shallowest here -- 2px off the height is what fits */
  .mb-zoom button { min-width: 28px; height: 18px; }
}
/* An iOS-style scroll indicator: no track, no chrome, just a soft capsule
   riding the right edge. Apple's scrollers overlay content rather than
   reserving a lane for themselves, so this sits over the board and stays
   quiet enough not to compete with it -- the earlier glass rail read as a
   piece of furniture parked on top of the emails.

   Centred vertically rather than pinned to the top, where it was crossing
   the first flow's cards. The element is wider than the capsule it draws so
   there is still something big enough to tap. */
.mb-map {
  position: absolute; z-index: 20; right: 2px;
  top: 50%; transform: translateY(-50%);
  width: 16px; height: 44vh; cursor: pointer;
  background: none; border: none; box-shadow: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  -webkit-tap-highlight-color: transparent;
}
/* the per-flow ticks belong to the old rail; an Apple scroller shows only
   its own thumb, and the thumb's length already says how much board there is */
.mb-map-seg { display: none; }
.mb-map-view {
  position: absolute; left: 5px; right: 5px; min-height: 26px;
  border-radius: 999px;
  background: rgba(240,244,252,.5);
  /* a hairline of shadow keeps it legible where it crosses a bright email */
  box-shadow: 0 0 0 .5px rgba(0,0,0,.22);
  border: none; backdrop-filter: none; -webkit-backdrop-filter: none;
  pointer-events: none;
}
/* ---------- meta ads room, ad stack ---------- */
.room-body.ads-mode { position: absolute; inset: 0; padding: 0; overflow: hidden; }
.as-root {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  --as-t: .32s;
}
/* Was 15vh, which spent a sixth of the screen on air above a progress bar
   and pushed the card most of the way down the phone. */
/* Clears the room's title plaque, which sits at top:5vh and is 42px tall --
   at 9vh the progress bar was running straight through it. */
.as-top { padding: 13.5vh 6vw 0; flex: 0 0 auto; }
.as-progress { display: flex; gap: 4px; margin-bottom: 10px; }
.as-dash {
  flex: 1 1 0; height: 2.5px; border-radius: 2px; background: rgba(255,255,255,.25);
}
.as-dash.on { background: #eef2fb; }
.as-brandline {
  display: flex; align-items: baseline; justify-content: space-between;
  text-shadow: 0 2px 10px rgba(0,0,0,.9);
}
.as-brandline b {
  font-family: var(--serif); font-weight: 400; font-size: 15px; letter-spacing: .03em; color: #fbfdff;
}
.as-brandline span { font-size: 10px; letter-spacing: .1em; color: #8fa4c6; }
/* The deck: current card centred and full size, the next couple sitting
   behind it peeking in from the right edge (and one behind on the left, so
   swiping back reads as possible too) is the whole pitch of this screen --
   it has to be obvious without the caption underneath being read first. */
/* Fills whatever vertical space the brand/description text above and the
   angle label below don't need -- a tall phone leaves it most of the
   screen, a wide desktop window leaves it much less, and either way the
   card inside sizes itself to fit (see measureCardSide in the JS, which
   reads this box's real rendered width AND height and takes whichever is
   tighter, so the square card is never taller than what's actually left). */
.as-deck {
  position: relative; flex: 1 1 auto; min-height: 0; margin: 14px 0;
  /* none, not pan-y. The room is overflow:hidden and nothing here scrolls, so
     pan-y bought nothing while still leaving the browser to arbitrate every
     touch -- and a thumb never swipes perfectly level, so it kept claiming
     the gesture as a vertical pan and cancelling the pointer mid-drag. That
     is the swipe that "does not register". */
  touch-action: none; cursor: grab;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  perspective: 1200px;   /* gives the cards behind real depth instead of a flat shrink */
}
/* The ads are square. --as-side is set by the JS from the deck's own
   measured box (see measureCardSide) rather than a CSS percentage, because
   a percentage can only ever look at one axis (width OR height) and this
   card has to fit inside whichever one is actually the tighter fit. */
.as-card {
  position: absolute; top: 50%; left: 50%;
  width: var(--as-side); height: var(--as-side);
  border-radius: 14px; overflow: hidden; background: rgba(12,17,28,.7);
  border: 1px solid rgba(186,208,248,.28);
  box-shadow: 0 24px 70px rgba(0,0,0,.65);
  transform-origin: center center;
  /* The overshoot curve is what makes a landed swipe feel like a snap into
     place rather than a card just gliding to a stop -- it runs a hair past
     its final spot and settles back. Opacity keeps a plain ease; overshooting
     opacity would flicker the card brighter than it should ever be. */
  transition: transform var(--as-t) cubic-bezier(.34, 1.56, .64, 1), opacity var(--as-t) ease;
  will-change: transform, opacity;
}
.as-card img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The angle sits in the gap between the progress bar and the card, naming the
   ad you are about to look at. */
.as-angle {
  display: block; flex: 0 0 auto; padding: 10px 7vw 9px; text-align: center;
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: #cfe0ff;
  text-shadow: 0 2px 10px rgba(0,0,0,.9);
}
/* The brand's sentence sits under the card. The bottom padding keeps it clear
   of "Work with me", which is pinned to this room's bottom-right corner. */
.as-below { flex: 0 0 auto; padding: 11px 7vw 8vh; text-align: center; }
.as-desc {
  display: block;
  font-family: var(--serif); font-style: italic;
  font-size: 13px; line-height: 1.4; color: #eef2fb; text-shadow: 0 2px 10px rgba(0,0,0,.9);
}
/* Fires once a swipe actually lands on a different brand, not on every card --
   flashes the name big enough to register before the caption underneath is
   ever read, the way stories mark a new account rather than a new post. */
.as-flash {
  position: absolute; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center;
  pointer-events: none; opacity: 0; padding: 0 8vw; text-align: center;
  background: rgba(4,6,12,.4);
  font-family: var(--serif); font-weight: 400; font-size: min(15vw, 15vh); letter-spacing: .03em; color: #fbfdff;
  text-shadow: 0 4px 30px rgba(0,0,0,.9);
  transition: opacity .35s ease;
}
.as-flash.show { opacity: 1; transition: opacity .15s ease; }

/* ---------- email room: the canvas ---------- */
/* dragging the board is a camera move, never a text selection */
.cv-stage, .cv-stage *, .rd-stage, .rd-stage * {
  user-select: none; -webkit-user-select: none; -ms-user-select: none;
  -webkit-touch-callout: none;
}
.cv-stage img, .rd-stage img { -webkit-user-drag: none; }
.cv-stage { position: absolute; inset: 0; overflow: hidden; cursor: grab; touch-action: none; }
.cv-stage.grabbing { cursor: grabbing; }
/* A column of brands, each brand a row of its flows. Adding another brand is
   one more group in the data, and it lands underneath with room around it. */
/* No will-change here on purpose. The board is wider than Chrome's 16384px
   maximum texture dimension, and promoting it to one composited layer makes
   the browser drop raster tiles, which is what blanked the previews at low
   zoom. Without it the visible region is painted on demand; smoothness comes
   from batching the writes into one frame, not from a promoted layer. */
/* Every board dimension is expressed against these so the whole thing can be
   laid out smaller without changing a single proportion. A phone gets a third
   of the desktop size (set from main.js): the board's on-screen appearance is
   identical, because it is only ever seen through a transform: scale() that
   compensates, but each preview's LAYOUT box drops from ~600x2040 to ~200x680.
   That matters because the browser sizes an image's decode buffer from its
   layout box, not from how small the transform paints it -- at desktop size
   the 55 previews reserve ~240MB, which on iOS (higher DPR) is well past what
   Safari will let a tab hold before killing it. */
.cv-world {
  --mail-w: 600px;
  --row-gap: 150px;
  --flow-gap: 1200px;
  --brand-gap: 2100px;
  --pad-y: 400px;
  --pad-x: 300px;
  position: absolute; top: 0; left: 0; transform-origin: 0 0;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--brand-gap);
  padding: var(--pad-y) var(--pad-x);
  --inv: 1;
}
.cv-brand { position: relative; }
.cv-brand-row { display: flex; align-items: flex-start; gap: var(--flow-gap); }
.cv-brand-name {
  position: absolute; left: 0; bottom: 100%; white-space: nowrap;
  /* translate inside the scale, so the gap under the label is a constant number
     of screen pixels instead of shrinking away as you zoom out */
  /* The brand name, the flow name and the first delay label all hang off one
     line, the top of the cards, so each has to clear the whole stack below it.
     The offsets are computed from the real rendered heights on build rather
     than guessed here; these values are only a sane starting point. */
  transform: scale(var(--inv)) translateY(calc(-1 * var(--brand-off, 88px)));
  transform-origin: 0 100%;
}
.cv-brand-name b {
  display: block; font-weight: 400; font-family: var(--serif); font-size: 26px;
  letter-spacing: .04em; color: #fbfdff; text-shadow: 0 2px 14px rgba(0,0,0,.95);
}
.cv-brand-name i {
  display: block; margin-top: 3px; font-style: normal; font-size: 10px;
  letter-spacing: .3em; text-transform: uppercase; color: #8fa3c4; text-shadow: 0 2px 10px rgba(0,0,0,.95);
}
/* zoomed far out only the frame names survive, the way a design tool drops
   detail rather than letting labels pile on top of each other */
.cv-world.far .cv-when,
.cv-world.far .cv-label,
.cv-world.far .cv-frame-note { opacity: 0; }
.cv-flow { position: relative; }
.cv-frame-name {
  position: absolute; left: 0; bottom: 100%;
  transform: scale(var(--inv)) translateY(calc(-1 * var(--frame-off, 34px)));
  transform-origin: 0 100%;
  white-space: nowrap; text-align: left;
}
.cv-frame-name b {
  display: block; font-weight: 400; font-size: 15px; letter-spacing: .22em; text-transform: uppercase;
  color: #eef4ff; text-shadow: 0 2px 10px rgba(0,0,0,.95);
}
.cv-frame-name i {
  display: block; margin-top: 4px; font-family: var(--serif); font-size: 12px; color: #9fb0cd;
  text-shadow: 0 2px 10px rgba(0,0,0,.95);
}
.cv-frame-note {
  position: absolute; left: 50%; top: 100%;
  /* Anchored to the flow's centre line, then pulled back by half its own width
     before the counter scale is applied, so it stays centred under the flow at
     every zoom. Clears the label hanging under the tallest card in the row. */
  transform: scale(var(--inv)) translate(-50%, var(--note-off, 34px));
  transform-origin: 0 0; white-space: nowrap;
  font-family: var(--serif); font-style: italic; font-size: 12px; color: #8b9ab8;
  text-shadow: 0 2px 10px rgba(0,0,0,.95);
}
.cv-row { display: flex; align-items: flex-start; gap: var(--row-gap); }
.cv-mail {
  position: relative; flex: 0 0 auto; width: var(--mail-w);
  background: none; border: none; padding: 0; cursor: pointer; display: block;
}
.cv-when, .cv-label {
  position: absolute; left: 0; white-space: nowrap;
  text-shadow: 0 2px 8px rgba(0,0,0,.95); transition: opacity .25s;
}
.cv-when {
  bottom: 100%; transform: scale(var(--inv)) translateY(calc(-1 * var(--when-off, 9px)));
  transform-origin: 0 100%;
  font-size: 11px; letter-spacing: .18em; text-transform: uppercase; color: #cfe0ff;
}
.cv-label {
  top: 100%; transform: scale(var(--inv)) translateY(var(--label-off, 9px));
  transform-origin: 0 0;
  font-family: var(--serif); font-style: italic; font-size: 12px; color: #a9b8d2;
}
.cv-shot {
  display: block; width: 100%; overflow: hidden; border-radius: 4px;
  border: 1px solid rgba(186,208,248,.4); background: rgba(12,17,28,.7);
  box-shadow: 0 30px 90px rgba(0,0,0,.6);
  transition: border-color .3s, box-shadow .3s;
}
.cv-mail:hover .cv-shot { border-color: rgba(236,246,255,.95); box-shadow: 0 30px 90px rgba(0,0,0,.6), 0 0 0 6px rgba(150,190,255,.18); }
/* the preview is the whole email at the same aspect as the box, so it is never cropped */
.cv-shot img { display: block; width: 100%; height: 100%; object-fit: fill; }
.cv-zoom {
  position: absolute; z-index: 20; right: 16px; bottom: 12px; display: flex; gap: 5px;
  background: rgba(10,15,26,.8); border: 1px solid rgba(160,185,230,.25); border-radius: 18px; padding: 5px;
  backdrop-filter: blur(6px);
}
.cv-zoom button {
  cursor: pointer; background: none; border: none; color: #c8d5ec;
  padding: 5px 11px; border-radius: 14px; font-size: 11.5px; letter-spacing: .06em;
  transition: background .25s, color .25s;
}
.cv-zoom button:hover { background: rgba(60,80,120,.6); color: #fff; }
/* the hint sits over the board, so it carries its own backing rather than
   letting a flow note read through it */
.cv-hint {
  position: absolute; z-index: 20; left: 50%; transform: translateX(-50%); bottom: 18px;
  max-width: calc(100% - 260px); text-align: center; pointer-events: none;
  padding: 5px 14px; border-radius: 13px;
  background: rgba(10,15,26,.8); backdrop-filter: blur(6px);
  color: #7f8ea9; font-size: 9.5px; letter-spacing: .16em; text-transform: uppercase;
}

/* ---------- handheld: drop the live blurs only where things move ----------
   backdrop-filter makes the compositor snapshot and blur whatever is behind
   an element every frame that changes. For a card being dragged that is every
   frame, and Android does not absorb it the way iOS does -- which is what
   made the results deck and the ad stack feel sticky there.

   It comes off ONLY those moving cards. Everything static -- the sky tiles,
   the panes, the form -- keeps its blur, because that blur is the entire
   reason those panels look frosted rather than see-through, and turning it
   off changed how the sky read. Their cost is a blur that settles once. */
@media (pointer: coarse) {
  .float-card, .as-card, .df-shot, .drum-focus {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }
  /* A shadow this large is repainted on every frame of a drag as well; the
     border and the fill carry the card's edge on their own. */
  .float-card { box-shadow: 0 8px 24px rgba(0,0,0,.5); }
  .df-shot, .as-card { box-shadow: 0 12px 34px rgba(0,0,0,.6); }

  /* text-shadow and box-shadow cannot be composited, so animating them
     repaints their element every frame, forever. The static state reads the
     same. */
  .whisper-link, .seat-whispers a {
    animation: none;
    text-shadow: 0 0 6px rgba(255,255,255,.2), 0 1px 6px rgba(0,0,0,.8);
  }
}
