:root {
  color-scheme: dark;
  font-family: Inter, system-ui, sans-serif;
  background: #101114;
  color: #f5f1e8;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  min-height: 100%;
  width: 100%;
}

body {
  background: #101114;
  color: #f5f1e8;
  overflow: hidden;
}

.app-shell {
  height: 100vh;
  height: 100svh;
  min-height: 100vh;
  min-height: 100svh;
  position: relative;
  width: 100%;
}

@supports (height: 100dvh) {
  .app-shell {
    height: 100dvh;
    min-height: 100dvh;
  }
}

.canvas-shell {
  height: 100%;
  min-height: 100%;
  position: relative;
}

canvas {
  opacity: 1;
  transition: opacity 760ms ease, filter 620ms ease;
  display: block;
  width: 100%;
  height: 100%;
  background: radial-gradient(circle at center, #252018 0%, #111114 58%, #07080a 100%);
  cursor: default;
  touch-action: manipulation;
}

.animation-heading {
  max-width: min(360px, calc(100vw - 40px));
  pointer-events: none;
  position: absolute;
  right: clamp(18px, 4vw, 48px);
  text-align: right;
  top: clamp(18px, 4vw, 42px);
  transition: opacity 620ms ease;
  z-index: 2;
}

/* The side panel (width 340px, its own opaque background) visually
   overlaps the top-right heading when both are on screen at once — hide
   the heading while the panel is open rather than let text collide with
   it. */
.canvas-shell:has(.active-moths-panel.is-open) .animation-heading {
  opacity: 0;
}

.animation-heading p {
  color: #f5f1e8;
  font-size: clamp(0.78rem, 1.1vw, 0.98rem);
  line-height: 1.35;
  margin: 0;
  opacity: 0.76;
}

.credit {
  bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(16px, 2.4vw, 30px));
  color: rgba(245, 241, 232, 0.5);
  font-size: 0.7rem;
  left: clamp(16px, 2.4vw, 30px);
  margin: 0;
  position: fixed;
  z-index: 11;
}

.credit a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.credit a:hover,
.credit a:focus-visible {
  color: rgba(245, 241, 232, 0.9);
}

.sound-toggle {
  align-items: center;
  background: rgba(10, 10, 12, 0.36);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 999px;
  bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(16px, 2.4vw, 30px));
  cursor: pointer;
  display: flex;
  height: 38px;
  justify-content: center;
  opacity: 0.78;
  padding: 0;
  position: fixed;
  right: clamp(16px, 2.4vw, 30px);
  transition: opacity 160ms ease, border-color 160ms ease, background 160ms ease;
  width: 38px;
  z-index: 11;
}

body.launch-screen-active canvas {
  opacity: 0;
}

body.launch-screen-active.launch-light-visible canvas {
  opacity: 1;
}

body.launch-screen-active:not(.launch-controls-visible) .animation-heading,
body.launch-screen-active:not(.launch-controls-visible) .sound-toggle,
body.launch-screen-active:not(.launch-controls-visible) .active-moths-toggle,
body.launch-screen-active:not(.launch-controls-visible) .active-moths-panel,
body.launch-screen-active:not(.launch-controls-visible) .loading-status,
body.launch-screen-active:not(.launch-controls-visible) .loading-spinner,
body.launch-screen-active:not(.launch-controls-visible) .fallback-status,
body.launch-screen-active:not(.launch-controls-visible) .debug-status {
  opacity: 0;
  pointer-events: none;
}

/* Real observations can take a few seconds to arrive on a slow connection —
   without this, that window looks identical to a broken/empty page (just
   the light, nothing else) instead of "still loading". While it lasts
   (.is-loading on .canvas-shell, cleared by app.js once the feed reports any
   real state) the whole scene is greyed out and a spinner turns around the
   light, with this text underneath pulsing gently. Nothing flashes: the
   light itself stays steady. */
.canvas-shell.is-loading canvas {
  filter: grayscale(1) brightness(0.55);
}

