/* ============================================================================
   Design tokens.

   No webfont anywhere. The screen surface runs on a Smart TV whose connection
   may be poor and whose browser is old; blocking first paint on fonts.google.com
   to gain a typeface nobody reads from a sofa is a bad trade. The system stack
   is made to look chosen rather than defaulted through weight, tracking and
   optical sizing instead.
   ========================================================================= */

:root {
  /* Surfaces, as elevation levels rather than one flat "panel". A page where
     every box has the same background cannot show what matters most. */
  --bg: #0a0c10;
  --surface: #12161e;
  --surface-2: #1a1f2a;
  --line: #232a36;
  --line-soft: #1a1f29;

  --text: #f2f5f9;
  --muted: #8a94a8;
  --dim: #5d6779;

  /* Semantic, not decorative. Blue is the thing to do next, green is money,
     rose is destructive. An earlier palette used green for success, for the
     buy button and for the host badge at once, so the colour said nothing. */
  --brand: #4da3ff;
  --brand-ink: #04121f;
  --brand-soft: rgba(77, 163, 255, .14);
  --money: #3ddc84;
  --money-ink: #04210f;
  --money-soft: rgba(61, 220, 132, .12);
  --danger: #ff6b6b;
  --danger-soft: rgba(255, 107, 107, .12);

  /* Kept as aliases so nothing that referenced the old names breaks. */
  --panel: var(--surface);
  --accent: var(--brand);
  --ok: var(--money);
  --err: var(--danger);

  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-pill: 999px;

  --shadow-1: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow-2: 0 8px 24px -8px rgba(0, 0, 0, .7);
  --shadow-glow: 0 0 0 1px rgba(255, 255, 255, .06), 0 20px 50px -20px rgba(77, 163, 255, .35);

  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
          "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Roboto, Arial, sans-serif;
}

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

[hidden] { display: none !important; }

html, body {
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-feature-settings: "kern" 1, "liga" 1, "tnum" 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

button, input, a { font-family: inherit; }
button { -webkit-tap-highlight-color: transparent; }

/* ---------- wordmark ----------
   A product has a name. Its absence was the single loudest signal that this
   was a prototype: every surface was an untitled web page. The mark is drawn
   in CSS, so it costs no request and stays sharp on a 4K panel. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: .5em;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--text);
  text-decoration: none;
}
.brand-mark {
  position: relative;
  width: 1.1em;
  height: 1.1em;
  border-radius: .28em;
  background: var(--brand);
  flex: none;
}
/* A second plane behind the first: a screen, and something shared onto it. */
.brand-mark::after {
  content: "";
  position: absolute;
  right: -.26em;
  bottom: -.26em;
  width: .72em;
  height: .72em;
  border-radius: .2em;
  background: var(--money);
  box-shadow: -.12em -.12em 0 .12em var(--bg);
}
.brand-sub {
  color: var(--dim);
  font-weight: 400;
  letter-spacing: 0;
}

/* ---------- landing ---------- */
.landing {
  min-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 24px;
  gap: 4px;
}
.landing .brand { margin-bottom: 26px; font-size: 19px; }
.landing h1 {
  font-size: clamp(28px, 7vw, 36px);
  font-weight: 650;
  letter-spacing: -.025em;
  margin-bottom: 12px;
}
.landing p {
  color: var(--muted);
  margin-bottom: 32px;
  font-size: 16px;
  line-height: 1.55;
  max-width: 34ch;
}

/* The home page only. Its one job is a stranger deciding, in the time it
   takes to read two lines, whether this is worth a tap - claim/expired/resume
   share .landing's base sizing because they are brief status text, not a
   pitch, and don't need to compete for that same ten seconds. */
#home h1 {
  font-size: clamp(24px, 5.6vw, 40px);
  line-height: 1.2;
  max-width: 26ch;
}
#home p {
  font-size: 18px;
  max-width: 30ch;
}
.btn-hero {
  font-size: 19px;
  padding: 20px 48px;
  box-shadow: var(--shadow-glow);
}

.btn {
  display: inline-block;
  background: var(--brand);
  color: var(--brand-ink);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.01em;
  padding: 16px 38px;
  border: none;
  border-radius: var(--r-md);
  text-decoration: none;
  cursor: pointer;
  box-shadow: var(--shadow-1);
  transition: transform .12s ease, opacity .12s ease;
}
.btn:active { transform: scale(.985); }
.btn:disabled { opacity: .5; cursor: default; transform: none; }

/* ============================================================================
   SCREEN  -  the TV surface. Sized in vh so it scales with the panel rather
   than with a phone's idea of a pixel.
   ========================================================================= */

.screen { height: 100%; display: flex; flex-direction: column; }

/* Empty state. The single most-looked-at surface in the product: a room full
   of people face it for minutes. Its only job is getting the QR scanned, so
   nothing competes - but flat black with a code on it reads as a debug page,
   and a code that looks unfinished is a code people hesitate to scan. */
