/* Animated galaxy-field banner for the landing page. Loaded only by
   index.html; the space background it sits on is site-wide and lives in
   space-background.css. */

.dsdb-banner {
  position: relative;
  width: 100%;
  /* layout.html wraps content in <main class="pt-5">, i.e. 3rem down, but the
     fixed navbar is 75px tall and opaque (chrome.css), so the first 27px of
     the banner would sit behind it. Pushed clear of the bar and shortened by
     the same amount, so the whole sky is visible and the banner still ends
     exactly at the fold. */
  margin-top: calc(75px - 3rem);
  height: calc(100vh - 75px);
  min-height: 520px;
  margin-bottom: 2rem;
  overflow: hidden;
  /* No background of its own: the body paints the field, so the banner and the
     content below it are one continuous surface. */
  background: transparent;
}

/* Vignette: darkens the corners so the eye is pulled to the galaxies and the
   panel edges don't read as a flat rectangle. */
.dsdb-banner::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;              /* over the star field, so the corners fall off too */
  pointer-events: none;
  background: radial-gradient(120% 95% at 50% 45%,
    rgba(0, 0, 0, 0) 45%, rgba(2, 3, 10, 0.34) 78%, rgba(1, 2, 7, 0.62) 100%);
  /* Fade the vignette out before the bottom edge, otherwise its darkened rim
     draws a hard line where the banner meets the page below. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 70%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 70%, transparent 100%);
}

#banner-canvas {
  position: absolute;
  inset: 0;
  z-index: 1;              /* the deep sky and galaxies; vignette sits over it */
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 900ms ease;
}

.dsdb-banner.is-ready #banner-canvas {
  opacity: 1;
}

/* ---------- headline ---------- */

.banner-overlay {
  position: absolute;
  /* Raised 100px clear of the galaxy field; max() so a short viewport can't
     push it off the top. */
  top: max(1.25rem, calc(16vh - 100px));
  left: 6vw;
  max-width: 35rem;
  z-index: 4;
  pointer-events: none;         /* never blocks point picking */
  color: #fff;
  /* children that need the pointer re-enable it individually */
  text-shadow: 0 2px 24px rgba(4, 6, 20, 0.85);
}

/* Logo on the left, name and tagline stacked to its right, so the tagline
   sits under "DeepSpaceDB" rather than under the logo. */
.banner-wordmark {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  margin-bottom: 0.4rem;
}

.banner-wordmark-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.banner-overlay .banner-logo {
  height: clamp(44px, 4.6vw, 68px);
  flex: 0 0 auto;
  margin: 0;
  filter: drop-shadow(0 4px 18px rgba(0, 0, 0, 0.6));
}

