/* ---------------------------------------------------------------
   RESET — a 2026-era baseline.
   Wrapped in a cascade layer so anything outside @layer always wins,
   no matter how specific the reset selector is.
   --------------------------------------------------------------- */
@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  /* Only kill the margins the browser invents for us. */
  :where(
    body,
    h1,
    h2,
    h3,
    h4,
    h5,
    h6,
    p,
    figure,
    blockquote,
    dl,
    dd,
    ul,
    ol,
    fieldset
  ) {
    margin: 0;
  }

  html {
    /* Consistent text sizing when a phone is turned sideways. */
    -webkit-text-size-adjust: none;
    text-size-adjust: none;

    /* Lets height animate to/from `auto` and other intrinsic sizes. */
    interpolate-size: allow-keywords;
  }

  @media (prefers-reduced-motion: no-preference) {
    html:focus-within {
      scroll-behavior: smooth;
    }
  }

  body {
    min-block-size: 100svb;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
  }

  /* Headings shouldn't inherit body leading, and shouldn't strand
     a single word on the last line. */
  :where(h1, h2, h3, h4, h5, h6, button, label) {
    line-height: 1.2;
    text-wrap: balance;
  }

  :where(p, li, figcaption) {
    text-wrap: pretty;
    /* Long URLs and code-ish strings can't blow out the layout. */
    overflow-wrap: break-word;
  }

  /* Replaced elements behave like blocks and stay inside their box. */
  :where(img, picture, video, canvas, svg, iframe) {
    display: block;
    max-inline-size: 100%;
    block-size: auto;
  }

  :where(img, video) {
    /* Reserve space from width/height attrs before the file lands. */
    block-size: auto;
  }

  /* Form controls opt out of font inheritance by default. */
  :where(input, button, textarea, select) {
    font: inherit;
    color: inherit;
  }

  :where(textarea:not([rows])) {
    min-block-size: 10em;
  }

  :where(button, [role="button"], label, summary) {
    cursor: pointer;
  }

  /* Keyboard focus stays visible; mouse focus doesn't flash a ring. */
  :where(:focus-visible) {
    outline: 2px solid currentColor;
    outline-offset: 2px;
  }

  :where(ul, ol)[role="list"] {
    list-style: none;
    padding-inline-start: 0;
  }

  :where(table) {
    border-collapse: collapse;
    border-spacing: 0;
  }

  /* Anchored content (skip links, in-page jumps) clears any header. */
  :where(:target) {
    scroll-margin-block-start: 3rem;
  }

  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }
}

/* ---------------------------------------------------------------
   PLANET — the canvas fills the viewport and nothing scrolls.
   --------------------------------------------------------------- */
body {
  overflow: hidden;
  background: #101119; /* matches SKY.background in worlds.js, so no flash before
                          three.js paints its first frame */
}

/* THE INDEX — the system's worlds, in the order they orbit.
   Wraps rather than scrolls: eight names is more than one line on
   a narrow window, and a row that runs off both edges hides the
   outer planets, which are the ones you'd have to go looking
   for. */
#ui {
  position: fixed;
  bottom: 22px;
  /* Spanned and centred, NOT parked at the middle and pulled
     back: a fixed box given only `left` is offered the room from
     there to the edge, so half the window is all the row ever
     gets to lay itself out in — and eight names wrap to two lines
     on a screen with space for one. */
  left: 0;
  right: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2px 10px;
  padding-inline: 16px;
}
:root {
  /* ui-monospace is the OS's own designed-for-screens mono — SF
     Mono on macOS, which is the pretty one. The rest are named
     fallbacks for machines that don't expose it, ending at the
     generic keyword so there's always something. */
  --mono:
    ui-monospace, "SF Mono", SFMono-Regular, Menlo, "JetBrains Mono",
    "Cascadia Code", Consolas, monospace;

  /* The survey panel's own set. Silkscreen is a bitmap face — the
     one part of the pixel look CSS can't fake — and the mono
     stack catches the frame before the webfont lands. */
  --pixel: "Silkscreen", var(--mono);
  --panel-bg: rgba(9, 10, 17, 0.93);
  --panel-edge: #333955;
  --panel-rule: #232840;
  --panel-void: #05060b; /* darker than the sky, so edges read as cut */
  --panel-key: #6b6e86;
  --panel-value: #e6ebff;

  /* Rewritten per planet from its own palette — see updateFacts().
     This is only the colour worn before the first world is
     built. */
  --accent: #8adce0;
  /* The same hue with the contrast a small pixel font needs. A
     band colour is chosen to sit on a lit planet, not on a black
     panel, and the dark end of the set (a jungle green, a
     crevasse blue) goes illegible at 10px — cutting it with
     white lifts every planet's accent into the same readable
     range without flattening the hues into each other. It
     re-resolves whenever --accent is overwritten, since both
     live on the same element. */
  --accent-text: color-mix(in srgb, var(--accent) 72%, #ffffff);
}

/* Grab-to-orbit: the cursor advertises that the view is draggable
   before you try, and changes while you're holding it. */
canvas {
  cursor: grab;
  /* Touch drags orbit the camera instead of scrolling the page. */
  touch-action: none;
}
/* Over a world out in the solar system: a click goes there. Set
   before .dragging so a drag that starts on one still reads as a
   drag. */
canvas.targetable {
  cursor: pointer;
}
canvas.dragging {
  cursor: grabbing;
}

#controls {
  position: fixed;
  top: 24px;
  right: 24px;
  display: flex;
  align-items: center;
  gap: 10px;
}