.hero {
  flex: 1;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 4vh 4vw;
  text-align: center;
}
/* Ambient depth so the background is not a void. Pure CSS, no image request. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(60vh 60vh at 50% 38%, rgba(77, 163, 255, .10), transparent 70%),
    radial-gradient(50vh 50vh at 50% 100%, rgba(61, 220, 132, .06), transparent 70%);
  pointer-events: none;
}
.hero > * { position: relative; }

.hero .brand {
  position: absolute;
  top: 4vh;
  left: 50%;
  transform: translateX(-50%);
  font-size: 2.4vh;
}

.hero-qr {
  background: #fff;
  padding: 2.2vh;
  border-radius: 2vh;
  box-shadow: var(--shadow-glow);
}
.hero-qr img { display: block; width: 46vh; height: 46vh; }

.hero-label {
  margin-top: 4vh;
  font-size: 4.4vh;
  font-weight: 650;
  letter-spacing: -.025em;
  line-height: 1.15;
}
.hero-url {
  margin-top: 1.6vh;
  font-size: 2.4vh;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
}
.hero-note {
  margin-top: 1.4vh;
  font-size: 2.1vh;
  color: var(--dim);
}

/* Reserved sponsor slot. Empty on purpose - see the product spec. */
.hero-banner {
  margin-top: 4vh;
  width: min(84vw, 96vh);
  height: 11vh;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface);
}
.hero-banner img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* End of the party: the screen stops taking photos and starts giving them back. */
.closed .hero-qr { background: #fff; }
.closed::before {
  background: radial-gradient(60vh 60vh at 50% 42%, rgba(61, 220, 132, .10), transparent 70%);
}

/* Populated state: the QR shrinks into a bar so photos get the screen. */
.screen-header {
  display: flex;
  align-items: center;
  gap: 2.4vh;
  padding: 2vh 3vw;
  border-bottom: 1px solid var(--line-soft);
  background: linear-gradient(180deg, rgba(255, 255, 255, .03), transparent);
}
.header-qr {
  background: #fff;
  padding: .9vh;
  border-radius: 1vh;
  flex: none;
  box-shadow: var(--shadow-1);
}
.header-qr img { display: block; width: 13vh; height: 13vh; }

/* Same code, same size as .header-qr - this is not the place to shrink it and
   find out later it was no longer scannable from across a room. */
.lb-qr {
  position: absolute;
  top: 3vh;
  left: 3vh;
  background: #fff;
  padding: .9vh;
  border-radius: 1vh;
  box-shadow: var(--shadow-1);
  z-index: 25;
}
.lb-qr img { display: block; width: 13vh; height: 13vh; }

.header-text { min-width: 0; }
.join-label {
  font-size: 1.8vh;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--dim);
}
.join-url {
  margin-top: .7vh;
  font-size: 3.2vh;
  font-weight: 600;
  letter-spacing: -.015em;
  color: var(--brand);
  font-variant-numeric: tabular-nums;
}

.header-meta {
  margin-left: auto;
  text-align: right;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: .5vh;
}
.counter {
  font-size: 2.6vh;
  font-weight: 600;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}
.new-screen {
  font-size: 1.7vh;
  color: var(--dim);
  text-decoration: none;
}
.new-screen-btn {
  background: none;
  border: none;
  font: inherit;
  color: var(--brand);
  cursor: pointer;
  padding: 0;
}

.wall {
  flex: 1;
  padding: 2vh 2vw;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  grid-auto-rows: 24vh;
  grid-gap: 1.4vh 1.2vw;
  align-content: start;
  overflow: hidden;
  /* No-ops without a background-image (Grid has none by default); set here,
     once, rather than on each per-season rule below - Sky already carries
     its own via the gradient-bearing rule further down, same values. */
  background-size: cover;
  background-position: center;
}

/* Tiles are focusable buttons: a TV has no pointer, only a D-pad. */
.tile {
  position: relative; /* anchors .like-badge; sky mode overrides this to absolute */
  border: none;
  padding: 0;
  border-radius: 1.2vh;
  overflow: hidden;
  background: var(--surface);
  cursor: pointer;
  box-shadow: var(--shadow-1);
  transition: transform .18s ease;
}

/* A heart only appears once a photo has one - a badge on every untouched
   photo would be the loudest thing on the wall for no reason. Same markup on
   a wall tile, a sky star and a vivid card; only the position nudges per
   shape so it never collides with the uploader badge in the opposite corner. */
.like-badge {
  position: absolute;
  right: .6vh;
  bottom: .6vh;
  z-index: 2;
  background: rgba(10, 12, 16, .62);
  color: #fff;
  font-size: 1.5vh;
  font-weight: 600;
  line-height: 1;
  padding: .5vh .8vh;
  border-radius: var(--r-pill);
  pointer-events: none;
  font-variant-numeric: tabular-nums;
}
.wall.sky .like-badge { right: 18%; bottom: 10%; }
.vivid-card .like-badge { right: 1vh; bottom: 1vh; }
.vivid-spotlight-main .like-badge { right: 1.6vh; bottom: 1.6vh; font-size: 2vh; padding: .7vh 1.1vh; }
.tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Sized to read from a sofa, not from a desk. */
.tile:focus {
  outline: .5vh solid var(--brand);
  outline-offset: .3vh;
  transform: scale(1.03);
}
.tile.enter { animation: pop .45s cubic-bezier(.2, .9, .3, 1.2); }

@keyframes pop {
  0% { opacity: 0; transform: scale(.86); }
  100% { opacity: 1; transform: scale(1); }
}

.status {
  position: fixed;
  right: 14px;
  bottom: 12px;
  font-size: 12px;
  letter-spacing: .04em;
  color: var(--dim);
}
.status.bad { color: var(--danger); }

/* Always on top, in every state (grid, lightbox, even the hero QR) - the
   choice is the host's to make whenever, not just once the slideshow starts. */