/* left/top/size/margin are set by app.js (positionLoadingSpinner) so the ring
   sits on the light; the values here are just a sane fallback. */
.loading-spinner {
  animation: loading-spinner-rotate 0.9s linear infinite;
  border: 3px solid rgba(245, 241, 232, 0.22);
  border-radius: 50%;
  border-top-color: rgba(245, 241, 232, 0.92);
  height: 46px;
  left: 50%;
  margin: -23px 0 0 -23px;
  opacity: 0;
  pointer-events: none;
  position: fixed;
  top: 50%;
  transition: opacity 620ms ease;
  width: 46px;
  z-index: 10;
}

.canvas-shell.is-loading .loading-spinner {
  opacity: 1;
}

@keyframes loading-spinner-rotate {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .loading-spinner {
    animation-duration: 2.4s;
  }
}

.loading-status {
  animation: loading-status-pulse 1.8s ease-in-out infinite;
  bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(16px, 2.4vw, 30px));
  color: rgba(245, 241, 232, 0.85);
  font-size: clamp(0.95rem, 3vw, 1.15rem);
  font-weight: 500;
  left: 50%;
  letter-spacing: 0.01em;
  margin: 0;
  pointer-events: none;
  position: fixed;
  text-align: center;
  text-shadow: 0 0 18px rgba(255, 214, 130, 0.35);
  transform: translateX(-50%);
  transition: opacity 620ms ease;
  white-space: nowrap;
  z-index: 10;
}

.loading-status.is-hidden {
  animation: none;
  opacity: 0;
}

@keyframes loading-status-pulse {
  0%, 100% {
    opacity: 0.85;
  }
  50% {
    opacity: 0.45;
  }
}

/* Shown only when the live feed has failed before ever delivering a real
   batch (see app.js's fallback-mode wiring) — the app.js-set text names
   where the substitute moths are actually from, rather than this file
   guessing at copy. Deliberately steady rather than pulsing like
   .loading-status: this isn't "still loading", it's a settled (if
   unwanted) state that can persist for a while. Sits just above
   .loading-status so the two can never overlap during the brief instant
   both are visible (fallback mode begins the moment the first poll fails,
   right as the loading text is fading out). */
.fallback-status {
  background: rgba(10, 10, 12, 0.45);
  border: 1px solid rgba(255, 214, 130, 0.32);
  border-radius: 10px;
  bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(16px, 2.4vw, 30px) + 40px);
  color: rgba(245, 241, 232, 0.85);
  font-size: clamp(0.78rem, 2.6vw, 0.92rem);
  left: 50%;
  line-height: 1.4;
  margin: 0;
  max-width: min(480px, calc(100vw - 40px));
  padding: 8px 16px;
  pointer-events: none;
  position: fixed;
  text-align: center;
  transform: translateX(-50%);
  transition: opacity 620ms ease;
  z-index: 10;
}

.fallback-status.is-hidden {
  opacity: 0;
}

/* A small, always-on live-state readout (connection state, request/observation
   counts, queue/store sizes) so a real-device bug report can include exact
   diagnostic state without needing devtools access on a phone. Deliberately
   plain and out of the way rather than polished chrome. */
.debug-status {
  background: rgba(10, 10, 12, 0.5);
  border-radius: 6px;
  bottom: calc(env(safe-area-inset-bottom, 0px) + clamp(16px, 2.4vw, 30px) + 22px);
  color: rgba(245, 241, 232, 0.55);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.68rem;
  left: 50%;
  margin: 0;
  padding: 3px 8px;
  pointer-events: none;
  max-width: 94vw;
  position: fixed;
  text-align: center;
  transform: translateX(-50%);
  white-space: pre-wrap;
  word-break: break-word;
  z-index: 10;
}

.debug-status:empty {
  display: none;
}

