/* The TV: a bar of players and buttons, one row of game tiles and the QR; a running game fills the screen below the
   bar and the QR shrinks into a corner for late arrivals. Notices sit at the bottom, overlays over everything. */
:root {
  /* One unit of a 1080p design: 1 u is 1 px on a 1920x1080 screen and scales with the smaller side of any other.
     What is read from the sofa is sized in u, so it keeps its place on a 720p laptop and on a 4K television. */
  --u: min(calc(100vw / 1920), calc(100vh / 1080));
  /* The margin a television may crop: 5% of each side, which is 96u by 54u on 16:9. Text and the QR stay inside. */
  --safe-x: 5vw;
  --safe-y: 5vh;
}

body {
  margin: 0;
  min-height: 100vh;
}

/* Catalogue posters decode behind this; room and QR load in parallel under it. */
.boot-splash {
  --boot-accent-rgb: 124 108 255;
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  opacity: 1;
  transition: opacity 320ms ease-out;
  pointer-events: auto;
}

/* Full-screen ellipses at the viewport centre — same smoothstep as .ambient-layer, no local “disc”. */
.boot-splash::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(
      ellipse 40% 38% at 50% 50%,
      rgb(var(--boot-accent-rgb) / 36%) 0%,
      rgb(var(--boot-accent-rgb) / 35%) 10%,
      rgb(var(--boot-accent-rgb) / 32.3%) 20%,
      rgb(var(--boot-accent-rgb) / 28.2%) 30%,
      rgb(var(--boot-accent-rgb) / 23.3%) 40%,
      rgb(var(--boot-accent-rgb) / 18%) 50%,
      rgb(var(--boot-accent-rgb) / 12.6%) 60%,
      rgb(var(--boot-accent-rgb) / 7.7%) 70%,
      rgb(var(--boot-accent-rgb) / 3.7%) 80%,
      rgb(var(--boot-accent-rgb) / 1%) 90%,
      rgb(var(--boot-accent-rgb) / 0%) 100%
    ),
    radial-gradient(
      ellipse 90% 75% at 50% 50%,
      rgb(var(--boot-accent-rgb) / 18%) 0%,
      rgb(var(--boot-accent-rgb) / 17.5%) 10%,
      rgb(var(--boot-accent-rgb) / 16.1%) 20%,
      rgb(var(--boot-accent-rgb) / 14.1%) 30%,
      rgb(var(--boot-accent-rgb) / 11.7%) 40%,
      rgb(var(--boot-accent-rgb) / 9%) 50%,
      rgb(var(--boot-accent-rgb) / 6.3%) 60%,
      rgb(var(--boot-accent-rgb) / 3.9%) 70%,
      rgb(var(--boot-accent-rgb) / 1.9%) 80%,
      rgb(var(--boot-accent-rgb) / 0.5%) 90%,
      rgb(var(--boot-accent-rgb) / 0%) 100%
    );
  pointer-events: none;
}

.boot-splash::after {
  content: '';
  position: absolute;
  inset: 0;
  background: url('/noise.png') repeat;
  background-size: 64px 64px;
  opacity: 0.04;
  mix-blend-mode: soft-light;
  pointer-events: none;
}

.boot-splash[hidden] {
  display: none !important;
}

.boot-splash.is-leaving {
  pointer-events: none;
}

.boot-splash-logo {
  position: relative;
  z-index: 1;
  width: min(28vw, 200px);
  height: auto;
  filter: drop-shadow(0 0 20px rgb(124 108 255 / 40%))
    drop-shadow(0 0 40px rgb(124 108 255 / 18%));
  animation: boot-logo-breathe 2.4s ease-in-out infinite;
}

@keyframes boot-logo-breathe {
  0%,
  100% {
    transform: scale(0.96);
    filter: drop-shadow(0 0 16px rgb(124 108 255 / 34%))
      drop-shadow(0 0 32px rgb(124 108 255 / 14%));
  }
  50% {
    transform: scale(1.04);
    filter: drop-shadow(0 0 24px rgb(124 108 255 / 48%))
      drop-shadow(0 0 48px rgb(124 108 255 / 22%));
  }
}

body.booting #tvShell {
  visibility: hidden;
}

/* Fullscreen goes here so modal dialogs inside stay in the same tree and paint above the game. */
#tvShell {
  display: grid;
  grid-template-rows: auto 1fr;
  min-height: 100vh;
  --bar-height: 72px;
}