.display-toggles {
  position: fixed;
  top: 2vh;
  right: 2vh;
  z-index: 20;
  display: flex;
  gap: 1vh;
}
.mode-toggle {
  border: 1px solid rgba(255, 255, 255, .14);
  background: rgba(40, 44, 52, .88);
  color: #fff;
  font-size: 1.6vh;
  font-weight: 600;
  letter-spacing: .02em;
  padding: .9vh 1.6vh;
  border-radius: var(--r-pill);
  cursor: pointer;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.mode-toggle:focus { outline: .3vh solid var(--brand); outline-offset: .2vh; }

/* ---------- sky wall ----------
   The wall as a night sky: positions and sizes are set per star by layoutSky()
   in screen.js, since they depend on how many photos there are. */
.wall.sky {
  position: relative;
  z-index: 0; /* a stacking context: a lit star must never rise above the slideshow */
  display: block;
  padding: 0;
  background: radial-gradient(120% 90% at 50% 100%, #1a2a52 0%, #0b1530 45%, #04070e 100%);
  /* The `background` shorthand just above resets size/position to their
     initial values even though .wall (lower specificity, earlier in the
     cascade) already set them - a shorthand always resets every sub-property
     it doesn't mention. Repeating them here is what keeps Sky's own cover/
     center instead of silently falling back to auto/0 0. */
  background-size: cover;
  background-position: center;
}

/* The slideshow in Sky: the same night, and the photos round like the stars
   they were a moment ago rather than polaroids. */
.lightbox.vivid-mode.sky-theme {
  background: radial-gradient(120% 90% at 50% 100%, #1a2a52 0%, #0b1530 45%, #04070e 100%);
  background-size: cover;
  background-position: center;
}

/* Holiday/event photo backgrounds (public/img/*.jpg) replace the blue
   starry-sky gradient above - and, in Grid, the plain dark .wall background -
   so both display modes share the same layout: the header strip stays as it
   is (still plain, still carrying the season-fx CSS decoration - bats,
   snowflakes, etc. - which roams the whole viewport in both modes exactly as
   before), and the main wall area below it is the photo. Targeting plain
   .wall rather than .wall.sky is what reaches Grid too, since Sky's element
   always carries .wall as well ("wall sky", never "sky" alone) - the pre-wall
   hero/QR screen has no #wall yet, so it stays untouched either way.
   seasons.js tags <body> with "season-<id>" (see apply()) because .season-fx
   is a sibling of #wall/.lightbox, not an ancestor - a selector scoped to it
   could never reach them. AI-generated (not a stock photo), dark-centered
   with detail framed at the edges by design, so the wall's stars/QR/text sit
   on the same calm, low-detail middle the gradient already provided. */
body.season-halloween .wall,
body.season-halloween .lightbox.vivid-mode.sky-theme { background-image: url(/img/halloween.jpg); }
body.season-thanksgiving .wall,
body.season-thanksgiving .lightbox.vivid-mode.sky-theme { background-image: url(/img/thanksgiving.jpg); }
body.season-christmas .wall,
body.season-christmas .lightbox.vivid-mode.sky-theme { background-image: url(/img/christmas.jpg); }
body.season-newyear .wall,
body.season-newyear .lightbox.vivid-mode.sky-theme { background-image: url(/img/newyear.jpg); }
body.season-valentine .wall,
body.season-valentine .lightbox.vivid-mode.sky-theme { background-image: url(/img/valentine.jpg); }
body.season-birthday .wall,
body.season-birthday .lightbox.vivid-mode.sky-theme { background-image: url(/img/birthday.jpg); }
body.season-party .wall,
body.season-party .lightbox.vivid-mode.sky-theme { background-image: url(/img/party.jpg); }
.lightbox:not([hidden]) { animation: lb-in .4s ease; }
@keyframes lb-in { from { opacity: 0; } to { opacity: 1; } }
.sky-theme .vivid-card {
  padding: 0;
  border-radius: 50%;
  background: none;
  transform: translate(-50%, -50%) scale(var(--s, 1));
  box-shadow: 0 0 1.6vh rgba(190, 215, 255, .5), 0 0 5vh rgba(120, 160, 255, .28);
}
.sky-theme .vivid-card img { border-radius: 50%; }
/* Grid's is-portrait rule (below) makes the image box taller than wide -
   round that off to a 4:5 oval here instead of this circle. Sky's whole
   point is treating every photo the same regardless of orientation, so pin
   this back to square rather than letting the box's own ratio show through. */
.sky-theme .vivid-card.is-portrait img { aspect-ratio: 1 / 1; }
.sky-theme .vivid-card.is-current {
  box-shadow: 0 0 0 .3vh rgba(255, 255, 255, .9), 0 0 3vh rgba(160, 190, 255, .6);
}
.sky-theme .vivid-spotlight .vivid-card.vivid-spotlight-main {
  border-radius: 1vh;
  box-shadow: 0 0 0 .3vh rgba(255, 255, 255, .9), 0 0 5vh rgba(160, 190, 255, .55);
}
.sky-dust { display: none; position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.wall.sky .sky-dust,
.lightbox.vivid-mode.sky-theme .sky-dust { display: block; }
.sky-dust > i {
  position: absolute;
  left: 0;
  top: 0;
  width: 2px;
  height: 2px;
  border-radius: 50%;
}
.sky-dust > i:nth-of-type(1) { animation: twinkle 5s ease-in-out infinite alternate; }
.sky-dust > i:nth-of-type(2) { animation: twinkle 8s ease-in-out -3s infinite alternate; }
@keyframes twinkle { from { opacity: .3; } to { opacity: 1; } }

/* The milky way: a soft glow along a diagonal strip, with its own dense scatter
   of stars (laid out in JS in the strip's own coordinates). */
.sky-milkyway {
  position: absolute;
  left: -30%;
  top: 28%;
  width: 160%;
  height: 34vh;
  transform: rotate(-16deg);
}
.sky-milkyway::before {
  content: "";
  position: absolute;
  inset: -45% 0;
  background:
    radial-gradient(ellipse 55% 38% at 50% 50%, rgba(175, 195, 255, .5), transparent 72%),
    radial-gradient(ellipse 28% 34% at 22% 56%, rgba(205, 165, 255, .4), transparent 72%),
    radial-gradient(ellipse 26% 30% at 76% 44%, rgba(140, 205, 255, .4), transparent 72%),
    radial-gradient(ellipse 18% 24% at 50% 48%, rgba(255, 240, 225, .34), transparent 75%);
  filter: blur(16px);
}
.sky-milkyway > i {
  position: absolute;
  left: 0;
  top: 0;
  width: 2px;
  height: 2px;
  border-radius: 50%;
}

.wall.sky .tile {
  position: absolute;
  border-radius: 50%;
  z-index: 1;
  box-shadow: 0 0 1.4vh rgba(190, 215, 255, .5), 0 0 4.5vh rgba(120, 160, 255, .25);
  transition: left .8s ease, top .8s ease, width .8s ease, height .8s ease,
    transform .45s cubic-bezier(.2, .9, .3, 1.1), opacity .5s ease, box-shadow .3s ease,
    z-index 0s linear .45s; /* drop back only once it has finished shrinking */
}
.wall.sky .tile:focus { outline: none; transform: none; }
.wall.sky .tile.lit {
  z-index: 20;
  transition-delay: 0s;
  transform: translate(var(--tx, 0), var(--ty, 0)) scale(var(--k, 1));
  box-shadow: 0 0 0 .22vh rgba(255, 255, 255, .9), 0 0 2.4vh rgba(160, 190, 255, .6);
}
/* The magic circle round a newcomer: drawn in over ~1.3s, held, then faded, 3s in all. */
.sky-ring { position: absolute; z-index: 21; pointer-events: none; overflow: visible; animation: ring-life 3s linear forwards; }
.sky-ring circle { fill: none; stroke-linecap: round; stroke-dasharray: 302; stroke-dashoffset: 302; animation: ring-draw 1.3s cubic-bezier(.3, .6, .2, 1) .05s forwards; }
.sky-ring .glow { stroke: rgba(150, 190, 255, .45); stroke-width: 4.5; }
.sky-ring .line { stroke: #fff; stroke-width: 1.3; }
@keyframes ring-draw { to { stroke-dashoffset: 0; } }
@keyframes ring-life { 0%, 80% { opacity: 1; } 100% { opacity: 0; } }

.wall.sky .tile.born { transform: scale(0); opacity: 0; }
.wall.sky .tile.dot { background: #dfe9ff; box-shadow: 0 0 1vh rgba(200, 220, 255, .8); }
.wall.sky .tile.dot img { opacity: 0; transition: opacity .3s ease; }
.wall.sky .tile.dot.lit img { opacity: 1; }

/* ---------- slideshow ----------
   Up to seven photos in fixed slots (VIVID_LAYOUTS in screen.js). */
.lightbox {
  position: fixed;
  inset: 0;
  background: #000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 10;
}
/* ---------- vivid mode ----------
   Grid's slideshow: a warm, pinned-to-a-wall look - deliberately not the
   product's usual dark surface, because this is meant to read as a physical
   photo wall, not as another UI screen. Sky overrides it below. */
.lightbox.vivid-mode {
  background: radial-gradient(120% 120% at 50% 0%, #fdfaf4 0%, #f3ede1 55%, #e9ddc6 100%);
}

.vivid-collage, .vivid-spotlight {
  position: absolute;
  inset: 0;
}

/* Position, size and tilt come per photo from VIVID_LAYOUTS in screen.js,
   since they depend on how many photos are on stage. */
.vivid-card {
  position: absolute;
  background: #fdfdfb;
  padding: 1.1vh 1.1vh 3.4vh;
  border-radius: .4vh;
  box-shadow: 0 2vh 5vh -1.5vh rgba(40, 30, 10, .45);
  cursor: pointer;
  transform: translate(-50%, -50%) rotate(var(--r, 0deg)) scale(var(--s, 1));
  transition: transform .25s ease, box-shadow .25s ease;
}
.vivid-card img {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: .1vh;
}
/* A 9:16 phone shot squared off loses most of its frame. 4:5, not the real
   9:16: tall enough to read as a portrait rather than a crop, but short
   enough that the hand-placed VIVID_LAYOUTS slots (sized for a square) still
   clear their neighbours without a second round of position tuning. */
.vivid-card.is-portrait img {
  aspect-ratio: 4 / 5;
}
.vivid-card.is-current {
  --s: 1.05;
  z-index: 9 !important;
  box-shadow: 0 0 0 .45vh var(--brand), 0 2vh 6vh -1.5vh rgba(40, 30, 10, .5);
}

/* The rest of the group stays visible but out of focus, so the spotlight
   reads as "one of these, pulled forward" rather than a scene change. */
.vivid-spotlight .vivid-card {
  filter: blur(6px);
  opacity: .55;
  cursor: default;
}
.vivid-spotlight .vivid-card.vivid-spotlight-main {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: auto; /* sized by the photo, not by a slot */
  filter: none;
  opacity: 1;
  z-index: 10;
  box-shadow: 0 3vh 8vh -2vh rgba(40, 30, 10, .55);
  cursor: pointer;
}

.vivid-spotlight-main img {
  /* The whole photo at its own proportions: the slots crop to a square, but
     pulling one forward is for looking at it properly. 96vw/96vh, not a
     smaller box kept clear of the control bar - the bar is z-index 30 and
     floats on top of every vivid card regardless, so reserving room under it
     only shrank the one photo someone chose to look closely at. A 9:16 photo
     is height-bound here, so its card (this box plus .vivid-card's own
     1.1vh/3.4vh polaroid padding) lands right at, or a hair past, the
     screen's top and bottom edges - the point, not an overflow to fix. */
  aspect-ratio: auto;
  width: auto;
  height: auto;
  max-width: 96vw;
  max-height: 96vh;
  object-fit: contain;
}
/* Sky's card has no polaroid padding (.sky-theme .vivid-card above sets it
   to 0) - the image box IS the card here, so matching Grid's "right at the
   edge" result takes a taller cap: 100vh, not 96vh, since there's no padding
   left to close that last gap. */
.sky-theme .vivid-spotlight-main img { border-radius: .8vh; max-height: 100vh; }

.lightbox-bar {
  position: absolute;
  z-index: 30; /* above every vivid card, whatever its layout */
  bottom: 4vh;
  display: flex;
  align-items: center;
  gap: .4vh;
  background: rgba(12, 15, 20, .78);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, .08);
  border-radius: var(--r-pill);
  padding: 1vh 1.4vh;
}
.lb-btn {
  border: none;
  background: none;
  color: var(--text);
  font-size: 3.4vh;
  line-height: 1;
  width: 6.4vh;
  height: 6.4vh;
  border-radius: 50%;
  cursor: pointer;
  transition: background .12s ease;
}
.lb-btn:hover { background: rgba(255, 255, 255, .08); }
.lb-btn:focus { outline: .4vh solid var(--brand); outline-offset: .2vh; }
.lb-count {
  font-size: 2.2vh;
  color: var(--muted);
  padding: 0 1.6vh;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.lb-remove { font-size: 2.8vh; }
.lb-close { font-size: 4vh; }

.lightbox-hint {
  position: absolute;
  bottom: 1.2vh;
  color: var(--dim);
  font-size: 1.7vh;
  letter-spacing: .02em;
}

/* ---------- host handover ----------
   Deliberately not the empty state's treatment: this QR is on screen for two
   minutes on purpose and must never be mistaken for the join code. */
.host-panel {
  background: rgba(6, 8, 12, .97);
  gap: 2vh;
}
.host-panel .hero-qr { padding: 2vh; }
.host-panel .hero-qr img { width: 38vh; height: 38vh; display: block; }
.host-panel .hero-label { font-size: 3.2vh; }
.host-panel .lb-close { position: absolute; top: 3vh; right: 3vw; }

/* ============================================================================
   PHONE  -  share, download and claim
   ========================================================================= */

.share {
  min-height: 100%;
  padding: 22px 20px 64px;
  max-width: 560px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
}

/* App bar. Gives every phone surface the same head, which is most of what
   separates "a product" from "a page that happens to work". */
.appbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 20px;
  margin-bottom: 4px;
}
.appbar-meta {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--dim);
  white-space: nowrap;
}

.share h1 {
  font-size: 27px;
  font-weight: 650;
  letter-spacing: -.028em;
  margin-bottom: 7px;
}
.share .sub {
  color: var(--muted);
  font-size: 15px;
  line-height: 1.5;
  margin-bottom: 26px;
}

/* ---------- primary action ---------- */
.pickers { display: grid; grid-gap: 12px; }
.picker {
  display: block;
  position: relative;
  background: var(--brand);
  color: var(--brand-ink);
  font-size: 18px;
  font-weight: 650;
  letter-spacing: -.01em;
  text-align: center;
  padding: 20px;
  border-radius: var(--r-md);
  cursor: pointer;
  box-shadow: 0 6px 18px -8px rgba(77, 163, 255, .8);
  transition: transform .12s ease;
}
.picker:active { transform: scale(.985); }
.picker-icon { margin-right: 8px; }
.picker-alt { background: var(--surface-2); color: var(--text); box-shadow: none; }
.picker input { display: none; }

/* ---------- acceptable use ----------
   Framed as information rather than as a legal wall: it has to be read by
   someone standing up at a party, and a grey slab is a slab people tap past. */
.terms {
  margin-bottom: 20px;
  padding: 18px;
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--line);
}
.terms-title {
  font-size: 16px;
  font-weight: 650;
  letter-spacing: -.015em;
  margin-bottom: 10px;
}
.terms-body {
  color: var(--muted);
  font-size: 13.5px;
  line-height: 1.6;
  margin-bottom: 10px;
}
.terms-body strong { color: var(--text); font-weight: 600; }
.terms-btn {
  margin-top: 8px;
  width: 100%;
  background: var(--brand);
  color: var(--brand-ink);
  font-size: 15px;
  font-weight: 650;
  line-height: 1.35;
  padding: 15px 14px;
  border: none;
  border-radius: var(--r-md);
  cursor: pointer;
}
.terms-btn:disabled { opacity: .55; }

/* ---------- commerce ----------
   Green is money and only money. Blue is the next thing to do. */
.paywall {
  margin-top: 26px;
  padding: 22px;
  border-radius: var(--r-lg);
  background: var(--surface-2);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-2);
  text-align: center;
}
.paywall-title {
  font-size: 20px;
  font-weight: 650;
  letter-spacing: -.02em;
}
.paywall-sub { margin-top: 7px; color: var(--muted); font-size: 14.5px; line-height: 1.5; }
.paywall-btn {
  margin-top: 18px;
  width: 100%;
  background: var(--money);
  color: var(--money-ink);
  font-size: 17px;
  font-weight: 650;
  letter-spacing: -.01em;
  padding: 17px;
  border: none;
  border-radius: var(--r-md);
  cursor: pointer;
  box-shadow: 0 6px 18px -8px rgba(61, 220, 132, .8);
  transition: transform .12s ease;
}
.paywall-btn:active { transform: scale(.985); }
/* Sits between the button and paywall-note (which only ever shows for the
   simulated unlock) rather than replacing either - trust copy and "nothing
   was charged" copy are never true at the same time, but both read fine in
   the same slot. */
.paywall-trust { margin-top: 14px; color: var(--muted); font-size: 13px; line-height: 1.5; }
.paywall-note { margin-top: 14px; color: var(--dim); font-size: 13px; line-height: 1.5; }

/* Both offers at once rather than one repeated: a second prompt after a top-up
   runs out converts worse and reads as a funnel. Side by side it is a choice. */
.tiers { margin-top: 18px; display: grid; grid-gap: 10px; }
.tier {
  width: 100%;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 15px 16px;
  color: var(--text);
  font: inherit;
  cursor: pointer;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  transition: transform .12s ease;
}
.tier:active { transform: scale(.99); }
.tier-main { font-size: 16.5px; font-weight: 650; letter-spacing: -.012em; }
.tier-sub { display: block; margin-top: 3px; font-size: 12.5px; color: var(--muted); font-weight: 400; letter-spacing: 0; }
.tier-price { font-size: 17px; font-weight: 700; white-space: nowrap; font-variant-numeric: tabular-nums; }
.tier.tier-lead {
  background: var(--money);
  border-color: var(--money);
  color: var(--money-ink);
  box-shadow: 0 6px 18px -8px rgba(61, 220, 132, .7);
}
.tier.tier-lead .tier-sub { color: rgba(4, 33, 15, .72); }
.tier:disabled { opacity: .55; cursor: default; transform: none; }

/* A tester must never be left believing they were charged. */
.sim-note {
  margin: 14px 0 22px;
  padding: 13px 15px;
  border-radius: var(--r-md);
  background: var(--money-soft);
  border: 1px solid rgba(61, 220, 132, .28);
  color: var(--money);
  font-size: 13.5px;
  line-height: 1.5;
}

/* Host-only heads-up as the event approaches its photo limit - informational
   blue, not --danger's red: nothing has gone wrong, there's just something
   worth watching. */
.host-limit-note {
  margin: 14px 0 22px;
  padding: 13px 15px;
  border-radius: var(--r-md);
  background: var(--brand-soft);
  border: 1px solid rgba(77, 163, 255, .28);
  color: var(--brand);
  font-size: 13.5px;
  line-height: 1.5;
}

/* ---------- feedback ---------- */
.progress { margin-top: 22px; font-size: 16px; min-height: 24px; line-height: 1.45; }
.progress.ok { color: var(--money); font-weight: 600; }
.progress.bad { color: var(--danger); }

.bar {
  margin-top: 14px;
  height: 5px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  overflow: hidden;
  display: none;
}
.bar span {
  display: block;
  height: 100%;
  width: 0;
  border-radius: var(--r-pill);
  background: var(--brand);
  transition: width .25s ease;
}
/* The width only moves once per file, and a large photo can sit unchanged for
   several seconds while it's processed server-side - this is what shows the
   wait is alive rather than stuck. */
.bar.busy span { animation: bar-pulse 1.1s ease-in-out infinite; }
@keyframes bar-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .4; }
}

.thumbs { margin-top: 26px; display: grid; grid-template-columns: repeat(4, 1fr); grid-gap: 8px; }
.thumbs img { width: 100%; height: 76px; object-fit: cover; border-radius: var(--r-sm); opacity: .45; }
.thumbs img.done { opacity: 1; }

/* ---------- section heads ---------- */
.mine { margin-top: 30px; }
.mine-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 12px;
}
.mine-head > span:first-child {
  font-size: 16px;
  font-weight: 650;
  letter-spacing: -.015em;
}
.mine-hint { font-size: 12.5px; color: var(--dim); white-space: nowrap; }
.mine-note { margin-top: 12px; color: var(--dim); font-size: 12.5px; line-height: 1.55; }