.banner-overlay h1 {
  font-size: clamp(2.2rem, 4.4vw, 3.6rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0;
  line-height: 1;
  white-space: nowrap;
}

.banner-overlay .banner-subtitle {
  font-size: clamp(0.95rem, 1.35vw, 1.25rem);
  font-weight: 500;
  color: rgba(226, 232, 255, 0.92);
  margin: 0.3rem 0 0;
  letter-spacing: 0.01em;
}

/* ---------- live filter ---------- */

.banner-filter-wrap {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.35rem;
  padding: 0.5rem 0.9rem;
  max-width: 24rem;
  border-radius: 999px;
  background: rgba(10, 15, 38, 0.72);
  border: 1px solid rgba(140, 170, 255, 0.3);
  backdrop-filter: blur(8px);
  pointer-events: auto;         /* the overlay above it is inert */
  transition: border-color 180ms ease, box-shadow 180ms ease;
}

.banner-filter-wrap:focus-within {
  border-color: rgba(170, 200, 255, 0.7);
  box-shadow: 0 0 0 3px rgba(90, 130, 235, 0.22);
}

.banner-filter-icon {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  color: rgba(170, 192, 240, 0.85);
}

.banner-filter {
  flex: 1 1 auto;
  min-width: 0;
  background: none;
  border: 0;
  outline: none;
  color: #eef3ff;
  font-size: 0.86rem;
}

.banner-filter::placeholder { color: rgba(166, 184, 226, 0.65); }
.banner-filter::-webkit-search-cancel-button { filter: invert(0.8); }

.banner-filter-count {
  flex: 0 0 auto;
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
  color: rgba(150, 200, 255, 0.9);
  white-space: nowrap;
}

.banner-filter-count.is-empty { color: rgba(255, 165, 150, 0.9); }

/* ---------- fly-in ---------- */

.banner-back {
  position: absolute;
  /* The banner now starts below the navbar (see .dsdb-banner), so this is a
     plain inset from its own top edge. */
  top: 1.15rem;
  right: 1.35rem;
  z-index: 5;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.42rem 0.85rem;
  border-radius: 999px;
  background: rgba(12, 18, 44, 0.85);
  border: 1px solid rgba(150, 180, 255, 0.38);
  color: #e6eeff;
  font-size: 0.78rem;
  font-weight: 500;
  cursor: pointer;
  backdrop-filter: blur(8px);
  transition: background 160ms ease, border-color 160ms ease;
}

.banner-back:hover {
  background: rgba(24, 34, 74, 0.95);
  border-color: rgba(180, 205, 255, 0.65);
}

.banner-back i { width: 15px; height: 15px; }

.galaxy-label:hover .galaxy-label-title {
  color: #fff;
  text-shadow: 0 0 18px rgba(140, 175, 255, 0.9);
}

/* The headline steps back while a single galaxy has the stage. */
/* ---------- opener ---------- */

/* The overlay rises on load.
   The hidden start state is gated on .is-anim, which an inline script in the
   template adds while the page is still parsing - before the first paint. Two
   reasons: without a gate the text is invisible when JS never runs at all, and
   adding the class from a deferred script would paint the text once and then
   hide it, which is the flicker.
   The reveal itself is .is-text-in, added as soon as the script runs, NOT when
   the sample data arrives - the text used to wait for /banner_points, so on a
   cold cache it sat invisible for seconds and then jumped in. */
.dsdb-banner.is-anim .banner-overlay > * {
  opacity: 0;
  transform: translateY(14px);
}

.dsdb-banner.is-text-in .banner-overlay > * {
  animation: banner-rise 900ms cubic-bezier(0.22, 0.9, 0.3, 1) forwards;
}

.dsdb-banner.is-text-in .banner-wordmark    { animation-delay: 120ms; }
.dsdb-banner.is-text-in .banner-filter-wrap { animation-delay: 340ms; }
.dsdb-banner.is-text-in .banner-totals      { animation-delay: 460ms; }

@keyframes banner-rise {
  to { opacity: 1; transform: translateY(0); }
}

/* The opener hides the overlay until .is-ready, which only arrives when
   deck.gl is up. On the degraded path it never does, so the text has to be
   restored explicitly or the fallback hero would render blank. */
.dsdb-banner.is-static .banner-overlay > *,
.dsdb-banner:not(.is-anim) .banner-overlay > * {
  opacity: 1;
  transform: none;
  animation: none;
}

.dsdb-banner.is-static .banner-filter-wrap,
.dsdb-banner.is-static .banner-totals {
  display: none;              /* nothing to filter or count without the field */
}

@media (prefers-reduced-motion: reduce) {
  .dsdb-banner.is-anim .banner-overlay > * { opacity: 1; transform: none; }
  .dsdb-banner.is-text-in .banner-overlay > * { animation: none; }
}

/* Database totals, promoted from the stats card row into the hero. */
.banner-totals {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem 1.8rem;
  margin: 1.25rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid rgba(150, 180, 255, 0.22);
}

.banner-totals dt {
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(176, 194, 240, 0.75);
  margin: 0 0 0.15rem;
}

.banner-totals dd {
  margin: 0;
  font-size: clamp(1.35rem, 2.1vw, 1.85rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: #fff;
  text-shadow: 0 0 22px rgba(120, 160, 255, 0.4);
}

/* ---------- galaxy labels ---------- */

#banner-labels {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.galaxy-label {
  position: absolute;
  top: 0;
  left: 0;
  /* Starts hidden: the JS fades each label in as its galaxy assembles, and
     without this the label paints at its untransformed origin first. */
  opacity: 0;
  text-align: center;
  white-space: nowrap;
  /* The titles are the way in: clicking one flies the camera into that cloud.
     A previous duplicate rule set this to none, which silently killed the
     click handler bound in banner-deck.js. */
  pointer-events: auto;
  cursor: pointer;
  transition: opacity 260ms ease;
  will-change: transform;
}

.galaxy-label-title {
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(236, 241, 255, 0.94);
  transition: color 220ms ease, text-shadow 220ms ease;
}

.galaxy-label-meta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  margin-top: 0.28rem;
  font-size: 0.74rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
  color: rgba(158, 184, 255, 0.82);
}

