/* ------------------------------------------------------------------
   Video Collage
------------------------------------------------------------------ */

:root {
  --bg: #0b0c0e;
  --fg: #f2f3f5;
  --muted: #8b9098;
  --line: #23262b;
  --accent: #4da3ff;
  --rows: 11vh;
  --overscan: 1.3;   /* >1 pushes YouTube's title bar outside the tile */

  /* One easing vocabulary rather than a scatter of `ease` and `linear`.
     Things arriving decelerate hard and settle; things moving between two
     places accelerate and decelerate evenly; things leaving just go. */
  --ease-out: cubic-bezier(.16, 1, .3, 1);

  /* Expanding a tile gets its own curve. --ease-out is an expo: it covers most
     of the distance in the first moment and spends the rest decelerating,
     which reads as a snap no matter how long the duration. A box travelling
     that far wants its movement spread across the whole time instead. */
  --ease-open: cubic-bezier(.4, .05, .2, 1);
  --ease-move: cubic-bezier(.65, 0, .35, 1);
  --ease-in: cubic-bezier(.5, 0, .75, 0);
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;          /* the mosaic is sized to fit, never to scroll */
  background: var(--bg);
  color: var(--fg);
  font: 15px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

/* --- top bar ---------------------------------------------------- */

.bar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--line);
  flex-wrap: wrap;
  flex: 0 0 auto;
  background: var(--bg);
}

.bar__title {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.bar__status {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.bar__actions {
  margin-left: auto;
  display: flex;
  gap: 8px;
}

.btn {
  font: inherit;
  font-size: 13px;
  color: var(--bg);
  background: var(--fg);
  border: 1px solid var(--fg);
  border-radius: 999px;
  padding: 6px 16px;
  cursor: pointer;
  transition: opacity .15s ease;
}

.btn:hover { opacity: .82; }

/* Only a keyboard visit gets a ring. Clicking a button left it looking picked
   out long after the click, which read as the bar highlighting itself. */
.btn:focus { outline: none; }

.btn:focus-visible {
  outline: 1px solid var(--fg);
  outline-offset: 2px;
}
.btn:disabled { opacity: .4; cursor: default; }

/* Icon beside a word, rather than instead of it. */
.btn--label {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 15px 6px 12px;
}

.btn--label .icon {
  width: 16px;
  height: 16px;
}

/* Icon-only: square-ish, and sized off the glyph rather than a text label. */
.btn--icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 5px;
  width: 32px;
  height: 32px;
  background: transparent;
  color: var(--fg);
  border-color: var(--line);
}

.btn--icon:hover { opacity: 1; border-color: var(--fg); }

.icon {
  width: 18px;
  height: 18px;
  display: block;
}

/* Only one of the two speakers shows at a time. */
.icon--off { display: none; }
body.is-paused .icon--on { display: none; }
body.is-paused .icon--off { display: block; }

.btn--ghost {
  background: transparent;
  color: var(--fg);
  border-color: var(--line);
}

/* How far through the track is, shown by filling the button that names it
   rather than by adding a bar somewhere. Very faint: it is the shape of the
   thing that reads, not the brightness. */
#track-btn {
  position: relative;
  overflow: hidden;                    /* the fill keeps to the pill */
}

#track-btn::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--fg);
  opacity: .12;
  transform: scaleX(var(--played, 0));
  transform-origin: 0 50%;
  /* timeupdate lands about four times a second, which would step. Linear,
     because the track it is following runs at a constant rate. */
  transition: transform .3s linear;
  pointer-events: none;
}

/* Above the fill, or the wash sits over the word. */
#track-btn .icon,
#track-btn span { position: relative; }

/* --- the collage ------------------------------------------------ */

.collage {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: var(--rows);
  grid-auto-flow: row dense;
  gap: 0;
  padding: 0;
}

/* Ten tiles: an exact 4×3 tiling, stretched to fill whatever height is left
   under the bar, so the mosaic meets every edge with no seams. Three rows
   rather than six keeps each cell near 16:9 — a cell is a quarter of the
   window wide and a third of it tall. */