.mine-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 8px; }
.mine-tile { position: relative; }
.mine-tile img {
  width: 100%;
  height: 96px;
  object-fit: cover;
  border-radius: var(--r-sm);
  display: block;
}
.mine-tile.removing { opacity: .35; }
/* A photo the host takes down is labelled, not vanished: silently disappearing
   reads as "my upload failed". */
.mine-tile.removed img { opacity: .28; filter: grayscale(1); }
.mine-del, .host-del {
  position: absolute;
  top: 5px;
  right: 5px;
  width: 26px;
  height: 26px;
  border: none;
  border-radius: 50%;
  background: rgba(8, 10, 14, .85);
  color: var(--text);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.mine-flag {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 4px 6px;
  background: rgba(8, 10, 14, .82);
  border-radius: 0 0 var(--r-sm) var(--r-sm);
  font-size: 10.5px;
  color: var(--muted);
  text-align: center;
}

/* ---------- host controls on the phone ----------
   They live here rather than on the TV because picking one tile out of a wall
   with a D-pad is painful, and a phone's storage is not wiped by a television. */
.host-tools { margin-top: 30px; }
.host-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 8px; }
.host-tile { position: relative; }
.host-tile img {
  width: 100%;
  height: 96px;
  object-fit: cover;
  border-radius: var(--r-sm);
  display: block;
}
.host-tile.removing { opacity: .35; }

