/* LN Barbershop — TV display for a vertical screen (a regular TV turned on its side).
   It fills the whole screen, about 5px in from every edge:
     rolling film down both sides, with lamps lighting up in the sprocket holes;
     between the strips, top to bottom: the slideshow, the "Work by" banner, and the booking bar.
   Sizes are container units (inside #stage 1cqw = 1% of the screen width; inside #hero,
   of the slideshow box), plus a few whole-pixel film sizes set by screen.js so that the
   frames, the holes and the lamps line up exactly. */

:root {
  --gold: #d4af37;
  --gold-hi: #f6e3a1;
  --gold-lo: #9c7a1c;
}

* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; background: #000; overflow: hidden; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: #fff;
  cursor: none;
}
body.awake { cursor: default; }

/* On the vertical TV the stage is the whole screen. In a landscape window (a laptop
   preview) it shows a centred 9:16 picture instead. */
#stage {
  position: absolute; left: 50%; top: 50%;
  width: 100vw; height: 100vh;
  transform: translate(-50%, -50%) scale(var(--inset-scale, 1)); /* --inset-scale: pulled in for TVs that crop the edges */
  overflow: hidden; background: #000;
  container-type: size;
  --edge: 5px;                                            /* in from the screen edges */
  --gap: 5px;                                             /* between film, slideshow, banner and bar */
  --rollw: 14cqw;                                         /* each film strip */
  --chanL: calc(var(--edge) + var(--rollw) + var(--gap)); /* the channel between the strips */
  --chanW: calc(100cqw - 2 * (var(--edge) + var(--rollw) + var(--gap)));
  --bannerH: 9.5cqw;
  --footerH: 24cqw;
}
@media (orientation: landscape) {
  #stage { width: min(100vw, 56.25vh); height: min(100vh, 177.78vw); }
}
/* ?rotate=90 or ?rotate=270 — the player outputs landscape, so lay the stage out tall and turn it. */
body.rot90 #stage, body.rot270 #stage { width: 100vh; height: 100vw; }
body.rot90 #stage { transform: translate(-50%, -50%) rotate(90deg) scale(var(--inset-scale, 1)); }
body.rot270 #stage { transform: translate(-50%, -50%) rotate(-90deg) scale(var(--inset-scale, 1)); }