.active-moths-toggle {
  align-items: center;
  background: rgba(10, 10, 12, 0.32);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  color: rgba(245, 241, 232, 0.86);
  cursor: pointer;
  display: flex;
  height: 34px;
  justify-content: center;
  padding: 0;
  position: fixed;
  right: clamp(14px, 2.1vw, 26px);
  top: 50%;
  transform: translateY(-50%);
  transition: background 260ms ease, border-color 260ms ease, opacity 820ms ease, transform 520ms ease;
  width: 34px;
  z-index: 12;
}

.active-moths-toggle:hover,
.active-moths-toggle:focus-visible {
  background: rgba(18, 18, 22, 0.72);
  border-color: rgba(255, 255, 255, 0.42);
  outline: none;
}

.active-moths-toggle::before,
.active-moths-toggle::after {
  background: currentColor;
  border-radius: 999px;
  content: "";
  height: 2px;
  position: absolute;
  transition: transform 260ms ease;
  width: 13px;
}

.active-moths-toggle::before {
  transform: translateY(-4px) rotate(-45deg);
}

.active-moths-toggle::after {
  transform: translateY(4px) rotate(45deg);
}

.active-moths-toggle.is-open {
  transform: translateY(-50%) translateX(-286px);
}

.active-moths-toggle.is-open::before {
  transform: translateY(-4px) rotate(45deg);
}

.active-moths-toggle.is-open::after {
  transform: translateY(4px) rotate(-45deg);
}

.active-moths-panel {
  background: #08080a;
  border-left: 1px solid rgba(255, 255, 255, 0.12);
  bottom: 0;
  box-shadow: -18px 0 34px rgba(0, 0, 0, 0.22);
  display: flex;
  flex-direction: column;
  gap: 10px;
  opacity: 0;
  padding: calc(env(safe-area-inset-top, 0px) + 18px) 14px calc(env(safe-area-inset-bottom, 0px) + 18px);
  pointer-events: none;
  position: fixed;
  right: 0;
  top: 0;
  transform: translateX(100%);
  transition: opacity 520ms ease, transform 520ms ease;
  width: 340px;
  z-index: 11;
}

.active-moths-panel.is-open {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(0);
}

.active-moths-panel__title {
  color: rgba(245, 241, 232, 0.82);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0;
  margin: 0;
  text-align: right;
}

.active-moths-list {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
  overflow-x: hidden;
  overflow-y: scroll;
  padding: 2px 2px 2px 0;
  /* A clearly visible vertical scrollbar, always shown: with many moths in
     the scene the cards are taller than the panel and the list scrolls
     (cards never shrink to fit — see .active-moths-card). The old "thin" bar
     in a barely-there colour was easy to miss entirely. Overlay-scrollbar
     platforms (phones, macOS) may still hide it until you scroll. */
  scrollbar-color: rgba(245, 241, 232, 0.55) rgba(255, 255, 255, 0.08);
  scrollbar-gutter: stable;
  scrollbar-width: auto;
}

/* Safari (older versions) ignores scrollbar-color/-width, so it gets the
   equivalent via the WebKit pseudo-elements. Everywhere else those
   pseudo-elements would override the standard properties above, so they're
   only applied where the standard ones aren't supported. */
@supports not (scrollbar-color: auto) {
  .active-moths-list::-webkit-scrollbar {
    width: 12px;
  }

  .active-moths-list::-webkit-scrollbar-track {
    background: rgba(255, 255, 255, 0.08);
    border-radius: 6px;
  }

  .active-moths-list::-webkit-scrollbar-thumb {
    background: rgba(245, 241, 232, 0.55);
    border-radius: 6px;
  }

  .active-moths-list::-webkit-scrollbar-thumb:hover {
    background: rgba(245, 241, 232, 0.8);
  }
}