.host-end {
  margin-top: 18px;
  width: 100%;
  background: none;
  border: 1px solid var(--line);
  color: var(--muted);
  font: inherit;
  font-size: 14.5px;
  padding: 13px;
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background .12s ease;
}
.host-end.armed {
  background: var(--danger);
  border-color: var(--danger);
  color: #2a0808;
  font-weight: 650;
}
.host-end:disabled { opacity: .5; }

/* Reachable throughout, quiet during the party: someone leaving early still
   wants the photos. */
.share-foot {
  display: block;
  margin-top: 34px;
  padding-top: 18px;
  border-top: 1px solid var(--line-soft);
  color: var(--muted);
  font-size: 14.5px;
  text-decoration: none;
  text-align: center;
}

/* ---------- taking photos home ----------
   Every photo of the night is shown. Only the full-resolution file is gated,
   so the grid is a browsable set rather than a teaser: three can be taken for
   nothing, and that is how someone finds out the rest is worth buying. */
.dl-all {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin: 2px 0 16px;
  padding: 15px 16px;
  border-radius: var(--r-md);
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--text);
  font-size: 15.5px;
  font-weight: 650;
  letter-spacing: -.012em;
  text-decoration: none;
}
.dl-all-size { color: var(--dim); font-size: 12.5px; font-weight: 400; font-variant-numeric: tabular-nums; }