.galaxy-label-sep {
  color: rgba(158, 184, 255, 0.4);
}

/* Hairline above the title, tying the label to its disc. */
.galaxy-label::before {
  content: '';
  display: block;
  width: 2.4rem;
  height: 1px;
  margin: 0 auto 0.55rem;
  background: linear-gradient(90deg, transparent, rgba(150, 180, 255, 0.55), transparent);
}

/* ---------- rocket ---------- */

.banner-rocket {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 3;                   /* above the canvas, below the hover card */
  pointer-events: auto;         /* clickable: it has something to say */
  cursor: pointer;
  will-change: transform;
  filter: drop-shadow(0 0 10px rgba(150, 190, 255, 0.55));
}

.banner-rocket:active {
  filter: drop-shadow(0 0 16px rgba(190, 220, 255, 0.9));
}

/* Speech bubble. A sibling of the rocket rather than a child, so it stays
   upright while the craft rotates. */
.banner-rocket-msg {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 4;
  pointer-events: none;
  padding: 0.4rem 0.7rem;
  border-radius: 999px;
  white-space: nowrap;
  font-size: 0.78rem;
  font-weight: 500;
  color: #eaf1ff;
  background: rgba(16, 22, 52, 0.92);
  border: 1px solid rgba(150, 180, 255, 0.4);
  box-shadow: 0 10px 30px rgba(2, 4, 16, 0.6);
  opacity: 0;
  transition: opacity 220ms ease;
}

.banner-rocket-msg > span {
  display: block;
  transform-origin: 50% 120%;
}

.banner-rocket-msg.is-shown {
  opacity: 1;
}

.banner-rocket-msg.is-shown > span {
  animation: rocket-pop 320ms cubic-bezier(0.22, 1.4, 0.4, 1);
}

@keyframes rocket-pop {
  0%   { transform: scale(0.7); }
  100% { transform: scale(1); }
}

/* The exhaust flickers on its own clock, independent of the flight path, so
   the rocket looks powered rather than pasted on. */
.rocket-flame {
  transform-origin: 26px 17px;   /* the nozzle, so the flare grows backwards */
  animation: rocket-thrust 0.16s steps(2, end) infinite;
}