.collage--mosaic {
  flex: 1 1 auto;
  min-height: 0;
  grid-template-rows: repeat(3, 1fr);

  /* The drift. Moving tiles individually would tear holes in the tiling, so
     it is the grid's own tracks that breathe instead: every tile changes size
     and position continuously and the mosaic stays exactly packed. The two
     durations are deliberately unrelated, so rows and columns never come back
     into step and the pattern doesn't visibly repeat. */
  /* Linear, deliberately. `ease-in-out` applies between *every* pair of
     keyframes, so the grid slowed almost to a stop four times a cycle and the
     drift read as a series of nudges. Continuous motion wants no easing. */
  animation:
    lb-drift-cols var(--drift-cols, 71s) linear infinite,
    lb-drift-rows var(--drift-rows, 53s) linear infinite;
}

@keyframes lb-drift-cols {
  0%   { grid-template-columns: 1fr 1fr 1fr 1fr; }
  25%  { grid-template-columns: 1.42fr .78fr 1.12fr .68fr; }
  50%  { grid-template-columns: .70fr 1.34fr .76fr 1.20fr; }
  75%  { grid-template-columns: 1.15fr .72fr 1.40fr .73fr; }
  100% { grid-template-columns: 1fr 1fr 1fr 1fr; }
}

@keyframes lb-drift-rows {
  0%   { grid-template-rows: 1fr 1fr 1fr; }
  33%  { grid-template-rows: 1.36fr .76fr .88fr; }
  66%  { grid-template-rows: .78fr 1.30fr .92fr; }
  100% { grid-template-rows: 1fr 1fr 1fr; }
}

/* An expanded tile is out of the flow, but its slot is not — and the drift
   would carry that slot on without it. The tile then animated back to where
   it had been rather than where it now belonged, missing by further the
   longer the video was watched. So the grid holds its breath for as long as
   one is open, and picks up exactly where it left off. */
body.is-expanded .collage--mosaic { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .collage--mosaic { animation: none; }
  .start__mark { animation: none; }
}

.no-drift .collage--mosaic { animation: none; }

.collage--mosaic .tile {
  grid-column: var(--c) / span var(--cs);
  grid-row: var(--r) / span var(--rs);
}

.tile {
  position: relative;
  overflow: hidden;
  /* Darker than it was: this only shows while a tile is loading, and any of it
     visible at an edge reads as a pale outline against the video. */
  background: linear-gradient(160deg, #131519, #0c0e11);
  padding: 0;
  border: 0;
  cursor: pointer;

  /* No container-type here. Nothing queries the tile any more, and size
     containment makes an element its own containing block — so a tile set to
     position:fixed filled its own grid cell instead of the window, which is
     what stopped this working the first time. */
  /* span values are written inline by the script */
}

/* A tile is a div with role="button", so without this it gets the browser's
   default ring: 3px of currentColor, which on this page is very nearly white,
   drawn hard against the edge. On an expanded video that is a white line
   around the whole panel — and because a mouse click leaves the tile focused,
   it appeared the moment the keyboard was touched, Escape included. */
.tile:focus { outline: none; }

/* A keyboard visit still has to be able to see where it is. Inside the edge,
   so it reads as part of the tile rather than a rectangle over the mosaic. */
.tile:focus-visible {
  outline: 1px solid var(--fg);
  outline-offset: -2px;
}

/* Loading state. These clips take a few seconds to buffer and start, and the
   grid would otherwise sit as ten dark rectangles — so each tile carries a
   slow sweep of light until its video is up. The stagger is negative delay,
   so every tile is already mid-sweep on the first frame rather than starting
   from a synchronised standstill. */
.tile::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    100deg,
    transparent 20%,
    rgba(255, 255, 255, .05) 42%,
    rgba(255, 255, 255, .10) 50%,
    rgba(255, 255, 255, .05) 58%,
    transparent 80%
  );
  background-size: 220% 100%;
  animation: lb-sweep 2.8s linear infinite;
  animation-delay: calc(var(--i, 0) * -0.34s);
  transition: opacity .5s ease;
}

/* Once the video is playing the sweep fades off over it. */
.tile.is-live::before {
  opacity: 0;
  animation: none;
}