.gold-text {
  background: linear-gradient(160deg, var(--gold-hi) 0%, var(--gold) 42%, var(--gold-lo) 68%, var(--gold-hi) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* ===== the film rolls =====
   Ported from the event photo walls: plain overflow boxes scrolled from JS (no
   transform animation, which Safari refused to paint), the list drawn twice so the
   loop closes on identical pixels, and plain gradient overlays for the soft ends.
   Every frame is the same size, a 3:4 picture with the same gap after it, and each
   frame spans exactly four sprocket holes, so holes and lamps sit the same way on
   every frame all the way round. --perf, --pic-h, --gap-top, --gap-bottom and --tile
   are whole pixels set by screen.js; the fallbacks only cover the first instant. */
.rollwrap { position: absolute; top: 0; bottom: 0; width: var(--rollw); overflow: hidden; background: #1b140e; z-index: 2; }
.rollwrap.left { left: var(--edge); }
.rollwrap.right { right: var(--edge); }
.rollwrap::before, .rollwrap::after { content: ""; position: absolute; left: 0; right: 0; height: 12cqw; z-index: 3; pointer-events: none; }
.rollwrap::before { top: 0; background: linear-gradient(#000, rgba(0, 0, 0, 0)); }
.rollwrap::after { bottom: 0; background: linear-gradient(rgba(0, 0, 0, 0), #000); }
.roll { position: absolute; inset: 0; overflow: hidden; }
.film {
  position: relative;
  background-color: #1b140e;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34' height='54' viewBox='0 0 34 54' preserveAspectRatio='none'%3E%3Crect x='8' y='17' width='18' height='20' rx='4' fill='%23e8e2d2'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34' height='54' viewBox='0 0 34 54' preserveAspectRatio='none'%3E%3Crect x='8' y='17' width='18' height='20' rx='4' fill='%23e8e2d2'/%3E%3C/svg%3E");
  background-position: left top, right top;
  background-repeat: repeat-y, repeat-y;
  background-size: var(--perf, 2.3cqw) var(--tile, 3.6cqw), var(--perf, 2.3cqw) var(--tile, 3.6cqw);
}
.film .fr { padding: var(--gap-top, .6cqw) var(--perf, 2.3cqw) var(--gap-bottom, .6cqw); }
.film .pic {
  position: relative; height: var(--pic-h, 12.6cqw); overflow: hidden; background: #000;
  box-shadow: 0 0 0 1px rgba(232, 226, 210, .18);
}
/* Photos and clips fill their frame exactly (cropped to fit, never stretched). */
.film .pic img, .film .pic video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: .9; }

/* ===== the light show in the sprocket holes =====
   From the event photo walls: the holes ARE the lamps. A gate sits over each strip with
   one column of lamps per edge. screen.js shifts each column by the film's scroll position
   modulo one hole, so a lamp always sits on its hole while the film runs past. It is
   decoration only: ?lights=off hides it, and it can never stop the film. */
.gate { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
body.nolights .gate { display: none; }
.gate i { position: absolute; top: 0; bottom: 0; width: var(--perf, 2.3cqw); overflow: hidden; }
.gate i.a { left: 0; }
.gate i.b { right: 0; }
.gate span { position: absolute; left: 0; right: 0; top: 0; bottom: 0; will-change: transform; }
/* Sized and placed like the hole in the tile: 18 wide at x 8, 20 tall at y 17, in 34 x 54. */
.gate b {
  position: absolute; left: 23.5%; width: 53%; height: var(--holeh, 8px);
  border-radius: 22% / 18%; background: #fff6d8; opacity: 0;
  box-shadow: 0 0 5px 1px rgba(255, 222, 150, .95),
              0 0 16px 5px rgba(255, 190, 90, .6),
              0 0 34px 12px rgba(255, 170, 60, .28);
  animation-timing-function: linear; animation-iteration-count: infinite;
}
/* What ONE lamp does in each show; screen.js staggers them. A travelling show's cycle is
   the time the pattern takes to cross every lamp, so it never leaves dead air. */
@keyframes lampWave { 0% { opacity: 0; } 8% { opacity: 1; } 22% { opacity: .4; } 34% { opacity: 0; } 100% { opacity: 0; } }        /* onda */
@keyframes lampSnake { 0% { opacity: 0; } 6% { opacity: .9; } 30% { opacity: 1; } 45% { opacity: 0; } 100% { opacity: 0; } }      /* serpiente */
@keyframes lampComet { 0% { opacity: 0; } 2% { opacity: 1; } 6% { opacity: .5; } 12% { opacity: .14; } 18% { opacity: 0; } 100% { opacity: 0; } } /* cometa */
@keyframes lampFall { 0% { opacity: 0; } 3% { opacity: 1; } 9% { opacity: .35; } 16% { opacity: 0; } 100% { opacity: 0; } }        /* cascada */
@keyframes lampTide { 0% { opacity: 0; } 10% { opacity: .85; } 40% { opacity: 1; } 70% { opacity: .2; } 82% { opacity: 0; } 100% { opacity: 0; } } /* marea */
@keyframes lampAlt { 0%, 44% { opacity: 1; } 52%, 100% { opacity: .05; } }                                                         /* alterna */
@keyframes lampVals { 0% { opacity: 1; } 9% { opacity: .08; } 33% { opacity: .6; } 42% { opacity: .08; } 66% { opacity: .6; } 75% { opacity: .08; } 100% { opacity: 1; } } /* vals */
@keyframes lampPulse { 0%, 100% { opacity: .06; } 50% { opacity: 1; } }                                                            /* latido */
@keyframes lampTwinkle { 0% { opacity: 0; } 8% { opacity: 1; } 20% { opacity: .12; } 34% { opacity: .75; } 52% { opacity: 0; } 100% { opacity: 0; } } /* destello */
@keyframes lampSpark { 0% { opacity: 0; } 3% { opacity: 1; } 10% { opacity: .28; } 19% { opacity: 0; } 100% { opacity: 0; } }      /* chispa */
@keyframes lampBlink { 0% { opacity: 0; } 4% { opacity: 1; } 10% { opacity: .35; } 17% { opacity: 0; } 100% { opacity: 0; } }  /* random: one blink */
@keyframes lampBlink2 { 0% { opacity: 0; } 4% { opacity: 1; } 9% { opacity: .08; } 14% { opacity: 1; } 20% { opacity: .3; } 27% { opacity: 0; } 100% { opacity: 0; } }  /* random: a double blink */
@media (prefers-reduced-motion: reduce) { .gate { display: none; } }

/* ===== the slideshow box: fills the channel from the top edge down to the banner ===== */
#hero {
  position: absolute; z-index: 3;
  left: var(--chanL); top: var(--edge); width: var(--chanW);
  height: calc(100cqh - 2 * var(--edge) - 2 * var(--gap) - var(--bannerH) - var(--footerH));
  overflow: hidden; border-radius: 1.2cqw; background: #000;
  box-shadow: 0 0 0 .25cqw rgba(212, 175, 55, .35), 0 2cqw 6cqw rgba(0, 0, 0, .6);
  container-type: size;
}
#slides { position: absolute; inset: 0; }

/* ===== the banner: whose work is on screen ===== */
#banner {
  position: absolute; z-index: 3;
  left: var(--chanL); width: var(--chanW);
  bottom: calc(var(--edge) + var(--footerH) + var(--gap)); height: var(--bannerH);
  display: flex; align-items: center; gap: 2cqw; padding: 0 2cqw;
  border-radius: 1.3cqw; background: linear-gradient(90deg, #141414, #0a0a0a);
  border: .25cqw solid var(--accent, var(--gold));
  box-shadow: 0 1.5cqw 5cqw rgba(0, 0, 0, .6);
  overflow: hidden; /* keeps the stars and the glint inside the box */
  transition: opacity .35s ease;
}
#banner.swap, #banner.empty { opacity: 0; }
.banner-logo, .banner-text { position: relative; z-index: 1; }
/* A dark halo round the words, so a star drifting behind them fades out instead of cutting through a letter. */
.banner-text { text-shadow: 0 0 .5cqw #0d0d0d, 0 0 1.2cqw #0d0d0d; }
.banner-logo { flex: none; display: flex; align-items: center; }
.banner-logo img { display: block; height: 6.8cqw; width: auto; max-width: 16cqw; border-radius: 1cqw; background: #000; }
.banner-logo img.round { width: 6.8cqw; border-radius: 50%; }
.banner-text { min-width: 0; display: flex; flex-direction: column; gap: .4cqw; }
.banner-label { font-size: 1.45cqw; font-weight: 700; letter-spacing: .28em; text-transform: uppercase; color: rgba(255, 255, 255, .55); }
.banner-name {
  font-size: 3.2cqw; font-weight: 900; line-height: 1; letter-spacing: .06em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--accent, var(--gold));
}
.banner-tagline { font-size: 1.75cqw; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: rgba(255, 255, 255, .78); }
.banner-name.long { font-size: 2.6cqw; }
/* A barber's Instagram, in small Instagram-coloured letters after their name (written lowercase, as
   Instagram writes it). No dark halo here: it would show through the see-through letters. */
.banner-ig {
  margin-left: .55em; font-size: .6em; font-weight: 800; letter-spacing: .01em; text-transform: none;
  background: linear-gradient(90deg, #feda75, #fa7e1e 30%, #e1306c 62%, #c13584 80%, #a347d4);
  -webkit-background-clip: text; background-clip: text; color: transparent; text-shadow: none;
}

/* The shine: small stars drifting about, each on its own clock (screen.js scatters them and
   tints them to the business's colour), and a glint of light sweeping across every few seconds. */
.banner-stars, .slide-stars { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
/* The page's own background. The film and the slideshow cover the rest of the screen, so the stars
   live in the strip behind the banner and the booking bar. */
#sky {
  position: absolute; z-index: 1; pointer-events: none;
  left: var(--chanL); width: var(--chanW); bottom: 0;
  height: calc(var(--edge) + var(--footerH) + 2 * var(--gap) + var(--bannerH));
}
.stars i {
  position: absolute; left: var(--x); top: var(--y); width: var(--s); height: var(--s);
  margin: calc(var(--s) / -2) 0 0 calc(var(--s) / -2);
  animation: starDrift var(--drift) ease-in-out var(--delay) infinite alternate;
}
.stars i::before {
  content: ""; position: absolute; inset: 0;
  background: var(--star) center / contain no-repeat;
  animation: starTwinkle var(--twinkle) ease-in-out var(--delay) infinite;
}
@keyframes starDrift {
  0% { transform: translate(0, 0); }
  35% { transform: translate(var(--dx1), var(--dy1)); }
  70% { transform: translate(var(--dx2), var(--dy2)); }
  100% { transform: translate(var(--dx3), var(--dy3)); }
}
@keyframes starTwinkle {
  0%, 100% { opacity: .12; transform: scale(.45) rotate(-12deg); }
  50% { opacity: 1; transform: scale(1) rotate(12deg); }
}
#banner::after {
  content: ""; position: absolute; top: -30%; bottom: -30%; left: 0; width: 20%; z-index: 2; pointer-events: none;
  background: linear-gradient(90deg, rgba(255, 244, 205, 0), rgba(255, 244, 205, .08) 30%, rgba(255, 244, 205, .3) 50%, rgba(255, 244, 205, .08) 70%, rgba(255, 244, 205, 0));
  transform: translateX(-130%) skewX(-20deg);
  animation: bannerGlint 6.5s ease-in-out 1s infinite;
}
@keyframes bannerGlint {
  0% { transform: translateX(-130%) skewX(-20deg); }
  32%, 100% { transform: translateX(630%) skewX(-20deg); }
}
@media (prefers-reduced-motion: reduce) { .stars, #banner::after { display: none; } }

/* The type of haircut: a tag in the bottom right of the box. */
.banner-cut {
  position: absolute; right: 1.6cqw; bottom: .7cqw; z-index: 1; max-width: 41%;
  padding: .4cqw 1.05cqw; border-radius: 99px;
  border: .2cqw solid color-mix(in srgb, var(--accent, var(--gold)) 62%, transparent);
  background: rgba(0, 0, 0, .62);
  font-size: 2.15cqw; font-weight: 800; line-height: 1.16; letter-spacing: .01em;
  color: var(--accent, var(--gold));
  text-shadow: 0 0 .5cqw #0d0d0d, 0 0 1.2cqw #0d0d0d;
  /* Izzy's descriptions run longer than a haircut's name, so it wraps to a second line
     instead of trailing off with an ellipsis. */
  text-align: right; overflow: hidden;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}
/* `display` above beats the browser's own rule for [hidden], so say it again here: no
   empty pill on the banners that have nothing to say. */
.banner-cut[hidden] { display: none; }
/* The tag is wider than the old one-line version, so the words step aside for it. */
#banner.has-cut .banner-text { padding-right: 42%; }
/* Posters and the booking slide say everything themselves, so screen.js hides the whole banner
   (the .empty state above) and the page's stars show in its place. */

/* ===== the bottom bar: book your next visit =====
   Same idea as the event walls' footer: the words and the code read as one
   thing, the words right-aligned so they run straight into the QR card. The
   code is black on white, square-cornered, with the card supplying the margin. */
#footer {
  position: absolute; z-index: 3;
  left: var(--chanL); width: var(--chanW); bottom: var(--edge); height: var(--footerH);
  display: flex; align-items: center; justify-content: center; gap: 2.4cqw; padding: 0 2cqw;
}
/* The words sit straight on the page's starry background, with a dark halo so a star drifting
   behind them never eats a letter. */
#footcta { flex: 0 1 auto; min-width: 0; text-align: right; filter: drop-shadow(0 0 .5cqw rgba(0, 0, 0, .95)); }

/* ===== making room =====
   A poster grows down into the space the banner leaves, and the booking slide takes the height of
   the screen. The slideshow box always stays between the film strips: it never covers the film.
   Everything moves together, so nothing pops in or out. screen.js puts these classes on the stage. */
#hero, #banner, #footer {
  transition: opacity .4s ease, transform .55s cubic-bezier(.4, 0, .2, 1), height .55s cubic-bezier(.4, 0, .2, 1);
}
#stage.no-banner #hero { height: calc(100cqh - 2 * var(--edge) - var(--gap) - var(--footerH)); }
#stage.no-banner.no-footer #hero { height: calc(100cqh - 2 * var(--edge)); }
#stage.no-banner #banner { opacity: 0; transform: translateY(1.5cqw) scale(.98); pointer-events: none; }
#stage.no-footer #footer { opacity: 0; transform: translateY(3cqw); pointer-events: none; }
@media (prefers-reduced-motion: reduce) { #hero, #banner, #footer { transition: none; } }
#footcta .big { font-size: 3.7cqw; font-weight: 900; line-height: 1.02; letter-spacing: .02em; text-transform: uppercase; }
#footcta .sub { margin-top: 1cqw; font-size: 1.8cqw; font-weight: 600; color: #fdf6e4; }
#footcta .url { margin-top: .6cqw; font-size: 1.35cqw; letter-spacing: .02em; color: var(--gold); white-space: nowrap; }
/* The code sits on a white card inside a coloured frame: the frame is the padding, and the colours
   spin behind it. */
#qrbox {
  position: relative; flex: none; line-height: 0; overflow: hidden;
  padding: var(--ring); border-radius: var(--ring-radius); background: #0b0b0b;
  box-shadow: 0 1.5cqw 4cqw rgba(0, 0, 0, .6), 0 0 1.6cqw rgba(140, 90, 255, .3), 0 0 3.4cqw rgba(0, 140, 255, .18);
  --ring: .5cqw; --ring-radius: 1.6cqw;
}
#qrbox .plate { position: relative; padding: 1.1cqw; border-radius: 1.1cqw; background: #fff; }
#qrbox img { display: block; width: 19cqw; height: 19cqw; border-radius: 0; image-rendering: pixelated; }
/* The shop's logo in the middle of the code, on a white ring. The code is made with the highest
   error correction, so it still scans with the centre covered. */
#qrbox img.qr-logo {
  position: absolute; left: 50%; top: 50%; width: 4.6cqw; height: 4.6cqw; margin: -2.3cqw 0 0 -2.3cqw;
  border-radius: 50%; background: #000; box-shadow: 0 0 0 .5cqw #fff; image-rendering: auto;
}

/* ===== the colour ring round the QR codes =====
   A square of colour spins behind the white card, clipped to a rounded frame, so the colours
   travel round the edge of the code. Turning a layer costs the browser nothing per frame, unlike
   repainting a gradient; the glow outside is a plain shadow, so nothing is blurred as it moves. */
.ring { position: absolute; inset: 0; border-radius: inherit; overflow: hidden; }
.ring i {
  position: absolute; top: -30%; left: -30%; width: 160%; height: 160%; display: block;
  background: conic-gradient(#0894ff, #c959dd, #ff2e54, #ff9004, #ffd60a, #0894ff);
  animation: ringSpin 4.5s linear infinite;
}
@keyframes ringSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .ring i { animation: none; } }

/* ===== slides (sizes relative to #hero) ===== */
.slide { position: absolute; inset: 0; opacity: 0; overflow: hidden; background: #000; transition: opacity .9s ease; }
.slide.show { opacity: 1; }

.backdrop {
  position: absolute; inset: -8%;
  background: center / cover no-repeat;
  filter: blur(5cqw) brightness(.42) saturate(1.15);
}
.media { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.slide.fill .media { object-fit: cover; }
.slide.photo .media { animation: drift var(--hold, 7s) ease-out both; }
@keyframes drift { from { transform: scale(1); } to { transform: scale(1.06); } }

/* A quiet word in the corner when somebody steers the slideshow by hand. Nobody presses
   keys at the shop, so this only ever shows while the TV is being set up. */
#cue {
  position: absolute; z-index: 6; left: 50%; bottom: 3.2cqh; transform: translateX(-50%);
  padding: .9cqh 2.2cqh; border-radius: 99px;
  background: rgba(8, 8, 8, .82); border: 1px solid rgba(212, 175, 55, .5); color: #f6ecd2;
  font-size: 1.9cqh; font-weight: 700; letter-spacing: .04em; white-space: nowrap;
  opacity: 0; transition: opacity .3s ease;
}
#cue.show { opacity: 1; }

/* ===== what's for sale in the picture =====
   The tag belongs to the photograph, not to the black beside it, so .tagbox takes the
   picture's own shape: aspect-ratio with both maximums set fits it exactly the way
   object-fit: contain fits the image, at any window size, with no measuring in script.
   A picture that fills the slide has no black beside it, so the box is the whole slide. */
.tagbox {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  aspect-ratio: var(--ar, 1); pointer-events: none; z-index: 2;
}
/* Which side of the picture runs out of room first decides which measurement is the real one.
   Setting both and leaning on max-width/max-height instead looks right in Safari and comes out
   the full width of the box in Chrome, so screen.js says which, and says it again on a resize. */
.tagbox.bywidth { width: 100%; height: auto; }
.tagbox.byheight { height: 100%; width: auto; }
.slide.fill .tagbox, .tagbox.unknown { aspect-ratio: auto; width: 100%; height: 100%; }
.tag { position: absolute; right: 3.2%; bottom: 2.6%; transform-origin: 50% 0; }

/* One thing for sale: a scrap torn off the pad and laid on the corner. screen.js gives each
   picture its own ragged edge. The words are handwritten where the player has a hand on it. */
.tag.scrap {
  background: #f2ebda; color: #20180e; padding: 1.6cqw 2.6cqw 1.9cqw; max-width: 62%;
  filter: drop-shadow(0 .8cqw 1.5cqw rgba(0, 0, 0, .62));
  transform: rotate(-1.8deg);
  animation: tagPlaced .55s cubic-bezier(.2, 1.5, .5, 1) .45s both;
}
/* paper: a grain across it and the light falling from the top left */
.tag.scrap::before {
  content: ""; position: absolute; inset: 0;
  background:
    repeating-linear-gradient(94deg, rgba(0, 0, 0, .03) 0 .2cqw, rgba(255, 255, 255, 0) .2cqw .5cqw),
    radial-gradient(120% 90% at 12% 8%, rgba(255, 255, 255, .72), rgba(0, 0, 0, .06) 72%);
}
.tag.scrap .what {
  position: relative; font-family: "Bradley Hand", "Segoe Print", "Chalkboard SE", cursive;
  font-size: 2.3cqw; font-weight: 700; line-height: 1.1; color: #4a3c28;
}
.tag.scrap .price {
  position: relative; font-family: "Bradley Hand", "Segoe Print", "Chalkboard SE", cursive;
  font-size: 5.4cqw; font-weight: 800; line-height: 1; margin-top: .5cqw;
}
@keyframes tagPlaced {
  from { opacity: 0; transform: translateY(-2.4cqw) rotate(-7deg) scale(1.05); }
  to   { opacity: 1; transform: translateY(0) rotate(-1.8deg) scale(1); }
}

/* A whole outfit: a swing ticket off the rail, a line for each piece, dropped in and settling. */
.tag.ticket {
  transform-origin: 3.2cqw -4.2cqw;                 /* it swings from where the string is tied */
  animation: tagSwing 1.15s cubic-bezier(.25, .8, .3, 1) .4s both;
  max-width: 66%;
}
.tag.ticket .string {
  position: absolute; left: 2.85cqw; top: -4.4cqw; width: .38cqw; height: 4.8cqw;
  background: linear-gradient(rgba(235, 222, 186, 0), rgba(235, 222, 186, .95));
  filter: drop-shadow(0 0 .3cqw rgba(0, 0, 0, .7));
}
.tag.ticket .card {
  position: relative; border-radius: 1.2cqw; padding: 1.5cqw 2.4cqw 1.6cqw 5.2cqw;
  background: linear-gradient(150deg, #fffaec, #efe3c4); color: #20180e;
  box-shadow: 0 1cqw 2cqw rgba(0, 0, 0, .62), inset 0 0 0 .3cqw rgba(212, 175, 55, .9);
}
.tag.ticket .hole {
  position: absolute; left: 2.2cqw; top: 50%; width: 1.4cqw; height: 1.4cqw; margin-top: -.7cqw;
  border-radius: 50%; background: rgba(0, 0, 0, .82); box-shadow: inset 0 .15cqw .3cqw rgba(0, 0, 0, .8);
}
.tag.ticket .line {
  display: flex; align-items: baseline; justify-content: space-between; gap: 2.6cqw;
  padding: .45cqw 0;
}
.tag.ticket .line + .line { border-top: 1px dashed rgba(74, 60, 40, .28); }
.tag.ticket .line b {
  font-size: 2cqw; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: #4a3c28;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tag.ticket .line i { font-style: normal; font-size: 3.2cqw; font-weight: 800; white-space: nowrap; }
@keyframes tagSwing {
  0%   { opacity: 0; transform: translateY(-5cqw) rotate(-15deg); }
  35%  { opacity: 1; transform: translateY(0) rotate(7deg); }
  62%  { transform: rotate(-2.5deg); }
  84%  { transform: rotate(1.4deg); }
  100% { transform: rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .tag.scrap, .tag.ticket { animation: none; }
}

/* ===== booking slide (every 7th slide, with the whole screen to itself) =====
   Every size is measured against the height of the box as well as its width, so the whole card
   always fits: the tall screen of the TV, and any shape of browser window on a computer. */
.slide.booking {
  display: flex;
  background: radial-gradient(120% 70% at 50% 40%, #14110a 0%, #08070a 55%, #000 100%);
}
.booking-card {
  margin: auto; width: 92%; max-height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center;
  gap: min(3.4cqw, 2.2cqh); padding: min(3cqw, 2cqh) 0;
}
.booking-card .logo {
  flex: none; width: min(20cqw, 13cqh); height: min(20cqw, 13cqh); border-radius: 50%;
  box-shadow: 0 0 0 .4cqw rgba(212, 175, 55, .5), 0 2cqw 8cqw rgba(0, 0, 0, .6);
}
.booking-card h1 { margin: 0; font-size: min(9.5cqw, 6.2cqh); line-height: 1.02; font-weight: 900; letter-spacing: .01em; text-transform: uppercase; }
.booking-card .qr {
  position: relative; flex: none; margin: 0; overflow: hidden;
  padding: var(--ring); border-radius: var(--ring-radius); background: #0b0b0b;
  box-shadow: 0 0 2.2cqw rgba(140, 90, 255, .22), 0 0 5cqw rgba(0, 140, 255, .14);
  --ring: min(1.7cqw, 1.1cqh); --ring-radius: min(5.7cqw, 3.7cqh);
}
.booking-card .qr .plate { position: relative; padding: min(2.4cqw, 1.6cqh); border-radius: min(4cqw, 2.6cqh); background: #fff; }
.booking-card .qr img { display: block; width: min(62cqw, 40cqh); height: min(62cqw, 40cqh); border-radius: 0; image-rendering: pixelated; }
/* The shop's logo in the middle, the same as the code in the bottom bar. */
.booking-card .qr img.qr-logo {
  position: absolute; left: 50%; top: 50%; width: 24%; height: auto; aspect-ratio: 1; margin: 0;
  transform: translate(-50%, -50%);
  border-radius: 50%; background: #000; box-shadow: 0 0 0 min(1.6cqw, 1.05cqh) #fff; image-rendering: auto;
}
.booking-card .scan { margin: 0; font-size: min(5.4cqw, 3.5cqh); font-weight: 700; }
.booking-card .url { margin: 0; font-size: min(3.6cqw, 2.3cqh); color: rgba(255, 255, 255, .72); letter-spacing: .02em; }

/* ===== text offer: the envelope =====
   .shell.back (paper) sits under the letter and .shell.front (pocket, flap,
   seal) over it; both share one transform so the envelope moves as one piece
   and can drop away while the letter is read. */
.slide.message { background: radial-gradient(110% 60% at 50% 45%, #1d1709 0%, #070604 60%, #000 100%); }
.slide.message .watermark {
  position: absolute; left: 50%; top: 50%; width: 120cqw; height: 120cqw; border-radius: 50%;
  transform: translate(-50%, -50%); opacity: .05;
  background: url(/brand/logo.png) center / contain no-repeat;  /* screen.js swaps in the business's logo */
  background-size: contain; background-position: center; background-repeat: no-repeat;
}

#envstage { position: absolute; inset: 0; z-index: 20; pointer-events: none; display: none; }
#envstage.show { display: block; }
/* Whose deal it is, from across the room: LN's envelope is black with gold edges, Izzy's is her
   pink, Imperio's is cream like a real letter. screen.js sets the class, the accent and the
   logo for the seal from the business. */
#envstage.env-cream .paper { background: linear-gradient(160deg, #f7f0df, #e6dcc2); border-color: rgba(156, 122, 28, .55); }
#envstage.env-cream .pocket { background: linear-gradient(200deg, #efe6cf, #ddd2b5); box-shadow: inset 0 2cqw 5cqw rgba(90, 70, 30, .22); }
#envstage.env-cream .flap { background: linear-gradient(180deg, #fbf5e6, #e9dfc6); }
#envstage.env-pink .paper { background: linear-gradient(160deg, #3d1233, #1f0a1b); border-color: rgba(233, 79, 208, .55); }
#envstage.env-pink .pocket { background: linear-gradient(200deg, #2f0f28, #160711); }
#envstage.env-pink .flap { background: linear-gradient(180deg, #4b1840, #2b0e25); }
#flight {
  position: absolute; left: 50%; top: 50%; width: 78cqw; aspect-ratio: 1.5; opacity: 0;
  transform: translate(-50%, -50%) translate(40cqw, 60cqh) rotate(14deg) scale(.35);
  transition: transform .55s cubic-bezier(.2, .9, .25, 1.05), opacity .3s ease;
}
#flight.in { transform: translate(-50%, -50%); opacity: 1; }
#flight.out {
  transform: translate(-50%, -50%) translate(-10cqw, 40cqh) rotate(-9deg) scale(.68); opacity: 0;
  transition: transform .5s cubic-bezier(.6, 0, .9, .4), opacity .45s ease;
}

.shell { position: absolute; inset: 0; transition: transform .5s cubic-bezier(.4, 0, .2, 1), opacity .42s ease; }
.shell.back { z-index: 1; }
.shell.front { z-index: 4; perspective: 1200px; }
#flight.away .shell { transform: translateY(70cqh) scale(.9); opacity: 0; }

.paper {
  position: absolute; inset: 0; border-radius: 2cqw;
  background: linear-gradient(160deg, #1f1f1f, #141414);
  border: .2cqw solid rgba(212, 175, 55, .25);
  box-shadow: 0 6cqw 14cqw rgba(0, 0, 0, .6);
}
.pocket {
  position: absolute; inset: 0; border-radius: 2cqw;
  background: linear-gradient(200deg, #161616, #0c0c0c);
  clip-path: polygon(0 0, 50% 47%, 100% 0, 100% 100%, 0 100%);
  box-shadow: inset 0 2cqw 5cqw rgba(0, 0, 0, .35);
}
.flap {
  position: absolute; left: 0; right: 0; top: 0; height: 52%; z-index: 2;
  transform-origin: top center;
  background: linear-gradient(180deg, #232323, #161616);
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  transition: transform .5s cubic-bezier(.5, -.15, .3, 1.35);
}
#flight.open .flap { transform: rotateX(-172deg); z-index: 0; filter: brightness(.7); }

.seal {
  position: absolute; left: 50%; top: 47%; width: 30%; aspect-ratio: 1; z-index: 3;
  transform: translate(-50%, -50%); border-radius: 50%;
  background: #000 var(--env-logo, url(/brand/logo.png)) center / cover no-repeat;
  border: .5cqw solid var(--env-accent, #d9b64a);
  box-shadow: 0 1.6cqw 4cqw rgba(0, 0, 0, .6), 0 0 0 .2cqw rgba(0, 0, 0, .4);
  transition: transform .3s cubic-bezier(.3, 1.6, .5, 1), opacity .28s ease;
}
#flight.unsealed .seal { transform: translate(-50%, -185%) scale(1.3) rotate(-16deg); opacity: 0; }

#letter {
  position: absolute; left: 4%; right: 4%; top: 8%; bottom: 8%; z-index: 3;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2cqw;
  padding: 4% 6%; text-align: center; border-radius: 1.6cqw;
  background: #111; color: #f4f1e8;
  border: .25cqw solid color-mix(in srgb, var(--env-accent, #d4af37) 40%, transparent);
  box-shadow: 0 3cqw 7cqw rgba(0, 0, 0, .5);
  transition: transform .58s cubic-bezier(.2, .8, .2, 1);
}
#flight.reading #letter { transform: scale(1.22); }
#msg {
  max-height: 74%; overflow: hidden; white-space: pre-line; text-wrap: balance; overflow-wrap: break-word;
  font-family: Georgia, "Times New Roman", serif; font-weight: 700; line-height: 1.18;
  font-size: var(--msg-size, 6cqw);
}
#from { font-size: 2.4cqw; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; color: var(--env-accent, var(--gold)); }
#letter .rule { width: 24%; height: .25cqw; background: var(--env-accent, var(--gold)); opacity: .65; }

/* ===== idle / status ===== */
#idle {
  position: absolute; inset: 0; z-index: 30;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4cqh;
  background: #000; transition: opacity .6s ease;
}
#idle.gone { opacity: 0; pointer-events: none; }
#idle img { width: 44cqw; height: 44cqw; border-radius: 50%; }
#idle .name { font-size: 7cqw; font-weight: 900; letter-spacing: .14em; }
#idle .note { font-size: 3cqw; color: rgba(255, 255, 255, .55); }

#net {
  position: fixed; right: 10px; bottom: 10px; z-index: 50; width: 10px; height: 10px; border-radius: 50%;
  background: #ff5c5c; box-shadow: 0 0 10px #ff5c5c; opacity: 0; transition: opacity .4s ease;
}
#net.off { opacity: 1; }
#hint {
  position: fixed; top: 14px; right: 14px; z-index: 50; padding: 8px 14px; border-radius: 8px;
  font: 600 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: var(--gold); background: rgba(20, 20, 20, .85); border: 1px solid #3a3020;
  opacity: 0; transition: opacity .3s ease; pointer-events: none;
}
body.awake #hint { opacity: 1; }