.bar {
  display: flex;
  align-items: center;
  gap: 16px;
  height: var(--bar-height);
  padding: 0 max(16px, var(--safe-x));
}

.bar h1 {
  margin: 0 8px 0 0;
  line-height: 0;
}

.brand {
  display: block;
  width: 72px;
  height: 32px;
}

.seats {
  display: flex;
  flex: 1;
  gap: 12px;
  min-width: 0;
  margin: 0;
  padding: 0;
  overflow: hidden;
  list-style: none;
}

.seats li {
  display: grid;
  padding: 6px 14px;
  border-left: 6px solid var(--seat);
  border-radius: 12px;
  background: var(--panel);
  white-space: nowrap;
}

.seats li.held {
  border-left-style: dashed;
  opacity: 0.6;
}

.seats span {
  color: var(--muted);
  font-size: 13px;
}

.ghost {
  padding: 8px 14px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: transparent;
  white-space: nowrap;
}

.bar .ghost {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.mark {
  width: 18px;
  height: 18px;
  flex: none;
}

#sound .mark-on {
  display: none;
}

#sound[aria-pressed='true'] .mark-off {
  display: none;
}

#sound[aria-pressed='true'] .mark-on {
  display: block;
}

/* Both labels sit in one cell and the button is as wide as the longer, so switching the sound on does not nudge
   the buttons beside it. */
.btn-swap {
  display: inline-grid;
}

.btn-swap > span {
  grid-area: 1 / 1;
}

#sound[aria-pressed='false'] .swap-on,
#sound[aria-pressed='true'] .swap-off {
  visibility: hidden;
}

.net-dot {
  width: 10px;
  height: 10px;
  flex: none;
  border-radius: 50%;
  background: #3ecf8e;
}

#network[data-net='warn'] .net-dot {
  background: #f5a524;
}

#network[data-net='bad'] .net-dot {
  background: #e5484d;
}

/* The microphone button says, in words anyone can see, that nothing is recorded. */
.mic small {
  display: block;
  color: var(--muted);
  font-size: 11px;
}

.primary {
  padding: 10px 18px;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  font-weight: 700;
}

/* The glow behind the shelf: two full-screen layers, one fading out while the other fades in. The colour comes
   from the poster in focus. Only opacity changes, so it never costs a repaint while it fades.
   A plain two-stop gradient ends in a visible rim, where its slope stops at once. These fall off along a
   smoothstep curve: flat in the middle, flat at the end, so no edge shows where the glow gives out. */
.ambient {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}

/* Breaks gradient banding on 8-bit panels; static tile, no animation. */
.ambient::after {
  content: '';
  position: absolute;
  inset: 0;
  background: url('/noise.png') repeat;
  background-size: 64px 64px;
  opacity: 0.04;
  mix-blend-mode: soft-light;
  pointer-events: none;
  /* Keep the shelf edge fade test stable: no grain in the outer 8% of the screen. */
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0%,
    #000 8%,
    #000 92%,
    transparent 100%
  );
  mask-image: linear-gradient(to right, transparent 0%, #000 8%, #000 92%, transparent 100%);
}

.ambient-layer {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(
      ellipse 40% 38% at 50% var(--ambient-y, 34%),
      rgb(var(--accent-rgb, 58 66 86) / 58%) 0%,
      rgb(var(--accent-rgb, 58 66 86) / 56.4%) 10%,
      rgb(var(--accent-rgb, 58 66 86) / 52%) 20%,
      rgb(var(--accent-rgb, 58 66 86) / 45.5%) 30%,
      rgb(var(--accent-rgb, 58 66 86) / 37.6%) 40%,
      rgb(var(--accent-rgb, 58 66 86) / 29%) 50%,
      rgb(var(--accent-rgb, 58 66 86) / 20.4%) 60%,
      rgb(var(--accent-rgb, 58 66 86) / 12.5%) 70%,
      rgb(var(--accent-rgb, 58 66 86) / 6%) 80%,
      rgb(var(--accent-rgb, 58 66 86) / 1.6%) 90%,
      rgb(var(--accent-rgb, 58 66 86) / 0%) 100%
    ),
    radial-gradient(
      ellipse 90% 75% at 50% var(--ambient-y, 34%),
      rgb(var(--accent-rgb, 58 66 86) / 24%) 0%,
      rgb(var(--accent-rgb, 58 66 86) / 23.3%) 10%,
      rgb(var(--accent-rgb, 58 66 86) / 21.5%) 20%,
      rgb(var(--accent-rgb, 58 66 86) / 18.8%) 30%,
      rgb(var(--accent-rgb, 58 66 86) / 15.6%) 40%,
      rgb(var(--accent-rgb, 58 66 86) / 12%) 50%,
      rgb(var(--accent-rgb, 58 66 86) / 8.4%) 60%,
      rgb(var(--accent-rgb, 58 66 86) / 5.2%) 70%,
      rgb(var(--accent-rgb, 58 66 86) / 2.5%) 80%,
      rgb(var(--accent-rgb, 58 66 86) / 0.7%) 90%,
      rgb(var(--accent-rgb, 58 66 86) / 0%) 100%
    );
  opacity: 0;
  transition: opacity 450ms ease;
}