.active-moths-card {
  background: rgba(255, 255, 255, 0.065);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 8px;
  color: rgba(245, 241, 232, 0.9);
  display: flex;
  flex-direction: column;
  /* Never squashed to fit: the list scrolls instead. Shrunk cards (which
     overflow: hidden) clip the text under the photo once a few are open. */
  flex-shrink: 0;
  min-height: 58px;
  opacity: 1;
  position: relative;
  text-align: left;
  transform: translateX(0);
  overflow: hidden;
  max-height: 460px;
  transition: background 220ms ease, border-color 220ms ease, opacity 820ms ease, transform 820ms ease, max-height 820ms ease, min-height 820ms ease;
  width: 100%;
  will-change: transform, opacity;
  border-color: rgba(255, 255, 255, 0.32);
}

.active-moths-card.is-entering,
.active-moths-card.is-leaving {
  opacity: 0;
  transform: translateX(14px);
}

.active-moths-card.is-entering {
  opacity: 0.08;
}

.active-moths-card.is-leaving {
  min-height: 0;
  max-height: 0;
}

.active-moths-card.is-focused {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.42);
}

.active-moths-card:hover,
.active-moths-card:focus-within {
  background: rgba(255, 255, 255, 0.1);
  border-color: rgba(255, 255, 255, 0.28);
  outline: none;
}

.active-moths-card__focus {
  all: unset;
  box-sizing: border-box;
  cursor: pointer;
  display: grid;
  column-gap: 8px;
  grid-template-columns: auto 1fr;
  padding: 9px 10px;
  row-gap: 4px;
  width: 100%;
}

.active-moths-card__focus:focus-visible {
  outline: 2px solid rgba(245, 241, 232, 0.5);
  outline-offset: -2px;
}

/* The photo runs the full width of the card, on top — big enough to
   actually see the moth. Elements below are auto-placed, so with no photo
   (hidden) the swatch/name simply move up to the first row. */
.active-moths-card__thumb {
  aspect-ratio: 4 / 3;
  border-radius: 6px;
  grid-column: 1 / -1;
  height: auto;
  margin-bottom: 2px;
  max-height: 230px;
  object-fit: cover;
  width: 100%;
}

.active-moths-card__swatch {
  border-radius: 999px;
  box-shadow: 0 0 10px currentColor;
  height: 9px;
  margin-top: 5px;
  width: 9px;
}

.active-moths-card__name {
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.2;
  overflow-wrap: anywhere;
}

/* The same facts the on-canvas hover pop-out shows: upload time, how
   precisely it's identified, where, and the photo credit. */
.active-moths-card__details {
  display: flex;
  flex-direction: column;
  gap: 2px;
  grid-column: 1 / -1;
}

.active-moths-card__uploaded,
.active-moths-card__identification,
.active-moths-card__place {
  color: rgba(245, 241, 232, 0.66);
  font-size: 0.74rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.3;
}