@keyframes lb-sweep {
  from { background-position: 170% 0; }
  to   { background-position: -70% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .tile::before {
    animation: none;
    background: rgba(255, 255, 255, .04);
  }
}

/* Iframes ignore object-fit, so the player is oversized and centred by hand:
   container units measure the tile, and the 16:9 player is grown until it
   covers the short side. Fallback below is a plain letterboxed fit. */
.tile__frame {
  position: absolute;
  /* Scaling happens from the top-left corner, not the middle. The frame is
     placed at the tile's centre and pulled back by half its own size, and that
     -50% is written in the same transform as the open/close scale. About the
     default centre origin the two fight: the frame slides by half its width
     times (1 - scale), which for a frame sized to fill the screen is thousands
     of pixels. It left the tile during the animation, showing the tile's own
     background at the edge and shoving the video into a corner. From 0 0 the
     scale multiplies the offset instead of adding to it, which is exactly what
     the geometry assumes. */
  transform-origin: 0 0;
  /* The iframe's own backdrop. White by default, which is the last thing that
     should ever show at a clipped edge. */
  background: var(--bg);
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border: 0;
  pointer-events: none;   /* clicks belong to the tile, not YouTube */
  width: 100%;
  height: 100%;
  /* Hidden over the still frame until playback starts, so the poster,
     play button and title bar are never seen. */
  opacity: 0;
  transition: opacity var(--swap-fade, 1.2s) ease;
}

.tile__frame.is-playing { opacity: 1; }

/* The old video of a crossfade: the next one is already showing beneath it,
   and it fades away to reveal it. */
.tile__frame.is-leaving { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .tile__frame { transition: none; }
}

/* No container-query sizing here on purpose. cqw/cqh are measured against the
   tile, so while the tile drifts they resize the iframe on every frame, and
   YouTube re-lays-out its whole player inside it each time. sizeFrame() always
   writes a pixel size instead, which stays put as the tile moves around it. */

/* --- tuning static ---------------------------------------------- */

/* Detuned-TV static over each tile. The texture is hard-pixel noise drawn on
   a canvas at load and handed in as --static: an SVG turbulence patch reads
   as a soft haze, and what makes static look like static is that every pixel
   is fully opaque and unrelated to its neighbour.
   ::before is already the loading sweep, so this rides on ::after, above the
   iframe. The offsets are deliberately large and unrelated, so consecutive
   steps share nothing and the field boils rather than slides. */
.tile::after {
  content: '';
  position: absolute;
  inset: -25%;              /* room to jump without exposing an edge */
  z-index: 2;
  pointer-events: none;
  background-image: var(--static);
  background-repeat: repeat;
  background-size: 180px 180px;
  opacity: var(--grain, .15);
  transition: opacity var(--tune, 3.5s) cubic-bezier(.2, .7, .3, 1);
  animation: lb-static .5s steps(1) infinite;

  /* Deliberately not promoted with will-change. This layer is white noise
     inset by -25%, so it is larger than the tile that clips it. Promoted, it
     gets its own compositor layer whose bounds are cached — and after a tile
     had been fixed-position and full-screen, the stale layer kept painting at
     the old size, leaving a pale rim around the tile once it was back in the
     grid. Unpromoted it is painted with its parent and clipped every time.
     The animation steps twice a second, so the promotion bought little. */
}

/* Tuning in. The static snaps to full — no transition on the way up, so it
   arrives as a wall of noise — and then the rule above eases it back down to
   its resting level over --tune, with the picture surfacing through it. */
body.is-tuning .tile::after {
  opacity: 1;
  transition: none;
}

body.is-tuning .tv {
  opacity: 1;
  transition: none;
}

/* Offsets stay inside the -25% inset above. They were nearly twice this, which
   needed an inset of -60% to hide the edges — and that meant every tile was
   painting a noise layer roughly five times its own area, ten times over,
   sixteen times a second. */
@keyframes lb-static {
  0%   { transform: translate(0, 0); }
  12%  { transform: translate(-52px, 31px); }
  25%  { transform: translate(44px, -47px); }
  37%  { transform: translate(-24px, -38px); }
  50%  { transform: translate(66px, 18px); }
  62%  { transform: translate(-47px, 44px); }
  75%  { transform: translate(28px, -14px); }
  87%  { transform: translate(-61px, -40px); }
  100% { transform: translate(0, 0); }
}

/* Scanlines and the slow vertical roll of a set that won't hold its picture.
   These sit on one overlay across the whole screen rather than per tile — a
   scanline that stopped at a tile edge would give the grid away, and it must
   not jitter with the static. */
.tv {
  position: fixed;
  inset: 0;
  z-index: 20;
  pointer-events: none;
  opacity: var(--scanlines, .5);
  transition: opacity var(--tune, 3.5s) cubic-bezier(.2, .7, .3, 1);
  background: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, .30) 0 1px,
    rgba(0, 0, 0, 0) 1px 3px
  );
}