/* The way back out, mirroring #controls in the opposite corner.
   It sits above the survey panel rather than beside it — the
   panel's top edge starts below this row, see #facts. */
#nav {
  position: fixed;
  top: 24px;
  left: 24px;
  display: flex;
  align-items: center;
  gap: 10px;
}
#nav button,
#controls button {
  font: 13px/1 var(--mono);
  text-transform: lowercase;
  letter-spacing: 0.02em;
  color: #fffdfa;
  background: rgba(16, 17, 25, 0.72);
  border: 1px solid #33364a;
  border-radius: 999px;
  padding: 10px 18px;
  cursor: pointer;
}
#nav button:hover,
#controls button:hover {
  border-color: #5a5e7c;
}

/* No frame on these: eight boxed names read as a toolbar, and
   this is a list of places. The mark does the carrying and the
   name sits next to it in the panel's own grey, until it's the
   one you're standing on. */
#ui button {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  border: none;
  background: none;
  font: 12px/1 var(--mono);
  letter-spacing: 0.02em;
  text-transform: capitalize;
  color: var(--panel-key);
  cursor: pointer;
}
#ui button:hover {
  color: var(--panel-value);
}
#ui button.active {
  color: var(--panel-value);
}

/* Under the name you're on, at the width of the name — a rule,
   not a box, so nothing moves when it appears. */
#ui button.active span {
  border-block-end: 1px solid currentColor;
  padding-block-end: 2px;
}

.mark {
  inline-size: 12px;
  block-size: 12px;
  fill: currentColor;
  flex: none;
}

#facts {
  position: fixed;
  /* Clears the #nav row above it: 24px inset + the button's own
     35px + a 12px gap. */
  top: 71px;
  left: 24px;
  inline-size: 272px;
  font-family: monospace;
  font-size: 12px;
  color: var(--panel-value);
  background: var(--panel-bg);
  border: 2px solid var(--panel-edge);
  pointer-events: none;
  overflow: hidden;
}

#facts-head {
  padding: 10px 12px 12px;
  border-block-end: 1px solid var(--panel-rule);
}

#facts-kicker {
  display: flex;
  align-items: center;
  gap: 6px;
  /* Fixed, not intrinsic — see #facts-name below. An empty span
     is a zero-height flex item, so this line has no height at all
     until the first character of the seed lands in it. */
  block-size: 1lh;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-text);
}

#facts-name {
  margin-block-start: 6px;
  /* One line, fixed. A name is a single word of four to twelve
     letters (see assemble() in readout.js) so it never wraps, and
     a header that measures itself against its contents is a
     header that changes height as those contents are typed into
     it. `lh` is this element's own line-height, so the two lines
     of the head both follow their font sizes down at narrow
     widths. */
  block-size: 1lh;
  font-size: 14px;
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: 0.02em;
  text-transform: capitalize;
  text-shadow: 2px 2px 0 var(--panel-void);
}

#facts-body {
  padding-block-end: 10px;
}

.fact-section {
  margin-block-start: 8px;
  padding: 7px 12px 3px;
  border-block-start: 2px solid var(--panel-rule);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-text);
}

/* The write-up. Same 12px gutter as the rows below it, so the
   prose and the data start on one left edge — the two blocks
   read as one panel rather than as a paragraph with a table
   pasted under it. Block padding is set separately: it's spacing
   between sections, not the alignment, and it has no reason to
   match. */
.fact-prose {
  padding-inline: 12px;
  padding-block: 10px 4px;
  line-height: 1.65;
  color: var(--panel-value);
  text-wrap: pretty;
}

/* THE BLANKS. Every interpolation in a madlibs.json template comes
   out as one of these — see madlib() in readout.js — so this rule
   is where any mad-libs effect would live.

   Nothing in it at the moment, and that's deliberate: the
   write-up is one colour now. Dimming the fixed wording put half
   a paragraph of 12px type below the contrast it needs to be read
   at, and did it to mark a distinction the reader can already
   see — the facts ARE the specific words. Bolding was tried here
   before and read as too heavy. If a treatment comes back it
   should be one that doesn't spend legibility to get it: a rule
   under the fact, or the planet's own accent rather than a
   greyer grey. */

.fact {
  display: flex;
  align-items: baseline;
  gap: 7px;
  padding-inline: 12px;
  padding-block: 1px;
}
.fact-key {
  color: var(--panel-key);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
}
/* Leader dots as a background rather than typed characters, so
   the run is always exactly as long as the gap it has to cross. */