.active-moths-card__credit {
  color: rgba(245, 241, 232, 0.48);
  font-size: 0.68rem;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.active-moths-card__meta {
  color: rgba(245, 241, 232, 0.48);
  font-size: 0.68rem;
  grid-column: 2;
  line-height: 1.2;
}

.active-moths-card__link {
  color: rgba(245, 241, 232, 0.66);
  font-size: 0.72rem;
  padding: 0 10px 9px;
  text-decoration: none;
}

.active-moths-card__link:hover,
.active-moths-card__link:focus-visible {
  color: rgba(245, 241, 232, 0.92);
  text-decoration: underline;
}

.sound-toggle span {
  inset: 0;
  position: absolute;
}

.sound-toggle:hover,
.sound-toggle:focus-visible {
  background: rgba(18, 18, 22, 0.72);
  border-color: rgba(255, 255, 255, 0.42);
  opacity: 1;
  outline: none;
}

.sound-toggle::before {
  border-bottom: 7px solid transparent;
  border-right: 11px solid rgba(245, 241, 232, 0.86);
  border-top: 7px solid transparent;
  content: "";
  height: 0;
  left: 9px;
  margin-left: 0;
  position: absolute;
  top: 11px;
  width: 0;
}

.sound-toggle::after {
  border: 2px solid rgba(245, 241, 232, 0.86);
  border-bottom-color: transparent;
  border-left-color: transparent;
  border-radius: 50%;
  content: "";
  height: 9px;
  left: 17px;
  margin-left: 0;
  position: absolute;
  top: 12px;
  transform: rotate(45deg);
  width: 9px;
}

.sound-toggle:not(.is-on)::after {
  display: none;
}

.sound-toggle span::before,
.sound-toggle span::after {
  background: rgba(245, 241, 232, 0.86);
  border-radius: 999px;
  content: "";
  height: 2px;
  opacity: 0;
  position: absolute;
  left: 19px;
  top: 18px;
  transform-origin: center;
  transition: opacity 120ms ease;
  width: 11px;
}

.sound-toggle:not(.is-on) span::before {
  opacity: 1;
  transform: rotate(45deg);
}

.sound-toggle:not(.is-on) span::after {
  opacity: 1;
  transform: rotate(-45deg);
}

.launch-screen {
  align-items: center;
  background: radial-gradient(circle at center, #252018 0%, #111114 58%, #07080a 100%);
  bottom: 0;
  color: #f5f1e8;
  display: flex;
  inset: 0;
  justify-content: center;
  left: 0;
  position: fixed;
  right: 0;
  top: 0;
  transition: opacity 280ms ease;
  width: 100%;
  z-index: 20;
}

.launch-screen.launch-screen--hidden {
  opacity: 0;
  pointer-events: none;
}

.launch-screen__panel {
  text-align: center;
  transition: opacity 300ms ease;
}

.launch-screen.launch-screen--switch-hidden .launch-screen__panel {
  opacity: 0;
  pointer-events: none;
}

/* A UK-style rocker light switch: the button is the faceplate, ::after is the
   rocker. Off = top edge raised; on = top edge pressed in. */
.launch-screen__switch {
  background: linear-gradient(145deg, #ffffff 0%, #f1f1ee 60%, #e4e4e0 100%);
  border: 0;
  border-radius: 14px;
  box-shadow:
    0 14px 30px rgba(0, 0, 0, 0.55),
    0 2px 4px rgba(0, 0, 0, 0.4),
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    inset 0 -2px 3px rgba(0, 0, 0, 0.12);
  cursor: pointer;
  display: inline-block;
  height: 150px;
  padding: 0;
  position: relative;
  transition: filter 180ms ease;
  width: 150px;
}

.launch-screen__switch:hover {
  filter: brightness(1.04);
}

.launch-screen__switch:focus-visible {
  outline: 2px solid rgba(245, 241, 232, 0.7);
  outline-offset: 4px;
}

.launch-screen__switch::after {
  background: linear-gradient(180deg, #ffffff 0%, #f4f4f1 45%, #dcdcd7 100%);
  border-radius: 5px;
  box-shadow:
    0 6px 8px rgba(0, 0, 0, 0.3),
    0 1px 2px rgba(0, 0, 0, 0.35),
    inset 0 1px 0 #ffffff;
  content: "";
  height: 66px;
  left: 50%;
  margin: -33px 0 0 -23px;
  position: absolute;
  top: 50%;
  transform: perspective(240px) rotateX(-14deg);
  transition: transform 200ms ease, box-shadow 200ms ease, background 200ms ease;
  width: 46px;
}

.launch-screen__switch.is-on::after {
  background: linear-gradient(180deg, #d6d6d1 0%, #f0f0ed 55%, #ffffff 100%);
  box-shadow:
    0 -4px 6px rgba(0, 0, 0, 0.25),
    0 1px 2px rgba(0, 0, 0, 0.35),
    inset 0 1px 0 #ffffff;
  transform: perspective(240px) rotateX(14deg);
}

.launch-screen__text {
  color: rgba(245, 241, 232, 0.88);
  font-size: 1rem;
  margin-top: 18px;
}

@media (max-width: 620px) {
  .animation-heading {
    max-width: calc(100vw - 32px);
    right: 16px;
    top: 16px;
  }
}