.tv::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 24%;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0),
    rgba(255, 255, 255, .045) 45%,
    rgba(255, 255, 255, .07) 55%,
    rgba(255, 255, 255, 0)
  );
  top: -26%;
  will-change: transform;
  animation: lb-roll 8s linear infinite;
}

/* Moved with transform rather than `top`. Animating `top` is a layout
   property, so it made the browser lay the overlay out again on every frame
   for the whole eight seconds; a transform is handled by the compositor and
   looks identical. */
@keyframes lb-roll {
  from { transform: translateY(0); }
  to   { transform: translateY(542%); }
}

/* The bar has to stay above the overlay, or the scanlines cross the title. */
.bar {
  position: relative;
  z-index: 25;
}

@media (prefers-reduced-motion: reduce) {
  .tile::after,
  .tv::after { animation: none; }
}

/* --- beginning and end ------------------------------------------ */

/* The grid is hidden until Play is pressed and fades away again when the
   soundtrack ends. It loads and plays throughout — only its opacity changes —
   so the videos are already running when they fade up. */
.collage,
.bar {
  transition: opacity var(--fade-in, 2.5s) var(--ease-out);
}

body:not(.is-started) .collage,
body:not(.is-started) .bar {
  opacity: 0;
  pointer-events: none;
}

/* --- the start button ------------------------------------------- */

/* The way in: a full-screen play button over an empty stage. */
.start {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 34px;
  width: 100%;
  height: 100%;
  border: 0;
  cursor: pointer;
  font: inherit;
  color: var(--fg);
  background: var(--bg);
  transition: opacity .5s ease;
}

.start.is-going {
  opacity: 0;
  pointer-events: none;
}

.start[hidden] { display: none; }

.start__title,
.start__mark,
.start__text {
  animation: lb-rise .7s var(--ease-out) backwards;
}

.start__text { animation-delay: .16s; }

.start__title {
  margin-bottom: 0;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--fg);
}

/* A thin ring with a small triangle in it, rather than a bare 58px arrowhead.
   The ring gives the mark an edge to react on, so hover has something to do
   besides scale the whole thing up. */
.start__mark {
  position: relative;
  display: grid;
  place-items: center;
  width: 50px;
  height: 50px;
  border: 1px solid rgba(255, 255, 255, .28);
  border-radius: 50%;

  /* It rises once, then breathes until it is pressed — the only moving thing
     on the title screen, so the eye has somewhere to go while the grid loads
     behind it. Slow and shallow: a signal that this is waiting for you, not a
     button demanding to be clicked. */
  animation:
    lb-rise .7s var(--ease-out) .08s backwards,
    lb-pulse 2.6s ease-in-out .9s infinite;

  transition:
    border-color .4s var(--ease-out),
    transform .4s var(--ease-out);
}

.start__mark::before {
  content: '';
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 6px 0 6px 11px;
  border-color: transparent transparent transparent var(--fg);
  /* A triangle's visual mass sits left of its bounding box, so centring it
     geometrically leaves it looking off. Nudge it back to the right. */
  margin-left: 2px;
  transition: transform .4s var(--ease-out);
}

/* A ring that expands and fades on hover — the only flourish, and it costs
   nothing because it is a pseudo-element on a compositor-friendly transform. */