.ambient-layer.on {
  opacity: 1;
}

/* The menu is one column across the whole screen. Text stays inside the 5% margin; the row of posters runs
   to the edges of the screen, where its far ends fade out. */
.home {
  position: relative;
  display: grid;
  align-content: safe center;
  min-height: 0;
  padding: 0 0 var(--safe-y);
}

.menu {
  display: grid;
  justify-items: center;
  min-width: 0;
}

.stage {
  position: relative;
  width: 100%;
  padding-block: calc(16 * var(--u));
  overflow: hidden;
  /* The far ends of the row fade into whatever is behind them, the glow included, rather than into a flat
     colour laid over it. The curve is smoothstep, so the fade has no visible start or end. */
  --fade: linear-gradient(
    to right,
    transparent 0%,
    rgb(0 0 0 / 16%) 2.5%,
    rgb(0 0 0 / 50%) 5%,
    rgb(0 0 0 / 84%) 7.5%,
    #000 10%,
    #000 90%,
    rgb(0 0 0 / 84%) 92.5%,
    rgb(0 0 0 / 50%) 95%,
    rgb(0 0 0 / 16%) 97.5%,
    transparent 100%
  );
  -webkit-mask-image: var(--fade);
  mask-image: var(--fade);
}

/* One shared clip, parked off-screen until it replaces the hero poster after a pause. */
.tile-preview {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  border: 0;
  border-radius: inherit;
  object-fit: cover;
  opacity: 0;
  transition: opacity 420ms ease;
  pointer-events: none;
}

.tiles button.show-preview .tile-preview.on {
  opacity: 1;
}

.tiles button.show-preview img.is-loaded {
  opacity: 0;
  transition: opacity 420ms ease;
}

/* Seven places in a row, centred on the screen: the focus, two neighbours each side and one parked just beyond
   each end, so the next poster is already loaded when it slides in. A poster is placed by `data-offset` alone
   and slides with `transform`, so a new step turns it round from wherever it is. */
.tiles {
  --slide: 300ms;
  position: relative;
  z-index: 1;
  height: calc(324 * var(--u));
  margin: 0;
  padding: 0;
  list-style: none;
}

.tiles li {
  display: contents;
}

.tiles button {
  --x: 0;
  --s: 1;
  --shade: 0;
  --show: 1;
  --z: 0;
  position: absolute;
  top: 0;
  left: 50%;
  width: calc(576 * var(--u));
  aspect-ratio: 16 / 9;
  padding: 0;
  border: 0;
  border-radius: calc(20 * var(--u));
  background: var(--panel);
  transform: translate3d(calc(-50% + var(--x) * var(--u)), 0, 0) scale(var(--s));
  opacity: var(--show);
  z-index: var(--z);
  transition:
    transform var(--slide) cubic-bezier(0.2, 0, 0, 1),
    opacity 200ms linear;
  will-change: transform;
}

/* The dark layer that dims a poster. The poster stays opaque, so nothing shows through it. */
.tiles button::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  background: #000;
  opacity: var(--shade);
  transition: opacity 200ms linear;
  pointer-events: none;
}

/* The ring of the focus: outside the picture, a gap away from it, and only ever faded in or out. */
.tiles button::after {
  content: '';
  position: absolute;
  inset: calc(-8 * var(--u));
  z-index: 2;
  border: calc(4 * var(--u)) solid rgb(255 255 255 / 92%);
  border-radius: calc(28 * var(--u));
  opacity: 0;
  transition: opacity 80ms linear;
  pointer-events: none;
}

.tiles button[data-offset='0']::after {
  opacity: 1;
  transition: opacity 140ms ease-out 180ms;
}

.tiles button[data-offset='0'] {
  --x: 0;
  --s: 1;
  --shade: 0;
  --z: 3;
}