.fact-dots {
  flex: 1;
  block-size: 2px;
  align-self: center;
  background: repeating-linear-gradient(
    to right,
    var(--panel-rule) 0 2px,
    transparent 2px 6px
  );
}
.fact-value {
  white-space: nowrap;
  color: var(--panel-value);
}

/* The colour itself, boxed in the same dark keyline as the panel
   so a pale band still has an edge against the background. */
.swatch {
  inline-size: 10px;
  block-size: 10px;
  align-self: center;
  box-shadow: 0 0 0 1px var(--panel-void);
}

/* ── the solar system: the wide shot ─────────────────────────── */
/* No ONE planet on screen means nothing for the survey panel to
   describe, nothing for the leader line to point at, and nowhere
   for "back" to go — so all three step out. The row along the
   bottom stays: it's how you dive into a world. */
/* Id selectors, not :where() — #ui's own `display: flex` carries
   id specificity, and a zero-weight :where() loses to it. */
body.system-view #facts,
body.system-view #callout,
body.system-view #nav {
  display: none;
}

/* The arrival, in two acts. Mid-approach nothing is on screen but
   the world itself. The seat stops, the node blinks on and the
   line runs out of it — that's `revealing`, and the card is the
   one thing still held back. When the line lands, both classes
   are gone and the panel is simply there.

   visibility, not display — the panel keeps its box, so the
   measurement the leader line is drawn from stays valid while
   it's out of sight. */
body.arriving #facts,
body.arriving #callout,
body.revealing #facts {
  visibility: hidden;
}

/* Unfolding: the frame is on screen, opening out of the seam the
   line plugged into, but there is nothing written on it yet. The
   words step in whole when it finishes — a squashed line of type
   stretching back to its proper height is the one part of this
   that would look like a fade. */
body.opening #facts-head,
body.opening #facts-body {
  visibility: hidden;
}

/* ── the hover label ─────────────────────────────────────────── */
/* The same frame as the survey card, at the size of one line —
   it's the card's first line, arriving early. Placed by
   showTip(); everything here is how it looks, not where it is. */
#tip {
  position: fixed;
  inset-block-start: 0;
  inset-inline-start: 0;
  padding: 5px 8px 6px;
  font-family: monospace;
  white-space: nowrap;
  background: var(--panel-bg);
  border: 2px solid var(--panel-edge);
  /* Never in the way of the thing it is labelling: it sits under
     the cursor and the cursor is picking planets. */
  pointer-events: none;
  visibility: hidden;
}
#tip.on {
  visibility: visible;
}

/* The name, then what it is — the card's own two head lines at
   the size a label can be read at in passing. */
#tip-name {
  display: block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: capitalize;
  color: var(--panel-value);
}
#tip-kind {
  display: block;
  margin-block-start: 2px;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--panel-key);
}

/* ── the leader line ─────────────────────────────────────────── */
#callout {
  position: fixed;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  pointer-events: none;
  shape-rendering: crispEdges;
}
#callout-path {
  fill: none;
  stroke: var(--accent-text);
  stroke-width: 2;
}
#callout-shade {
  fill: none;
  stroke: var(--panel-void);
  stroke-width: 2;
  transform: translate(2px, 2px);
}
#callout-node {
  fill: var(--accent-text);
}

/* Stepped, never faded — a pixel display has no in-between frames
   to fade through.

   The node blinks on its own the whole time it's on screen. The
   caret doesn't: it's solid while it writes and blinks once at
   each pause, which is a thing only the printout knows the timing
   of — see blinkCaret() in script.js. */
@media (prefers-reduced-motion: no-preference) {
  #callout-node {
    animation: pixel-blink 1.6s steps(1) infinite;
  }
}

/* Pinned rather than inherited: the caret leaves the kicker to
   lead the title in, and a cursor that changes colour when it
   changes lines reads as two different things. */
.caret {
  color: var(--accent-text);
}
@keyframes pixel-blink {
  0%,
  60% {
    opacity: 1;
  }
  60.01%,
  100% {
    /* Dimmed, never off. A node that vanishes leaves the line
       hanging in space on every other frame. */
    opacity: 0.45;
  }
}

@media (max-width: 760px) {
  #controls {
    top: 16px;
    right: 16px;
    gap: 8px;
  }
  #nav {
    top: 16px;
    left: 16px;
    gap: 8px;
  }
  #nav button,
  #controls button {
    padding: 9px 14px;
  }
  #ui button {
    padding: 5px 6px;
  }

  #callout,
  #facts-body {
    display: none;
  }
  #facts {
    /* 16px inset + the shorter mobile button + a 10px gap. */
    top: 59px;
    left: 16px;
    /* Stops short of the controls in the opposite corner. */
    inline-size: min(50vw, 220px);
  }
  #facts-head {
    border-block-end: none;
  }
  #facts-name {
    font-size: 13px;
  }
}