.dl-grid { margin-top: 4px; display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 8px; }
.dl-tile {
  display: block;
  position: relative;
  border: none;
  padding: 0;
  border-radius: var(--r-sm);
  overflow: hidden;
  line-height: 0;
  background: none;
  cursor: pointer;
}
.dl-tile img {
  width: 100%;
  height: 108px;
  object-fit: cover;
  display: block;
  transition: opacity .18s ease, filter .18s ease;
}
.dl-tile.is-pickable img { opacity: .82; }
.dl-tile.is-pickable:active img { opacity: 1; }
/* Picks spent. Dimmed rather than hidden - the photo has to stay visible for
   the offer beside it to mean anything. */
.dl-tile.is-locked img { opacity: .34; filter: saturate(.5); }
.dl-tile.is-picked, .dl-tile.is-mine { outline: 2px solid var(--money); outline-offset: -2px; }

.dl-badge {
  position: absolute;
  left: 5px;
  bottom: 5px;
  padding: 2px 7px;
  border-radius: var(--r-pill);
  background: var(--money);
  color: var(--money-ink);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.5;
  letter-spacing: .02em;
}

/* ---------- what is on the screen, and reporting it ----------
   Every guest sees this, not only the host: a report button a guest cannot
   reach is not a safety control. */
.onscreen { margin-top: 30px; }
.onscreen-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 8px; }
.onscreen-tile {
  position: relative;
  display: block;
  border: none;
  padding: 0;
  background: none;
  border-radius: var(--r-sm);
  overflow: hidden;
  line-height: 0;
  cursor: pointer;
}
.onscreen-tile img { width: 100%; height: 96px; object-fit: cover; display: block; }
.onscreen-tile::after {
  content: "\2691";
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(8, 10, 14, .8);
  color: var(--muted);
  font-size: 12px;
  line-height: 22px;
  text-align: center;
}
/* Already taken down. Shown to the host so there is something to review. */
.onscreen-tile.is-hidden img { opacity: .25; filter: grayscale(1); }
.onscreen-tile.is-hidden::after { content: "\2691"; background: var(--danger); color: #2a0808; }

.sheet {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: rgba(4, 6, 10, .74);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 16px;
}
.sheet-card {
  width: 100%;
  max-width: 520px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 20px;
  box-shadow: var(--shadow-2);
}
.sheet-title { font-size: 18px; font-weight: 650; letter-spacing: -.018em; }
.sheet-sub { margin-top: 6px; color: var(--muted); font-size: 13.5px; }
.sheet-options { margin-top: 16px; display: grid; grid-gap: 7px; }
.sheet-opt {
  width: 100%;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 14px 15px;
  color: var(--text);
  font: inherit;
  font-size: 15px;
  cursor: pointer;
}
/* The reasons that cannot be undone read differently from the ones that can. */
.sheet-opt.is-severe { border-color: rgba(255, 107, 107, .4); color: #ffc2c2; }
.sheet-cancel {
  margin-top: 14px;
  width: 100%;
  background: none;
  border: none;
  color: var(--muted);
  font: inherit;
  font-size: 15px;
  padding: 12px;
  cursor: pointer;
}
/* Same corner .host-restore uses - the two never show at once, since restore
   only appears once a photo is hidden and liking only while it is not. */
.like-btn {
  position: absolute;
  left: 4px;
  bottom: 4px;
  display: flex;
  align-items: center;
  gap: 3px;
  border: none;
  border-radius: var(--r-pill);
  background: rgba(8, 10, 14, .72);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  padding: 5px 8px;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.like-btn.is-liked { color: var(--danger); background: rgba(8, 10, 14, .85); }
.like-btn span { color: #fff; }

.host-restore {
  position: absolute;
  left: 4px;
  bottom: 4px;
  border: none;
  border-radius: var(--r-pill);
  background: var(--brand);
  color: var(--brand-ink);
  font-size: 10px;
  font-weight: 700;
  padding: 3px 8px;
  cursor: pointer;
}

/* Picking a few rather than taking the night. Off by default: most people who
   have paid want everything, and a grid that opens in selection mode makes the
   common case two taps instead of one. */
/* A full-width secondary action under the primary one. It was a small muted
   pill in the grid header, which is a place nobody looks. */
.dl-select {
  display: block;
  width: 100%;
  margin: -6px 0 18px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  color: var(--text);
  font: inherit;
  font-size: 14.5px;
  font-weight: 600;
  padding: 13px;
  cursor: pointer;
}
.dl-select.is-on { background: none; border-color: transparent; color: var(--muted); font-weight: 400; }

.dl-tile.is-choosable img { opacity: .55; }
.dl-tile.is-chosen img { opacity: 1; }
.dl-tile.is-chosen { outline: 3px solid var(--brand); outline-offset: -3px; }
.dl-check {
  position: absolute;
  top: 5px;
  right: 5px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(8, 10, 14, .55);
  border: 2px solid rgba(255, 255, 255, .55);
  color: var(--brand-ink);
  font-size: 13px;
  font-weight: 700;
  line-height: 19px;
  text-align: center;
}
.dl-tile.is-chosen .dl-check { background: var(--brand); border-color: var(--brand); }

/* Other live events this phone took part in, on the download page. */
.other-events { margin: 0 0 1.4rem; font-size: .85rem; color: var(--muted); display: flex; flex-wrap: wrap; gap: .4rem .9rem; }
.other-events a { color: var(--brand); text-decoration: none; }

/* ============================================================================
   SEASONAL ATMOSPHERE  -  public/seasons.js decides whether one is active and
   appends this; purely decorative (aria-hidden, pointer-events:none), so
   nothing here can block a tap, a scan or a D-pad move. Shapes are flat
   silhouettes drawn in CSS/SVG, not photos - mood over polish, for now.

   Confined to roughly the header strip's own height (~19vh: 2vh+2vh padding
   plus the 13vh QR plus its own padding) rather than the whole viewport. It
   used to span the full screen in both display modes, which worked when the
   wall behind it was a plain gradient; now that the wall itself carries a
   photo background (see the holiday background-image rules), decoration
   drifting across that photo too was "the whole screen moving", competing
   with both the photo and the actual guest photos. All the @keyframes below
   are scaled to this ~20vh band, not the old full-viewport range - rescaling
   the container alone, with the old vh values, would have just clipped each
   piece to a brief flash crossing the bottom edge, not a graceful hover.
   ========================================================================= */
.season-fx {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 20vh;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.season-piece-wrap {
  position: absolute;
  top: -3vh;
  width: 3vh;
  height: 3vh;
  will-change: transform;
}
.season-piece { width: 100%; height: 100%; display: block; opacity: .8; }

/* Jack-o'-lanterns float gently upward rather than falling. */
.season-halloween .drift-rise { animation: drift-rise linear infinite; }
.season-halloween .pumpkin-piece { top: 21vh; width: 6vh; height: 5.4vh; }

/* Thanksgiving: leaves tumble - more rotation, more horizontal wander than snow. */
@keyframes drift-fall {
  0%   { transform: translate(0, -3vh) rotate(0deg); }
  50%  { transform: translate(-4vw, 10vh) rotate(160deg); }
  100% { transform: translate(3vw, 23vh) rotate(340deg); }
}
.season-thanksgiving .drift-fall { animation: drift-fall linear infinite; }
.season-thanksgiving .season-piece-wrap { width: 2.6vh; height: 2.6vh; }

/* Christmas: small, slow, nearly straight - the classic effect, kept plain. */
@keyframes drift-snow {
  0%   { transform: translate(0, -3vh); }
  50%  { transform: translate(1.2vw, 10vh); }
  100% { transform: translate(-0.6vw, 23vh); }
}
.season-christmas .drift-snow { animation: drift-snow linear infinite; }
.season-christmas .season-piece-wrap { width: 1.6vh; height: 1.6vh; }
/* Tree/gingerbread/candy cane are feature ornaments mixed in with the plain
   snowflakes (see RECIPES.christmas.variants) - sized up from the 1.6vh
   default above, the same way the pumpkin is sized up from the bat, but
   small enough now to actually fit inside the confined band. */
.season-christmas .tree-piece { width: 5vh; height: 6.7vh; }
.season-christmas .gingerbread-piece { width: 5vh; height: 6vh; }
.season-christmas .candycane-piece { width: 3.3vh; height: 6vh; }

/* Birthday: balloons rise instead of falling, so the base top (below the
   band, for everything that drifts up) is overridden to start below it. */
@keyframes drift-rise {
  0%   { transform: translate(0, 0) rotate(0deg); }
  50%  { transform: translate(-2.5vw, -11vh) rotate(-3deg); }
  100% { transform: translate(2vw, -23vh) rotate(3deg); }
}
.season-birthday .season-piece-wrap { top: 21vh; width: 7vh; height: 9.3vh; }
.season-birthday .drift-rise { animation: drift-rise linear infinite; }
/* More translucent than the other pieces - a balloon this size reads as a
   sticker at .8 opacity; lighter is what makes it feel like it's lit from
   behind rather than a flat shape pasted on the screen. */
.season-birthday .season-piece { opacity: .55; }

/* Party: confetti tumbles exactly like a Thanksgiving leaf - smaller and
   brighter is what turns the same fall into paper instead of foliage. */
.season-party .drift-fall { animation: drift-fall linear infinite; }
.season-party .season-piece-wrap { width: 1.8vh; height: 1.8vh; }

/* New Year: sparkles tumble like confetti, smaller than the party ones - a
   glint, not a chip of paper. */
.season-newyear .drift-fall { animation: drift-fall linear infinite; }
.season-newyear .season-piece-wrap { width: 2vh; height: 2vh; }

/* Valentine: hearts rise like balloons, smaller - a heart floating off is the
   gesture, not a balloon-sized spectacle. */
.season-valentine .season-piece-wrap { top: 21vh; width: 4vh; height: 3.6vh; }
.season-valentine .drift-rise { animation: drift-rise linear infinite; }