.tiles button[data-offset='-1'],
.tiles button[data-offset='1'] {
  --s: 0.84;
  --shade: 0.32;
  --z: 2;
}

.tiles button[data-offset='-1'] {
  --x: -570;
}

.tiles button[data-offset='1'] {
  --x: 570;
}

.tiles button[data-offset='-2'],
.tiles button[data-offset='2'] {
  --s: 0.7;
  --shade: 0.58;
  --z: 1;
}

.tiles button[data-offset='-2'] {
  --x: -1054;
}

.tiles button[data-offset='2'] {
  --x: 1054;
}

/* Parked beyond the screen, or on the way in or out: not seen. */
.tiles button[data-offset='-3'],
.tiles button[data-offset='-4'],
.tiles button[data-offset='-5'],
.tiles button[data-offset='-6'],
.tiles button[data-offset='3'],
.tiles button[data-offset='4'],
.tiles button[data-offset='5'],
.tiles button[data-offset='6'] {
  --s: 0.6;
  --shade: 0.7;
  --show: 0;
}

.tiles button[data-offset='-3'] {
  --x: -1470;
}

.tiles button[data-offset='-4'] {
  --x: -1800;
}

.tiles button[data-offset='-5'] {
  --x: -2100;
}

.tiles button[data-offset='-6'] {
  --x: -2400;
}

.tiles button[data-offset='3'] {
  --x: 1470;
}

.tiles button[data-offset='4'] {
  --x: 1800;
}

.tiles button[data-offset='5'] {
  --x: 2100;
}

.tiles button[data-offset='6'] {
  --x: 2400;
}

/* A first paint, or a jump over several places: every poster is placed at once. */
.tiles.instant button,
.tiles.instant button::before,
.tiles.instant button::after {
  transition: none;
}

.tiles img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  object-fit: cover;
  opacity: 0;
  transition: opacity 250ms ease;
}

.tiles img.is-loaded {
  opacity: 1;
}

/* The words under the poster. The title is large, then one line of facts, two lines of description and, after a
   pause on one game, how to play it. Their places are reserved, so nothing moves when the words change. */
.stage-caption {
  display: grid;
  justify-items: center;
  width: min(100%, calc(1260 * var(--u)));
  min-width: 0;
  margin: calc(24 * var(--u)) 0 0;
  text-align: center;
  text-shadow: 0 calc(2 * var(--u)) calc(12 * var(--u)) rgb(0 0 0 / 55%);
  transition:
    opacity 200ms ease,
    transform 200ms ease;
}

.stage-caption.is-swapping {
  opacity: 0;
  transition-duration: 120ms;
}

.stage-caption.is-entering {
  opacity: 0;
  transform: translateY(calc(8 * var(--u)));
  transition: none;
}

.stage-title {
  min-height: 1.1em;
  font-size: calc(60 * var(--u));
  font-weight: 700;
  line-height: 1.1;
  overflow-wrap: anywhere;
  text-wrap: balance;
}

.stage-meta {
  min-height: 1.3em;
  margin-top: calc(10 * var(--u));
  color: #b7bfd0;
  font-size: calc(26 * var(--u));
  line-height: 1.3;
}

.stage-description {
  display: -webkit-box;
  min-height: calc(81 * var(--u));
  max-width: calc(1190 * var(--u));
  margin-top: calc(22 * var(--u));
  overflow: hidden;
  color: #d5dae5;
  font-size: calc(30 * var(--u));
  line-height: 1.35;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
}

.control-line {
  display: flex;
  flex-wrap: wrap;
  align-content: start;
  justify-content: center;
  gap: calc(6 * var(--u)) calc(28 * var(--u));
  width: 100%;
  min-height: calc(79 * var(--u));
  min-width: 0;
  margin-top: calc(16 * var(--u));
  color: #aab2c2;
  font-size: calc(26 * var(--u));
  line-height: 1.4;
  opacity: 0;
  transition: opacity 300ms ease;
}

.stage-caption.is-revealed .control-line {
  opacity: 1;
}

.control-line span {
  white-space: nowrap;
}

.hint {
  max-width: calc(1190 * var(--u));
  margin: calc(20 * var(--u)) 0 0;
  color: #b7bfd0;
  font-size: calc(26 * var(--u));
  line-height: 1.3;
  text-align: center;
}