.start__mark::after {
  content: '';
  position: absolute;
  inset: -1px;
  border: 1px solid var(--fg);
  border-radius: 50%;
  opacity: 0;
  transform: scale(1);
  transition: opacity .5s ease, transform .5s var(--ease-out);
}

@keyframes lb-pulse {
  0%, 100% { border-color: rgba(255, 255, 255, .16); }
  50%      { border-color: rgba(255, 255, 255, .72); }
}

/* The pulse keeps running under this, so border-color here is decorative
   only — the loop owns that property. The scale is what hover actually does. */
.start:hover .start__mark {
  border-color: rgba(255, 255, 255, .65);
  transform: scale(1.04);
}

.start:hover .start__mark::before { transform: scale(1.06); }

.start:hover .start__mark::after {
  opacity: .18;
  transform: scale(1.28);
}

.start:active .start__mark { transform: scale(.97); }

.start__text {
  margin-top: 0;
  font-size: 10px;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--muted);
  opacity: .8;
}

/* --- opened tile ------------------------------------------------ */

/* Clicking a tile lifts it out of the grid to fill the window. The same
   element and the same iframe: re-parenting an iframe reloads it, and a second
   player would have to buffer from cold, so the video simply keeps playing as
   its box grows. Its grid slot is left empty behind it, which nothing can see
   because the tile is covering the screen. */
.tile.is-open {
  position: fixed;
  z-index: 30;
  cursor: default;

  /* Deliberately square. The static layer is white noise inset by -25%, on its
     own compositor layer, and a promoted layer overflowing a rounded
     `overflow: hidden` parent is exactly what Chrome fails to clip mid-
     animation — the noise painted past the corners as a white rim while the
     tile was closing. A 3px radius is not worth that. */
  box-shadow: 0 30px 80px rgba(0, 0, 0, .7);
  /* Opening decelerates into place; closing is a shade quicker and eases both
     ends, so it retreats rather than being flung back. */
  transition:
    left var(--open, .9s) var(--ease-open),
    top var(--open, .9s) var(--ease-open),
    width var(--open, .9s) var(--ease-open),
    height var(--open, .9s) var(--ease-open);
}

/* The frame grows on the same curve, or the picture stops covering the tile
   partway and the edges go dark. */
.tile.is-open .tile__frame {
  /* Only the transform. Width and height are set once when the tile opens and
     left alone: animating them would make YouTube re-lay out its player on
     every frame, which is what put a pale edge on the closing tile. */
  transition:
    transform var(--open, .9s) var(--ease-open),
    opacity .3s ease;
}

/* Everything behind an expanded video is blurred — the grid, and the
   scanlines with it, since the overlay sits below this. The video itself is
   above and stays sharp. */
.backdrop {
  position: fixed;
  inset: 0;
  z-index: 25;
  background: rgba(3, 4, 6, .82);
  backdrop-filter: blur(30px) saturate(.7);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--open, .9s) var(--ease-out);
}

.backdrop[hidden] { display: none; }

.backdrop.is-in {
  opacity: 1;
  pointer-events: auto;      /* clicking the surround closes the video */
  cursor: pointer;
}

.closer {
  position: fixed;
  top: 14px;
  right: 16px;
  z-index: 35;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  font: inherit;
  font-size: 26px;
  line-height: 1;
  color: var(--fg);
  background: rgba(10, 11, 13, .55);
  border: 1px solid var(--line);
  border-radius: 50%;
  cursor: pointer;
  backdrop-filter: blur(4px);
  opacity: 0;
  transform: scale(.85);
  transition: opacity .3s var(--ease-out), transform .3s var(--ease-out);
}

/* Held back until the tile has nearly finished growing, so it settles onto a
   still picture instead of racing it. */
.closer.is-in {
  opacity: 1;
  transform: none;
}

.closer[hidden] { display: none; }
.closer:hover { border-color: var(--fg); }

/* Closing: slightly quicker, and eased at both ends so it retreats rather
   than being thrown back into the grid. */
.tile.is-closing,
.tile.is-closing .tile__frame {
  transition-duration: calc(var(--open, .9s) * .8);
  transition-timing-function: var(--ease-move);
}