@keyframes rocket-thrust {
  0%   { transform: scaleX(0.82); opacity: 0.85; }
  100% { transform: scaleX(1.12); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .banner-rocket,
  .banner-rocket-msg { display: none; }
  .rocket-flame { animation: none; }
}

/* ---------- hover card ---------- */

.banner-card {
  position: absolute;
  z-index: 5;
  width: 19rem;
  padding: 0.85rem 1rem 0.9rem;
  border-radius: 12px;
  background: rgba(12, 17, 42, 0.9);
  border: 1px solid rgba(140, 165, 255, 0.28);
  box-shadow: 0 18px 50px rgba(2, 4, 16, 0.7);
  backdrop-filter: blur(10px);
  color: #e8eeff;
  /* Interactive on purpose: the card carries the link to the sample, so the
     cursor has to be able to travel onto it. Paired with CARD_GRACE_MS in
     banner-deck.js, which keeps it open long enough to get there. */
  pointer-events: auto;
  font-size: 0.82rem;
  line-height: 1.45;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}

.banner-card:hover {
  border-color: rgba(170, 195, 255, 0.55);
  box-shadow: 0 18px 50px rgba(2, 4, 16, 0.78), 0 0 0 1px rgba(140, 165, 255, 0.18);
}

.banner-card-link {
  display: block;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}

.banner-card-link:hover,
.banner-card-link:focus {
  color: inherit;
  text-decoration: none;
}

.banner-card:hover .banner-card-cta {
  color: #cfe0ff;
}

/* Invisible bridge spanning the gap between the dot and the card, so the
   pointer never touches bare canvas on its way over. Without it, moving to the
   card crossed a few pixels of field and a neighbouring dot took the hover,
   swapping the card to a different sample. */
.banner-card::before {
  content: '';
  position: absolute;
  top: -6px;
  bottom: -6px;
  left: -14px;
  width: 14px;
}

.banner-card.flip-x::before {
  left: auto;
  right: -14px;
}

.banner-card-thumb {
  margin: -0.85rem -1rem 0.7rem;   /* bleed to the card edges */
  border-radius: 12px 12px 0 0;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.04);
  line-height: 0;
}

.banner-card-thumb img {
  display: block;
  width: 100%;
  height: 8.5rem;
  object-fit: cover;
  object-position: center;
}

.banner-card-head {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-bottom: 0.55rem;
}

.banner-card-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex: 0 0 auto;
  box-shadow: 0 0 10px currentColor;
}

.banner-card-id {
  font-weight: 700;
  letter-spacing: 0.02em;
}

.banner-card-galaxy {
  margin-left: auto;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(160, 180, 235, 0.75);
}

.banner-card-rows {
  display: grid;
  grid-template-columns: 5.2rem 1fr;
  gap: 0.15rem 0.6rem;
  margin: 0;
}

.banner-card-rows dt {
  font-weight: 500;
  color: rgba(158, 174, 216, 0.8);
}

.banner-card-rows dd {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.banner-card-desc {
  margin: 0.55rem 0 0;
  font-size: 0.76rem;
  color: rgba(200, 210, 240, 0.75);
}

.banner-card-cta {
  margin-top: 0.65rem;
  padding-top: 0.5rem;
  border-top: 1px solid rgba(140, 165, 255, 0.18);
  font-size: 0.76rem;
  font-weight: 600;
  color: #9dbcff;
}

/* ---------- scroll cue ---------- */

.banner-scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 1.1rem;
  transform: translateX(-50%);
  z-index: 4;
  color: rgba(200, 212, 245, 0.6);
  animation: banner-bob 2.4s ease-in-out infinite;
  pointer-events: none;
}

@keyframes banner-bob {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50%      { transform: translateX(-50%) translateY(7px); }
}

/* ---------- degraded / static fallback ---------- */

.dsdb-banner.is-static #banner-canvas,
.dsdb-banner.is-static #banner-labels {
  display: none;
}

.dsdb-banner.is-static .banner-overlay {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  max-width: 44rem;
}

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

@media (max-width: 900px) {
  .dsdb-banner { height: calc(88vh - 75px); }
  .banner-overlay { top: max(1rem, calc(9vh - 40px)); left: 50%; transform: translateX(-50%); text-align: center; max-width: 88vw; }
  .banner-wordmark { justify-content: center; }
  .banner-totals { justify-content: center; gap: 0 1.3rem; }
  .banner-card { width: 15.5rem; font-size: 0.78rem; }
}

@media (prefers-reduced-motion: reduce) {
  .banner-scroll-cue { animation: none; }
  #banner-canvas { transition: none; }
}