/* The QR sits in the corner inside the 5% margin, so the poster in focus can stand in the middle of the screen. */
.join {
  position: fixed;
  right: var(--safe-x);
  bottom: var(--safe-y);
  z-index: 4;
  display: grid;
  justify-items: center;
  gap: calc(8 * var(--u));
  width: calc(224 * var(--u));
  padding: calc(12 * var(--u));
  border-radius: calc(20 * var(--u));
  background: var(--panel);
  font-size: calc(20 * var(--u));
  line-height: 1.35;
  text-align: center;
}

.join img {
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: contain;
  border-radius: calc(12 * var(--u));
  background: #fff;
}

.join-link {
  min-height: 1.35em;
  color: var(--accent);
  word-break: break-all;
}

.join-hint {
  margin: 0;
  padding: calc(4 * var(--u)) calc(8 * var(--u));
  border: 0;
  border-radius: calc(8 * var(--u));
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: calc(18 * var(--u));
  line-height: 1.3;
  text-align: center;
  cursor: pointer;
}

.join-hint:hover,
.join-hint:focus-visible {
  color: var(--text);
  background: rgb(255 255 255 / 6%);
  outline: none;
}

.join-hint.is-warn {
  color: #f5a524;
}

.game {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

body.playing .game {
  display: block;
  z-index: 0;
}

body.playing #tvShell:fullscreen .bar,
body.playing #tvShell:fullscreen .home {
  z-index: 2;
}

/* A game that did not start: in the middle of where it would be, large enough to read from the sofa. */
.game-failure {
  position: fixed;
  top: 30%;
  left: 50%;
  z-index: 3;
  width: min(760px, calc(100% - 96px));
  margin: 0;
  padding: 24px 32px;
  border-radius: var(--radius);
  background: #3b1d22;
  font-size: 28px;
  text-align: center;
  white-space: pre-line;
  transform: translateX(-50%);
}

body.playing .menu {
  display: none;
}

/* The shelf leaves the screen; the buttons and the QR become one column of the same width, with the same gap
   inside the buttons as between the column and the code. */
body.playing #tvShell {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: flex-end;
  gap: 8px;
  min-height: 100dvh;
  padding: 4%;
}

body.playing .bar {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  width: min(220px, 22vw);
  height: auto;
  padding: 8px;
  border-radius: 12px;
  background: rgb(11 13 18 / 82%);
}

body.playing .bar h1,
body.playing .join-hint,
body.playing .join-link {
  display: none;
}

body.playing .seats {
  flex-direction: column;
}

body.playing .home {
  position: relative;
  z-index: 2;
  display: block;
  width: min(220px, 22vw);
  padding: 0;
}

body.playing .join {
  position: static;
  display: grid;
  gap: 0;
  width: auto;
  padding: 8px;
  border-radius: var(--radius);
  font-size: inherit;
  line-height: 0;
  opacity: 0.92;
}

body.playing .join img {
  width: 100%;
  height: auto;
  border-radius: 12px;
  aspect-ratio: auto;
}

body.playing .ambient {
  display: none;
}

.network-dialog {
  width: min(600px, calc(100% - 48px));
  max-height: calc(100dvh - 48px);
  padding: 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--text);
  background: var(--panel);
  z-index: 20;
}
.network-dialog::backdrop {
  background: rgb(0 0 0 / 60%);
}
.network-dialog header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
.network-dialog h2 {
  font-size: 20px;
}
.network-dialog button {
  min-height: 48px;
  min-width: 48px;
}
.notices {
  padding-left: 20px;
}
.notices li {
  margin-block: 12px;
}

.overlay {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  background: rgb(0 0 0 / 70%);
}

.overlay .dialog {
  display: grid;
  gap: 20px;
  max-width: 560px;
  padding: 32px;
  border-radius: var(--radius);
  background: var(--panel-raised);
  font-size: 20px;
  text-align: center;
}

.overlay .dialog p {
  margin: 0;
}

.overlay.dragging .dialog {
  outline: 3px dashed var(--accent);
  outline-offset: 8px;
}

.actions {
  display: flex;
  justify-content: center;
  gap: 12px;
}

/* No travel for those who asked for none: the posters stay put and only fade; the glow and the words fade too. */
@media (prefers-reduced-motion: reduce) {
  .boot-splash {
    transition-duration: 120ms;
  }

  .boot-splash-logo {
    animation: none;
    transform: none;
  }

  .tiles button {
    transition: opacity 200ms linear;
  }

  .stage-caption.is-entering {
    transform: none;
  }

  .ambient-layer {
    transition-duration: 200ms;
  }
}