@media (prefers-reduced-motion: reduce) {
  .tile.is-open,
  .tile.is-open .tile__frame { transition-duration: .01s; }
}

/* --- track picker ----------------------------------------------- */

.picker { position: relative; }

/* Anchored under its button rather than centred like the about panel: it is a
   menu, and it should read as belonging to the control that opened it. */
.picker__list {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 45;
  min-width: 11rem;
  padding: 5px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #101216;
  box-shadow: 0 12px 30px rgba(0, 0, 0, .55);

  /* Grows out of the button it belongs to rather than appearing whole. */
  transform-origin: top right;
  animation: lb-pop .22s var(--ease-out);
}

@keyframes lb-pop {
  from { opacity: 0; transform: scale(.94) translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

.picker__list[hidden] { display: none; }

.picker__item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 7px 10px;
  font: inherit;
  font-size: 13px;
  text-align: left;
  white-space: nowrap;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: 3px;
  cursor: pointer;
}

.picker__item:hover { color: var(--fg); background: rgba(255, 255, 255, .05); }

.picker__item[aria-checked="true"] { color: var(--fg); }

/* A dot rather than a tick: it reads as "playing" instead of "selected". */
.picker__dot {
  width: 5px;
  height: 5px;
  flex: 0 0 5px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0;
}

.picker__item[aria-checked="true"] .picker__dot { opacity: 1; }

.picker__time {
  margin-left: auto;
  padding-left: 14px;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  opacity: .75;
}

/* --- about ------------------------------------------------------ */

/* Sits above the static overlay and the bar, so the text is read on a still,
   clean ground rather than through scanlines. */
.info {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(6, 7, 9, .82);
  backdrop-filter: blur(6px);
  animation: lb-fade .3s ease;
}

.info__panel {
  animation: lb-rise .42s var(--ease-out);
}

@keyframes lb-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

.info[hidden] { display: none; }

@keyframes lb-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.info__panel {
  position: relative;
  max-width: 33rem;
  max-height: 100%;
  overflow-y: auto;
  padding: 34px 38px 30px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #101216;
}

.info__title {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
}

.info__by {
  margin: 0 0 20px !important;
  font-size: 12.5px !important;
  letter-spacing: .04em;
  color: var(--muted) !important;
}

.info p {
  margin: 0 0 14px;
  font-size: 14px;
  line-height: 1.62;
  color: #c3c7cd;
}

.info em { font-style: italic; color: var(--fg); }

.info__credit {
  margin: 20px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  font-size: 12.5px !important;
  color: var(--muted) !important;
}

.info__close {
  position: absolute;
  top: 8px;
  right: 12px;
  padding: 4px 8px;
  font: inherit;
  font-size: 22px;
  line-height: 1;
  color: var(--muted);
  background: none;
  border: 0;
  cursor: pointer;
}

.info__close:hover { color: var(--fg); }

@media (max-width: 560px) {
  .info__panel { padding: 30px 22px 24px; }
}

/* --- messages --------------------------------------------------- */

.error {
  margin: 40px auto;
  max-width: 620px;
  padding: 18px 20px;
  border: 1px solid #55282c;
  border-radius: 6px;
  background: #1c1113;
  color: #ffc9cd;
  font-size: 14px;
}

.error code {
  color: var(--fg);
  font-size: 13px;
  word-break: break-all;
}

/* --- responsive ------------------------------------------------- */

@media (max-width: 900px) {
  :root { --rows: 15vh; }
  body { overflow: auto; }

  .collage--mosaic {
    flex: 0 0 auto;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: none;
    grid-auto-rows: var(--rows);
    animation: none;          /* the tracks are no longer what drifts */
  }

  /* Auto-flow instead, keeping the tall tiles tall. */
  .collage--mosaic .tile {
    grid-column: auto / span 1;
    grid-row: auto / span var(--rs);
  }
}

@media (max-width: 560px) {
  :root { --rows: 18vh; }
  .collage--mosaic { grid-template-columns: repeat(2, 1fr); }
  .collage--mosaic .tile { grid-row: auto / span 1; }
  .bar__actions { margin-left: 0; }
}
