/* ==========================================================================
   Birth chart calculator

   Tokens, type scale and rhythm come from docs/DESIGN.md. Components use
   named tokens only, so there is no raw hex below the :root block.
   ========================================================================== */

:root {
  color-scheme: light;

  /* Ground and ink. */
  --ink: #14213a;
  --ink-soft: #4a5773;
  --paper: #fbf9f4;
  --paper-warm: #f3efe4;
  --card: #ffffff;
  /* Raised from #D9D2C4, which was 1.31:1 on the tinted band and drew no
     edge at all. This clears 2.5:1 on all three light grounds and 3.64:1 on
     navy, so one token draws every card edge on the site. */
  --rule: #9e9179;

  /* The dark bands: masthead, the wheel, the footer. */
  --navy: #1b3a6b;
  --navy-deep: #12264a;

  /* Gold is the visitor's own data. Rust, sage and slate are section
     colours: one per band, never two in the same band. */
  /* Two golds on purpose, and they must not be merged back.
     --gold is 3.79:1 on navy and belongs to the dark bands: the wheel
     glyphs and the emblem. It is only 2.59:1 on the tinted band, which
     fails even as large text, so it is never set on a light ground.
     --gold-ink is 4.22:1 on the tinted band and 4.84:1 on white, and is
     what the price and the visitor's three signs are set in. */
  --gold: #c8892a;
  --gold-ink: #986820;
  --rust: #a6462e;
  --sage: #4c6b57;
  --slate: #52678c;

  /* Not in DESIGN.md, but the wheel needs linework on navy and the doc
     asks for 3:1 there. Navy mixed 45% toward paper: 3.48:1. */
  --wheel-line: #8090a9;
  /* Light text on a dark band. paper on navy is 10.71:1. */
  --on-navy: var(--paper);
  --on-navy-soft: #b9c6dd;
  /* The section accents are 1.90:1 on navy, so the aspect chords take
     lightened variants. Composited at 0.85 opacity these are 3.52:1 and
     3.35:1, and 46 points apart in the red channel, which is what makes
     harmonious readable as different from hard rather than just softer.
     A first pass at 0.65 opacity measured fine and was invisible on the
     render: thin strokes lose most of their contrast to anti-aliasing. */
  --sage-on-navy: #92a496;
  --rust-on-navy: #c88e7d;
  /* navy-deep at 72%, behind the placement sheet. */
  --scrim: rgb(18 38 74 / 0.72);

  --display: "Fraunces", Georgia, "Times New Roman", serif;
  --body: "Public Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --data: "IBM Plex Mono", ui-monospace, SFMono-Regular, Consolas, monospace;

  --shell: 1160px;
  /* The header lines up with the body shell. */
  --header-shell: var(--shell);
  --gutter: 20px;
  --band: 56px;
  --section: 56px;
  --measure: 68ch;
  --radius: 6px;

  /* The header band and the medallion that straddles it. --band above is the
     vertical rhythm of a section, which is why this one is spelled out. The
     pair is what sets the overhang: (medallion - band) / 2, so 12px here and
     30px on desktop. */
  --sky-band-h: 118px;
  --medallion: 142px;
}

@media (min-width: 561px) {
  :root {
    --sky-band-h: 132px;
    --medallion: 158px;
  }
}

@media (min-width: 900px) {
  :root {
    --gutter: 40px;
    --band: 88px;
    --section: 88px;
    --sky-band-h: 172px;
    --medallion: 232px;
  }
}

/* --------------------------------------------------------------------------
   Reset
   -------------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 17px/1.6 var(--body);
  /* Newsreader carries an optical size axis; let the browser drive it. */
  font-optical-sizing: auto;
}

h1,
h2,
h3,
p,
ul,
dl,
dd,
figure {
  margin: 0;
}

ul {
  padding: 0;
  list-style: none;
}

a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-underline-offset: 3px;
}

a:hover {
  text-decoration-color: var(--ink-soft);
}

:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Type scale. The mobile and desktop sizes are literal, not fluid.
   -------------------------------------------------------------------------- */

h1 {
  font: 600 34px/1.12 var(--display);
  letter-spacing: -0.02em;
  text-wrap: balance;
}

h2 {
  font: 600 24px/1.2 var(--display);
  letter-spacing: -0.01em;
  text-wrap: balance;
}

h3 {
  font: 600 18px/1.35 var(--body);
}

p {
  max-width: var(--measure);
}

@media (min-width: 900px) {
  body {
    font-size: 18px;
  }

  h1 {
    font-size: 52px;
  }

  h2 {
    font-size: 32px;
  }

  h3 {
    font-size: 21px;
  }
}

/* --------------------------------------------------------------------------
   Bands

   The page is a stack of full-width grounds, not one surface. Each band
   carries its own ground and one section accent, and the accent is used for
   that band's heading rule and card edge. Never two accents in one band.
   -------------------------------------------------------------------------- */

.band {
  padding-block: var(--band);
}

/* The first band of the result opens the most important screen on the site.
   A visitor who has just pressed Calculate is put at the top of it by
   result.focus(), and a full band of padding spends 56px of a 568px phone on
   air before the first sign appears.

   Only the first one, and only inside the result: the padding between bands is
   what separates them, and this band has nothing above it to be separated
   from. */
#result > .band:first-child {
  padding-top: 20px;
}

.band--paper { background: var(--paper); }
.band--warm { background: var(--paper-warm); }
.band--white { background: var(--card); }

.band--navy,
.band--deep {
  color: var(--on-navy);
}

.band--navy { background: var(--navy); }
.band--deep { background: var(--navy-deep); }

.band--navy h1,
.band--navy h2,
.band--navy h3,
.band--deep h1,
.band--deep h2,
.band--deep h3 {
  color: var(--on-navy);
}

/* One accent per band, read by the rules and card edges inside it. */
.band--slate { --accent: var(--slate); }
.band--sage { --accent: var(--sage); }
.band--rust { --accent: var(--rust); }

/* The mark that opens a band. Information, not ornament: it is how the
   visitor sees that a new part of the page has started. */
.band__title {
  padding-top: 14px;
  border-top: 3px solid var(--accent, var(--rule));
}

.band__eyebrow {
  font: 600 13px/1 var(--body);
  letter-spacing: 0.06em;
  color: var(--accent, var(--ink-soft));
}

/* --------------------------------------------------------------------------
   Shell and header
   -------------------------------------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

/* The header is a cream utility line over a navy sky band, with the medallion
   straddling the band's edge. The band is --navy-deep rather than --navy: it
   is the closer match to the artwork, and it is what lets the gold CTA clear
   4.5:1: --gold is 5.04:1 on --navy-deep against 3.79:1 on --navy.

   Sizes here are literal px like the rest of the type scale. The band and the
   medallion are tokens because their difference is the design: the medallion
   is 60px taller than the band at every breakpoint above 900, which is the
   30px of overhang top and bottom. */

/* --paper, not --paper-warm. The padding below the band is what the medallion
   overhangs into, and in warm it read as a 36px strip of a second cream butted
   against the page: measured #F3EFE4 from y=220 to y=255 and #FBF9F4 from 256
   at 1440. One ground, no seam. */
.site-header {
  position: relative;
  padding-bottom: 22px;
  background: var(--paper);
}

/* The utility line. The domain on the left, the product's one-line subtitle
   on the right; DESIGN.md calls it a subtitle, so it takes no full stop. */
.header-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: var(--header-shell);
  margin: 0 auto;
  padding: 12px 20px;
  font: 400 11px/1 var(--data);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.header-top a {
  color: inherit;
  text-decoration: none;
}

.header-top a:hover {
  color: var(--gold-ink);
}

/* The band. The flat fill is the fallback the artwork paints over, and it is
   the same value as the rect inside sky-band.svg, so a failed image request
   changes the star field and nothing else. */
.sky-band {
  position: relative;
  height: var(--sky-band-h);
  background: var(--navy-deep) url("/img/sky-band.svg") center / cover no-repeat;
}

/* One grid at every width: the menu button, then the lockup, then whatever
   nav the width can afford. align-content as well as align-items, because the
   medallion is taller than the band and the row is therefore taller than the
   container; without it the row sits flush to the top and the whole overhang
   lands below the band.

   The button is in the flow rather than absolutely placed. Floating it over a
   centred lockup is what the demo did, and at 390 the medallion runs into it:
   measured -3px of clearance at 390 and -38px at 320. The cost of the flow is
   that the lockup centres in the space left over rather than on the band, so
   it sits about 28px right of centre on a phone. */
.band-inner {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  align-content: center;
  gap: 12px;
  height: 100%;
  max-width: var(--header-shell);
  margin: 0 auto;
  padding-inline: 16px;
}

.lockup {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  justify-self: center;
  text-decoration: none;
}

/* The medallion is taller than the band on purpose and overhangs it. */
.lockup img {
  display: block;
  width: var(--medallion);
  height: var(--medallion);
}

/* Below 375 the wordmark cannot sit beside the medallion without pushing the
   page wider than the viewport: measured 29px of overflow and a horizontal
   scrollbar at 320. The medallion carries the mark on its own down there. */
.lockup-text {
  display: none;
  flex-direction: column;
  gap: 4px;
}

@media (min-width: 375px) {
  .lockup-text {
    display: flex;
  }
}

.lockup .wordmark {
  font: 400 16px/1 var(--display);
  letter-spacing: 0.1em;
  white-space: nowrap;
  color: var(--on-navy);
}

.subword {
  font: 400 8px/1 var(--data);
  letter-spacing: 0.26em;
  padding-left: 0.2em;
  color: var(--gold);
}

/* --------------------------------- the band nav and the drawer that
   replaces it below 900px */

.band-nav {
  display: none;
  align-items: center;
  gap: 28px;
}

.band-nav--right {
  justify-content: flex-end;
}

.band-nav a {
  padding-block: 6px;
  font: 400 15px/1 var(--body);
  color: var(--on-navy);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.band-nav a:hover,
.band-nav a:focus-visible {
  border-bottom-color: var(--gold);
}

/* The pill is filled, not open. The artwork is drawn with slice, so which
   stars land under the CTA changes with the viewport: measured 15 ink pixels
   below 4.5:1 and 5 below 3:1 at 1440, on an open pill. Deleting the stars
   underneath fixes one width and breaks the next. A ground fixes every
   width, and it is the same value as the band's own fill, so the pill reads
   as a hole punched in the star field rather than a plate laid on it. */
.band-nav .cta {
  padding: 8px 20px;
  color: var(--gold);
  background: var(--navy-deep);
  border: 1px solid var(--gold);
  border-radius: 999px;
}

.band-nav .cta:hover {
  color: var(--navy-deep);
  background: var(--gold);
  border-bottom-color: var(--gold);
}

.menu-toggle {
  display: block;
  padding: 8px 10px;
  line-height: 0;
  color: var(--on-navy);
  background: none;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  cursor: pointer;
}

.menu-toggle svg {
  width: 20px;
  height: 20px;
}

/* The drawer sits above the placement sheet: it is the only thing that can be
   opened from the header, and the sheet cannot be open behind it. */
.side-panel {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 61;
  display: flex;
  flex-direction: column;
  width: min(340px, 86vw);
  padding: 24px 28px;
  overflow-y: auto;
  background: var(--paper-warm);
  border-right: 1px solid var(--rule);
  transform: translateX(-100%);
  transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}

.side-panel[data-open="true"] {
  transform: translateX(0);
}

.side-panel a {
  padding-block: 14px;
  font: 400 17px/1 var(--body);
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
}

.side-panel a:hover {
  color: var(--gold-ink);
}

.panel-close {
  align-self: flex-end;
  margin-bottom: 8px;
  padding: 4px 8px;
  font-size: 22px;
  color: var(--ink-soft);
  background: none;
  border: 0;
  cursor: pointer;
}

.scrim {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--scrim);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.32s;
}

.scrim[data-open="true"] {
  opacity: 1;
  visibility: visible;
}

@media (min-width: 561px) {
  .band-inner {
    gap: 14px;
    padding-inline: 20px;
  }

  .lockup .wordmark {
    font-size: 18px;
  }

  .subword {
    font-size: 9px;
  }

  /* Only the CTA survives the middle width; the reading links are in the
     drawer with it. */
  .band-nav--right {
    display: flex;
  }

  .band-nav--right a:not(.cta) {
    display: none;
  }

  .band-nav .cta {
    padding: 7px 14px;
    font-size: 13px;
  }
}

@media (min-width: 900px) {
  .site-header {
    padding-bottom: 36px;
  }

  .header-top {
    padding: 18px 32px;
    font-size: 12px;
  }

  .band-inner {
    grid-template-columns: 1fr auto 1fr;
    gap: 24px;
    padding-inline: 32px;
  }

  .lockup {
    gap: 18px;
  }

  .lockup .wordmark {
    font-size: 30px;
    letter-spacing: 0.14em;
  }

  .subword {
    font-size: 11px;
    letter-spacing: 0.34em;
  }

  .band-nav,
  .band-nav--right a:not(.cta) {
    display: flex;
  }

  .band-nav .cta {
    padding: 8px 20px;
    font-size: 15px;
  }

  .menu-toggle {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .side-panel,
  .scrim {
    transition: none;
  }
}

main {
  display: block;
}

/* --------------------------------------------------------------------------
   Hero. Copy left, form card right on desktop; stacked on mobile.
   -------------------------------------------------------------------------- */

.hero {
  display: grid;
  gap: 32px;
}

@media (min-width: 900px) {
  .hero {
    grid-template-columns: 1fr 400px;
    gap: 64px;
    align-items: start;
  }
}

.lede {
  margin-top: 16px;
  max-width: var(--measure);
  font-size: 19px;
  color: var(--ink-soft);
}

@media (min-width: 900px) {
  .lede {
    font-size: 21px;
  }
}

/* Three factual lines. The mobile layout in DESIGN.md does not carry them. */
.proof {
  display: none;
}

@media (min-width: 900px) {
  .proof {
    display: grid;
    gap: 8px;
    max-width: var(--measure);
    margin-top: 40px;
    padding-top: 24px;
    border-top: 1px solid var(--rule);
  }

  .proof li {
    font: 400 14px/1.5 var(--body);
    color: var(--ink-soft);
  }
}

/* --------------------------------------------------------------------------
   Today's sky

   The real positions right now, in the hero. It takes the dark ground for the
   same reason the natal wheel does: a chart drawing reads as an instrument on
   dark and as a diagram on light. The frame reserves its own square before
   the ephemeris has finished downloading, so nothing shifts when it lands.
   -------------------------------------------------------------------------- */

.skypanel {
  margin-top: 32px;
  padding: 20px;
  background: var(--navy);
  border-radius: var(--radius);
}

.skypanel[hidden] {
  display: none;
}

.skypanel__frame {
  aspect-ratio: 1 / 1;
  max-width: 340px;
  margin-inline: auto;
}

.sky {
  display: block;
  width: 100%;
  height: auto;
}

.skypanel__caption {
  max-width: var(--measure);
  margin: 16px auto 0;
  font: 400 15px/1.5 var(--body);
  color: var(--on-navy-soft);
  text-align: center;
}

.skypanel__caption:empty {
  /* Holds its line before the caption is generated. */
  min-height: 1.5em;
}

.sky__ring,
.sky__tick {
  fill: none;
  stroke: var(--wheel-line);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.sky__ring--major {
  stroke-width: 1.5;
}

.sky__sign {
  color: var(--on-navy-soft);
}

.sky__glyph {
  color: var(--gold);
}

/* The unlit side, painted so the sphere keeps its shape. Left open it takes
   the navy of the band and a gibbous reads as a full disc. */
.sky__moon-dark {
  fill: var(--navy-deep);
  stroke: none;
}

.sky__moon-disc {
  fill: none;
  stroke: var(--wheel-line);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* The lit limb. paper on navy is 10.71:1, so a full moon reads as one. */
.sky__moon-lit {
  fill: var(--paper);
  stroke: none;
}

/* --------------------------------------------------------------------------
   Image slots

   Empty but defined. Each one states its ratio and the pixel width to supply,
   so a file can be dropped in without touching the layout. aspect-ratio does
   the reserving, which means no shift when the real image arrives.
   -------------------------------------------------------------------------- */

.imageslot {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 16px;
  background: var(--paper-warm);
  border: 1px dashed var(--rule);
  border-radius: var(--radius);
  text-align: center;
}

.band--warm .imageslot,
.band--white .imageslot {
  background: var(--paper);
}

.imageslot--hero {
  aspect-ratio: 3 / 2;
  margin-top: 32px;
}

.imageslot--square {
  aspect-ratio: 1 / 1;
}

.imageslot--page {
  aspect-ratio: 3 / 4;
  padding: 12px;
}

.imageslot__ratio {
  font: 600 15px/1 var(--data);
  color: var(--ink);
}

.imageslot__spec {
  font: 400 13px/1 var(--data);
  color: var(--ink-soft);
}

.imageslot__note {
  max-width: 28ch;
  font: 400 13px/1.45 var(--body);
  color: var(--ink-soft);
}

/**
 * A filled slot, which is a different object from an empty one: no dashed
 * edge, no centred label, and no border, because DESIGN.md asks imagery to
 * run full bleed with no framing.
 *
 * aspect-ratio does the same job the placeholder was doing. The image is
 * lazy, so without the square reserved the reading would reflow under the
 * reader at the moment the file lands.
 */
.readingfig {
  margin: 0;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: var(--radius);
}

.readingfig img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --------------------------------------------------------------------------
   Card and form
   -------------------------------------------------------------------------- */

.card {
  padding: 24px var(--gutter);
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

/* Why this is worth paying for, said once, above the fields. Quieter than the
   heading and than the labels: it is the reason someone keeps reading, not an
   instruction, and a reader who has already decided should be able to skip it
   on their way to the first field. */
.card__why {
  margin: 12px 0 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--rule);
  font: 400 14px/1.55 var(--body);
  color: var(--ink-soft);
}

/* Every card gets a real edge and one accent stripe from its band. */
.card,
.offer,
.imageslot--page {
  border-color: var(--rule);
}

@media (min-width: 900px) {
  .card {
    position: sticky;
    top: 24px;
    padding: 32px;
  }
}

.field {
  display: grid;
  gap: 8px;
  margin-top: 24px;
}

label {
  font: 500 14px/1.4 var(--body);
  color: var(--ink);
}

/* A field sits on the card, so it takes the card's ground, not the page's
   old dark one. --paper rather than --card so the input reads as a well in
   the white card rather than as a borderless gap. Text is 15.25:1 on it and
   the placeholder 6.88:1. */
input[type="date"],
input[type="time"],
input[type="text"] {
  width: 100%;
  min-height: 48px;
  padding: 10px 12px;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  color: var(--ink);
  font: 400 17px/1.4 var(--body);
}

input[type="date"]:focus,
input[type="time"]:focus,
input[type="text"]:focus {
  background: var(--card);
}

/* Date and time are real values, so they take the mono face. */
input[type="date"],
input[type="time"] {
  font-family: var(--data);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}

input::placeholder {
  color: var(--ink-soft);
}

input[aria-invalid="true"] {
  border-color: var(--rust);
}

.field__hint {
  font: 400 14px/1.45 var(--body);
  color: var(--ink-soft);
}

.field__error {
  font: 400 14px/1.45 var(--body);
  color: var(--rust);
}

.field__error[hidden] {
  display: none;
}

/* The birth time is the one field a visitor may genuinely not have, and the
   answer to that is a stated choice rather than a silent default. The box sits
   under the field it answers, and the copy under the box says what the choice
   costs before it is made, not after. */
.notime {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px 10px;
  margin-top: 14px;
}

/* 20px is the box, and the label is its target as well, so the pair clears
   the 44px rule between them without a tap target drawn around nothing. */
.notime input[type="checkbox"] {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--navy);
  justify-self: start;
  align-self: center;
}

.notime label {
  align-self: center;
  padding: 12px 0;
  font: 400 16px/1.3 var(--body);
  cursor: pointer;
}

/* Indented under the label, not under the box: it explains the sentence the
   visitor just read, and hanging it off the checkbox would read as a caption
   for the control. */
.notime__copy {
  grid-column: 2;
  margin: 0;
  font: 400 14px/1.45 var(--body);
  color: var(--ink-soft);
}

/* Checked, the field it replaces stops accepting input rather than sitting
   there looking usable while being ignored. The JS does the disabling; the
   field sits above the box, so no sibling selector can reach it from here. */
input[type="time"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Used by the missing birth time path to offer a way forward. */
.linkbutton {
  padding: 0;
  background: none;
  border: 0;
  color: var(--ink);
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.button {
  width: 100%;
  min-height: 52px;
  margin-top: 32px;
  padding: 14px 20px;
  background: var(--navy);
  border: 1px solid var(--navy);
  border-radius: var(--radius);
  color: var(--on-navy);
  font: 600 17px/1 var(--body);
  cursor: pointer;
}

.button:disabled {
  background: transparent;
  border-color: var(--rule);
  color: var(--ink-soft);
  cursor: default;
}

.form__status {
  font: 400 14px/1.45 var(--body);
  color: var(--ink-soft);
}

.form__status:not(:empty) {
  margin-top: 16px;
}

/* --------------------------------------------------------------------------
   City combobox
   -------------------------------------------------------------------------- */

.combobox {
  position: relative;
}

/* The suggestions sit above the card, so they are the card's own white with
   a real edge, not the old dark surface. */
.combobox__list {
  position: absolute;
  z-index: 10;
  top: calc(100% + 4px);
  right: 0;
  left: 0;
  max-height: 264px;
  overflow-y: auto;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

.combobox__list[hidden] {
  display: none;
}

.combobox__option {
  padding: 10px 12px;
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
}

.combobox__option:last-child {
  border-bottom: 0;
}

/* The highlighted row was white, which on a white list is no highlight at
   all. It takes the warm tint instead. */
.combobox__option[aria-selected="true"] {
  background: var(--paper-warm);
}

.combobox__name {
  font: 400 16px/1.3 var(--body);
  color: var(--ink);
}

.combobox__where {
  display: block;
  font: 400 14px/1.4 var(--body);
  color: var(--ink-soft);
}

.combobox__empty {
  padding: 12px;
  font: 400 14px/1.45 var(--body);
  color: var(--ink-soft);
}

/* The coordinates we resolved are shown back rather than hidden. */
.city-readout {
  font: 400 15px/1.5 var(--data);
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

.city-readout:empty {
  display: none;
}

/* --------------------------------------------------------------------------
   The chart wheel

   No well. A wheel is round, so a square panel behind it only draws a corner
   the drawing does not have; it sits directly on --night. What is left is a
   centring box, and the wheel grows to fill the width it is given up to 420px
   — the size that renders a 13-unit glyph at exactly 16px.
   -------------------------------------------------------------------------- */

/* The band carries the navy now, so the well is only a centring box. */
.wheel-well {
  display: flex;
  justify-content: center;
  margin: 0;
}

.wheel-well:empty {
  display: none;
}

.wheel {
  display: block;
  width: 100%;
  max-width: 420px;
  height: auto;
}

/* The glyph is 3.5% of the wheel, so an 11px glyph needs a 315px wheel. On
   the narrowest phones the wheel takes the gutter back rather than shrink
   under that. */
@media (max-width: 420px) {
  .wheel-well {
    margin-inline: calc(var(--gutter) * -0.5);
  }
}

/* Weight is the hierarchy. Four steps, heaviest first: the circles that
   bound the drawing, the four angles, the twelve of everything, the chords. */
.wheel__ring,
.wheel__tick,
.wheel__spoke {
  fill: none;
  stroke: var(--wheel-line);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* The outer ring and the hub bound the drawing. */
.wheel__ring--major {
  stroke-width: 1.5;
}

/* The band circle only separates two zones, so it stays a hairline. */
.wheel__ring--band {
  stroke-width: 1;
}

/* The ascendant/descendant and meridian axes have to read through the other
   ten spokes, or the chart has no orientation. */
.wheel__spoke--axis {
  stroke-width: 1.5;
}

/* Chords across the middle, coloured by kind. Sage for the harmonious ones,
   rust for the hard ones: a monochrome aspect field is a tangle, two colours
   make it readable. Composited on navy at this opacity they are 3.45:1 and
   3.33:1, so soft does not mean lost. */
/* Full opacity, and the softness comes from the colour instead. A 1px chord
   at 0.65 alpha measured 3.45:1 and was invisible on the render: a hairline
   gives most of its contrast away to anti-aliasing, so alpha on top of that
   leaves nothing. These composite to 4.28:1 and 4.09:1. */
.wheel__aspect {
  fill: none;
  stroke-width: 1.25;
  vector-effect: non-scaling-stroke;
}

.wheel__aspect--soft {
  stroke: var(--sage-on-navy);
}

.wheel__aspect--hard {
  stroke: var(--rust-on-navy);
}

/* Glyphs take their colour through currentColor, which is why the sprite
   carries no colour of its own. Brass marks the visitor's own placements; the
   zodiac and the house numbers are the frame those placements sit in, so they
   are bone-dim and never compete. */
.wheel__glyph {
  color: var(--gold);
}

/* --rust is 1.90:1 on navy, which is no mark at all. The lightened variant
   the aspect chords use is 4.09:1 there. */
.wheel__retrograde {
  color: var(--rust-on-navy);
}

.wheel__sign {
  color: var(--on-navy-soft);
}

.wheel__house {
  fill: var(--on-navy-soft);
  font-family: var(--data);
  font-variant-numeric: tabular-nums;
  stroke: none;
}

/* Invisible, and the only thing on the wheel that takes a pointer: a 13px
   glyph is not a thumb target. Not focusable — the wheel is aria-hidden and
   the table carries the same figures. */
.wheel__hit {
  fill: transparent;
  stroke: none;
  cursor: pointer;
}

/* The draw sequence is deliberately not here yet. The drawing has to be
   right before it is worth choreographing, and a timeline written against the
   old three-group wheel would only have to be rewritten. See docs/BACKLOG.md.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   Placement sheet. Opened by tapping a glyph. It does not slide in: the wheel
   draw is the only motion in the product.
   -------------------------------------------------------------------------- */

.sheet {
  position: fixed;
  z-index: 50;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.sheet[hidden] {
  display: none;
}

.sheet__scrim {
  position: absolute;
  inset: 0;
  background: var(--scrim);
}

.sheet__panel {
  position: relative;
  width: 100%;
  max-width: 520px;
  max-height: 80vh;
  overflow-y: auto;
  padding: 20px var(--gutter) 28px;
  background: var(--card);
  border-top: 1px solid var(--rule);
  border-radius: 4px 4px 0 0;
}

@media (min-width: 900px) {
  .sheet {
    align-items: center;
  }

  .sheet__panel {
    border: 1px solid var(--rule);
    border-radius: var(--radius);
  }
}

.sheet__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

.sheet__body-name {
  font: 500 22px/1.2 var(--display);
  letter-spacing: -0.01em;
}

.sheet__close {
  flex: none;
  padding: 0;
  background: none;
  border: 0;
  color: var(--ink-soft);
  font: 400 14px/1.45 var(--body);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.sheet__facts {
  margin-top: 20px;
  border-top: 1px solid var(--rule);
}

.sheet__facts > div {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 24px;
  padding: 10px 0;
  border-bottom: 1px solid var(--rule);
}

.sheet__facts dt {
  font: 400 14px/1.5 var(--body);
  color: var(--ink-soft);
}

.sheet__facts dd {
  font: 400 15px/1.5 var(--data);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* Marked as unwritten on purpose. Nothing here is generated copy. */
.sheet__reading {
  margin-top: 20px;
  padding-left: 12px;
  border-left: 1px solid var(--rule);
  font: 400 14px/1.5 var(--body);
  color: var(--ink-soft);
}

/* --------------------------------------------------------------------------
   Result
   -------------------------------------------------------------------------- */

.result {
  margin-top: var(--section);
}

.result[hidden] {
  display: none;
}

.notice {
  max-width: var(--measure);
  margin-bottom: 32px;
  padding: 16px;
  border: 1px solid var(--rule);
  border-left: 2px solid var(--rust);
  border-radius: var(--radius);
  font: 400 14px/1.5 var(--body);
  color: var(--ink-soft);
}

.notice[hidden] {
  display: none;
}

/* The moment. Sun, moon and rising, in brass, large. */
.bigthree {
  border-top: 1px solid var(--rule);
}

.bigthree li {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 8px 16px;
  padding: 20px 0;
  border-bottom: 1px solid var(--rule);
}

.bigthree__label {
  grid-column: 1 / -1;
  font: 400 14px/1 var(--body);
  color: var(--ink-soft);
}

/* gold-ink, not gold: this sits on a light band, where --gold is 2.59:1. */
.bigthree__sign {
  font: 600 34px/1.08 var(--display);
  letter-spacing: -0.02em;
  color: var(--gold-ink);
}

.bigthree__degree {
  font: 400 15px/1.5 var(--data);
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
  text-align: right;
  /* A range may never break inside a figure: rasterised at 320 it folded as
     "– 24°" above "30′". */
  white-space: nowrap;
}

/* And kept whole it does not fit beside a 34px sign either -- the closing ′
   was cut off the card's right edge at 320. Below 900px the card is a two
   column grid, so a span takes a row of its own under the sign, where it has
   the whole card to lie on. Above it, the card is already stacked. */
.bigthree__degree.is-span {
  grid-column: 1 / -1;
  text-align: left;
}

/* Sits under the Moon when noon was assumed. Small: it qualifies the figure
   above it, it does not compete with it. */
.bigthree__caveat {
  grid-column: 1 / -1;
  margin-top: 8px;
  font: 400 14px/1.45 var(--body);
  color: var(--ink-soft);
}

/* Where the rising sign would be if we had the time. No brass: there is no
   visitor data in this card, which is the whole point of it. */
.bigthree__prompt {
  display: block;
}

.bigthree__ask {
  margin: 8px 0 12px;
  font: 400 15px/1.5 var(--body);
  color: var(--ink-soft);
}

@media (min-width: 900px) {
  .bigthree {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    border-bottom: 1px solid var(--rule);
  }

  .bigthree li {
    display: block;
    padding: 24px 24px 24px 0;
    border-right: 1px solid var(--rule);
    border-bottom: 0;
  }

  .bigthree li:last-child {
    border-right: 0;
  }

  .bigthree__sign {
    display: block;
    margin-top: 8px;
    font-size: 52px;
  }

  .bigthree__degree {
    display: block;
    margin-top: 8px;
    font-size: 16px;
    text-align: left;
  }
}

/* --------------------------------------------------------------------------
   The reading

   The free sample, and the most important text on the page: it is what the
   visitor is being asked to buy more of. Set at the widest comfortable
   measure and given room, because it is the one thing here meant to be read
   rather than scanned.
   -------------------------------------------------------------------------- */

.reading[hidden] {
  display: none;
}

.reading__inner {
  display: grid;
  gap: 40px;
}

@media (min-width: 900px) {
  .reading__inner {
    grid-template-columns: minmax(0, 1fr) 320px;
    gap: 64px;
    align-items: start;
  }

  .reading__inner .readingfig {
    position: sticky;
    top: 40px;
  }
}

/* The one headline on the result page that speaks to the visitor rather than
   naming a section. It is the handover from the three signs to the writing. */
.reading__heading {
  max-width: var(--measure);
  margin-bottom: 28px;
  font: 500 22px/1.2 var(--display);
  letter-spacing: -0.01em;
  color: var(--ink);
  text-wrap: balance;
}

.reading__body {
  max-width: var(--measure);
}

/* Sun, then moon, then rising, each divided from the last by one hairline.
   The source line inside a brick is set apart by its own voice rather than by
   a second rule, so a boundary reads as a boundary. */
.reading__brick {
  display: grid;
  gap: 20px;
}

.reading__brick + .reading__brick {
  margin-top: 36px;
  padding-top: 36px;
  border-top: 1px solid var(--rule);
}

.reading__label {
  font: 400 14px/1 var(--body);
  color: var(--ink-soft);
}

.reading__paragraph {
  font: 400 17px/1.65 var(--body);
  color: var(--ink);
}

/* The source line. Where the jargon is allowed to live, and set quieter than
   the writing it explains. */
.reading__source {
  font: 400 14px/1.6 var(--body);
  font-style: italic;
  color: var(--ink-soft);
}

@media (min-width: 900px) {
  .reading {
    margin-top: 56px;
  }

  .reading__heading {
    margin-bottom: 36px;
    font-size: 28px;
  }

  .reading__brick + .reading__brick {
    margin-top: 48px;
    padding-top: 48px;
  }

  .reading__paragraph {
    font-size: 19px;
  }

  .reading__source {
    font-size: 15px;
  }
}

/* --------------------------------------------------------------------------
   The report offer. A card in the flow, never a modal.
   -------------------------------------------------------------------------- */

.offer {
  padding: 28px var(--gutter);
  background: var(--card);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--accent, var(--rule));
  border-radius: var(--radius);
}

@media (min-width: 900px) {
  .offer {
    padding: 40px;
  }
}

.offer__heading {
  margin-top: 10px;
}

.offer__note {
  max-width: var(--measure);
  margin-top: 12px;
  font: 400 15px/1.55 var(--body);
  color: var(--ink-soft);
}

/* The birth time, answered before the money rather than after. It sits on
   the offer card because that is where the question gets asked, and a buyer
   who finds out in the PDF that two readings are missing asks for a refund
   and is right to. Set apart from the note above it, not shouted. */
.offer__time {
  max-width: var(--measure);
  margin-top: 16px;
  padding: 14px 16px;
  border-left: 3px solid var(--rust);
  background: var(--paper-warm);
  border-radius: 3px;
  font: 400 15px/1.6 var(--body);
  color: var(--ink);
}

.offer__time b {
  font-weight: 600;
}

/* Three real pages, side by side. Sharp, never blurred: a blurred sample
   reads as hiding something. */
/* minmax(0, 1fr) rather than 1fr. A bare 1fr is minmax(auto, 1fr), and auto
   floors each track at its own min-content: three tiles whose labels will not
   wrap below a certain width simply refuse to shrink, and the page scrolls
   sideways. Measured before this: 371px of content in a 238px box at 320,
   which pushed the whole document 92px wide.
   The floor of zero is right for the real images too, which are 3:4 and scale
   to whatever track they are given. */
.offer__pages {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  margin-top: 28px;
}

@media (min-width: 900px) {
  .offer__pages {
    gap: 20px;
    max-width: 620px;
  }
}

/* Marked as unbuilt on its face, so it cannot be mistaken for finished. */
.offer__price {
  margin-top: 10px;
  font: 400 17px/1.5 var(--body);
}

.offer__contents-note {
  margin-top: 22px;
  margin-bottom: 4px;
  font: 600 15px/1.4 var(--body);
}

/* Says what the three thumbnails are, because at 320px they are 93px wide and
   carry texture rather than content. */
.offer__caption {
  margin-top: 10px;
  font: 400 14px/1.45 var(--body);
  color: var(--ink-soft);
}

/* Five lines, each a sentence rather than a title. One column at every width:
   these wrap, and a wrapped sentence in a second column reads as two lists. */
.offer__contents {
  margin: 0 0 8px;
  padding: 0;
  list-style: none;
  font: 400 15px/1.5 var(--body);
  color: var(--ink-soft);
}

.offer__contents li {
  padding-left: 16px;
  text-indent: -16px;
}

.offer__contents li + li {
  margin-top: 8px;
}

/* A mark that says "one of a list" without a bullet's roundness. */
.offer__contents li::before {
  content: "— ";
  color: var(--rule);
}

.offer__pages img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
  border-radius: 2px;
}

.offer__refund {
  margin-top: 20px;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
  font: 400 15px/1.55 var(--body);
  color: var(--ink-soft);
}

/* --------------------------------------------------------------------------
   Drawers. The chart data, for the few who read charts. Closed by default:
   it is the evidence, not the product. <details> so it works without JS.
   -------------------------------------------------------------------------- */

.drawer {
  margin-top: 24px;
  border-top: 1px solid var(--rule);
}

.drawer:last-of-type {
  border-bottom: 1px solid var(--rule);
}

.drawer > summary {
  padding: 20px 0;
  font: 600 17px/1.3 var(--body);
  color: var(--ink);
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

/* Safari draws its own triangle unless this is turned off too. */
.drawer > summary::-webkit-details-marker {
  display: none;
}

/* A plus that becomes a minus. Mono, so it sits on the data side of the
   type system rather than reading as an icon. */
.drawer > summary::after {
  content: "+";
  font: 400 18px/1 var(--data);
  color: var(--ink-soft);
}

.drawer[open] > summary::after {
  content: "\2212";
}

.drawer__body {
  padding-bottom: 32px;
}

@media (min-width: 900px) {
  .drawer > summary {
    font-size: 19px;
  }
}

/* --------------------------------------------------------------------------
   Placement table. A first-class part of the page, not a fallback.
   -------------------------------------------------------------------------- */

/* The credibility lines, now that they live in the drawer instead of the
   hero. Three facts for the few who want them. */
.method {
  display: grid;
  gap: 8px;
  margin-top: 28px;
  padding-top: 20px;
  border-top: 1px solid var(--rule);
}

.method li {
  max-width: var(--measure);
  font: 400 15px/1.5 var(--body);
  color: var(--ink-soft);
}

.panel__note {
  max-width: var(--measure);
  font: 400 14px/1.5 var(--body);
  color: var(--ink-soft);
}

.table-scroll {
  margin-top: 20px;
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

thead th {
  padding: 0 12px 8px 0;
  border-bottom: 1px solid var(--rule);
  font: 400 14px/1.4 var(--body);
  color: var(--ink-soft);
  white-space: nowrap;
}

tbody td {
  padding: 12px 12px 12px 0;
  border-bottom: 1px solid var(--rule);
  vertical-align: baseline;
  white-space: nowrap;
}

tbody tr:last-child td {
  border-bottom: 0;
}

.cell-body,
.cell-sign {
  font: 400 16px/1.4 var(--body);
  color: var(--ink);
  /* "Taurus → Gemini" is the widest thing this column ever holds and tbody td
     makes it unbreakable, which is what pushed the untimed table past a 320px
     drawer. A sign name is one word, so the only place this can break is at
     the arrow, which is exactly where it should. */
  white-space: normal;
}

.cell-degree {
  font: 400 15px/1.4 var(--data);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  /* tbody td is nowrap, which is right for a single figure and wrong for a
     span: at 320px the untimed table runs 105px past the drawer and the second
     half of every range sits off the edge. Taller is free here, wider is not. */
  white-space: normal;
}

/* The two ends of a span. The line may break at the dash between them and
   nowhere else -- a break inside a figure gives "– 24°" above "30′". */
.degree-end {
  white-space: nowrap;
}

.cell-house {
  font: 400 15px/1.4 var(--data);
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

/* Retrograde is the one thing ember is allowed to mark. */
.retrograde {
  font: 500 15px/1.4 var(--data);
  color: var(--rust);
  text-decoration: none;
}

.group th {
  padding: 32px 12px 8px 0;
  border-bottom: 1px solid var(--rule);
  font: 400 14px/1.4 var(--body);
  font-weight: 400;
  color: var(--ink-soft);
}

@media (min-width: 900px) {
  .cell-body,
  .cell-sign {
    font-size: 17px;
  }

  .cell-degree,
  .cell-house {
    font-size: 16px;
  }
}

/* --------------------------------------------------------------------------
   Calculation readout. The mono role doing the job it is there for.
   -------------------------------------------------------------------------- */

.readout {
  margin-top: 20px;
  border-top: 1px solid var(--rule);
}

.readout > div {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 24px;
  padding: 12px 0;
  border-bottom: 1px solid var(--rule);
}

.readout dt {
  font: 400 14px/1.5 var(--body);
  color: var(--ink-soft);
}

.readout dd {
  font: 400 15px/1.5 var(--data);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

@media (min-width: 900px) {
  .readout dd {
    font-size: 16px;
  }
}

/* --------------------------------------------------------------------------
   Colophon
   -------------------------------------------------------------------------- */

.colophon {
  background: var(--navy-deep);
}

.colophon p {
  max-width: var(--measure);
  font: 400 15px/1.6 var(--body);
  color: var(--on-navy-soft);
}

.colophon a {
  color: var(--on-navy);
  text-decoration-color: var(--wheel-line);
}

.colophon p + p {
  margin-top: 8px;
}

/* --------------------------------------------------------------------------
   Legal and about pages

   One column of running prose, which is the one thing the rest of the site
   never is. The reset zeroes every margin and takes the bullets off lists,
   and that is right for the composed pages and wrong here, so the rhythm and
   the bullets come back inside .prose and nowhere else.

   The column is 52ch, not --measure. A ch is the width of a zero, which is
   wider than the average lowercase letter, so --measure at 68ch renders 96
   characters of real prose here, counted line by line off the render. 52ch is
   the widest value at which no line on any of the four documents passes 75;
   the worst measures 74. Narrower than the rest of the site on purpose,
   because the rest of the site is not a page of running text.
   -------------------------------------------------------------------------- */

.prose {
  max-width: 52ch;
}

.prose > * + * {
  margin-top: 20px;
}

.prose h2 {
  margin-top: 48px;
}

.prose h3 {
  margin-top: 32px;
}

.prose ul {
  display: grid;
  gap: 8px;
  padding-left: 22px;
  list-style: disc;
}

.prose li {
  max-width: var(--measure);
}

/* The date the document was last changed. It is not body copy: it is the
   stamp that tells a reader which version they are holding, so it takes the
   same quiet treatment as .method and .panel__note elsewhere. */
.prose__updated {
  margin-top: 48px;
  padding-top: 20px;
  border-top: 1px solid var(--rule);
  font: 400 15px/1.6 var(--body);
  color: var(--ink-soft);
}

/* The five document links, which are now the whole footer: the GeoNames
   attribution moved onto the credits page, where the licence is stated in
   full. A nav rather than a sentence, because that is what it is, and it has
   to wrap on a phone. No rule above it any more, since there is nothing above
   it to be separated from. */
.colophon__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
}

.colophon__nav a {
  font: 400 15px/1.6 var(--body);
}

/* --------------------------------------------------------------------------
   The buyer's own contents. Titles only, never a line of the writing.
   -------------------------------------------------------------------------- */

.offer__own {
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--rule);
}

.offer__own-count {
  font: 600 15px/1.4 var(--body);
}

/* The notable case, alone and first: the one line a stranger can read without
   knowing any astrology. Given the accent the list does not get. */
.offer__own-featured {
  margin-top: 10px;
  padding: 10px 12px;
  background: var(--paper-warm);
  border-left: 3px solid var(--accent, var(--rule));
  font: 400 16px/1.4 var(--body);
}

.offer__own-list {
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
  font: 400 14px/1.5 var(--data);
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

.offer__own-list li + li {
  margin-top: 3px;
}

/* The drawer. The count stays outside it: the promise must not depend on
   anyone opening anything. */
.offer__own-more {
  margin-top: 12px;
}

.offer__own-more summary {
  padding: 8px 0;
  font: 400 14px/1.4 var(--body);
  color: var(--ink-soft);
  cursor: pointer;
}

.offer__own-more summary:hover {
  color: var(--ink);
}

.offer__own-more[open] summary {
  margin-bottom: 4px;
}

/* --------------------------------------------------------------------------
   The sky. The first thing in the result, and the only image on the page that
   could not have been made for anybody else.
   -------------------------------------------------------------------------- */

/* One screen, near enough: the visitor sees the whole sky without scrolling
   and reaches the reading in one gesture. The band's own vertical rhythm is
   cut here because the disc is the content and padding around it is dead
   space on a phone. */
.skyband {
  padding-block: 24px;
}

/* The invitation that stands in the sky's place when no birth time was given.
   Same ground, same width, same centre line: the slot reads as the sky's
   whether or not there is a sky in it. It is never shown next to the sky --
   one of the two is always wrong -- so it can borrow the band's rhythm
   without competing with it. */
.skyprompt {
  padding-block: 48px;
  text-align: center;
}

.skyprompt__title {
  max-width: 20ch;
  margin: 0 auto 12px;
  color: var(--on-navy);
  font: 600 26px/1.2 var(--display);
  text-wrap: balance;
}

.skyprompt__copy {
  max-width: 38ch;
  margin: 0 auto 18px;
  color: var(--on-navy-soft);
  font: 400 16px/1.55 var(--body);
  text-wrap: pretty;
}

.skyprompt__ask {
  color: var(--on-navy);
}

/* --------------------------------------------------------------------------
   The reveal. The site's only animation, and DESIGN.md carries the exception.

   Three seconds: the horizon alone, then the few you could name, then
   hundreds, then thousands. The horizon and the four names are never animated
   -- they are the frame the rest arrives into, and they are there on the first
   frame.

   THE RULE THIS OBEYS

   "Any animation must leave no trace at rest." Every star carries its own
   opacity, worked out from its magnitude, and that weighting is what makes
   Sirius read as Sirius. These keyframes fade the GROUP, so a group left at
   anything but opacity 1 would scale every star inside it and quietly undo
   the field. animation-fill-mode is `both`: 0 before the delay, and 1 at the
   end, held. test/sky-reveal.test.js checks the resting value four seconds in
   rather than while it runs.
   -------------------------------------------------------------------------- */

@keyframes sky-arrive {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The resting state, and the only state a reader who asked for stillness
   sees. Declared outside the media query so it is what applies by default. */
.skyband__tier,
.skyband__halos,
.skyband__bodies {
  opacity: 1;
}

@media (prefers-reduced-motion: no-preference) {
  .skyband__tier,
  .skyband__halos,
  .skyband__bodies {
    animation: sky-arrive var(--sky-arrive-dur, 0.55s) ease-out
      var(--sky-arrive-delay, 0.35s) both;
  }

  /* The Moon and the planets are among the brightest things up there, so they
     arrive with the stars you could name rather than after everything. */
  .skyband__tier--1,
  .skyband__halos,
  .skyband__bodies {
    --sky-arrive-delay: 0.35s;
    --sky-arrive-dur: 0.55s;
  }

  .skyband__tier--2 {
    --sky-arrive-delay: 0.95s;
    --sky-arrive-dur: 0.75s;
  }

  .skyband__tier--3 {
    --sky-arrive-delay: 1.7s;
    --sky-arrive-dur: 1.25s;
  }
}

.skyband__line {
  max-width: 34ch;
  margin: 0 auto 14px;
  color: var(--on-navy);
  font: 400 17px/1.45 var(--body);
  text-align: center;
  text-wrap: balance;
}

.skyband__frame {
  display: flex;
  justify-content: center;
  margin: 0;
}

/* The size comes from the script, in a custom property, because the drawing
   is built at that size: the viewBox, the star radii and the pixels have to be
   the same number. A width of 100% here would let CSS stretch a 320-unit
   viewBox across 520px and every star would come out 1.6 times too big.

   max-width is the belt: if the property is ever missing the disc still cannot
   overflow its column. */
.skyband__disc {
  display: block;
  width: var(--sky-size, 100%);
  height: auto;
  max-width: 100%;
}

/* The cardinal names sit on the rim in the same gold as the horizon, small
   enough to read as engraving rather than as a label. */
/* The anchor's name. The same gold as the horizon, because it belongs to the
   frame the sky is read against rather than to the sky itself. */
/* No font-size here on purpose. It is carried as a presentation attribute so
   the navigator can shrink it as the sky is zoomed into, and a CSS rule would
   win over that attribute and pin the label at 12 user units -- which is 12px
   at rest and 72px at six times in, which is what it did. */
.skyband__anchor-name {
  /* Same: the anchor's label sits over its own ring. */
  pointer-events: none;
  font-family: var(--body);
  font-weight: 400;
  letter-spacing: 0.04em;
}

/* Same: the size lives on the element, not here, so it can be held still
   while the sky scales under it. */
.skyband__cardinal {
  pointer-events: none;
  fill: #c8892a;
  font-family: var(--display);
  font-weight: 400;
  letter-spacing: 0.04em;
}

/* The way back. Hidden until the sky has been moved, and 44px like every
   other target on this page, because the thumb that lost the sky is the same
   thumb that has to find it again. */
.skyband__reset {
  display: block;
  min-height: 44px;
  margin: 10px auto 0;
  padding: 10px 18px;
  border: 1px solid rgba(200, 137, 42, 0.5);
  border-radius: var(--radius);
  background: none;
  color: var(--on-navy);
  font: 400 14px/1.4 var(--body);
  cursor: pointer;
}

.skyband__reset[hidden] {
  display: none;
}

/* The names, which only exist while somebody is far enough in to read them.
   Sized in user units by the navigator so they hold still on screen while the
   view scales underneath. */
.skyband__name {
  /* A NAME IS NOT A TARGET, AND MUST NOT TAKE ITS TAP.

     The labels are drawn after the targets, so they sit on top of them and
     were catching the clicks meant for the star underneath -- which the sky
     then read as a tap on empty space and answered by putting the sentence
     away. Tapping a named star did nothing, or worse, undid the last thing. */
  pointer-events: none;
  fill: #cdd8ea;
  font-family: var(--body);
  paint-order: stroke;
  stroke: rgba(18, 38, 74, 0.85);
  stroke-width: 3px;
  stroke-linejoin: round;
}

/* The way in. At rest, under the disc: the count, and this. Nothing on the
   disc itself is a gesture to guess. */
.skyband__open {
  display: block;
  min-height: 44px;
  margin: 14px auto 0;
  padding: 11px 22px;
  border: 1px solid rgba(200, 137, 42, 0.55);
  border-radius: var(--radius);
  background: rgba(230, 181, 102, 0.08);
  color: var(--on-navy);
  font: 400 15px/1.4 var(--body);
  cursor: pointer;
}

.skyband__open:focus-visible {
  outline: 2px solid #e6b566;
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   The sky, full screen.

   A CSS overlay, not requestFullscreen: the Fullscreen API is documented as
   unavailable on non-video elements in Safari on iOS, which is a large share
   of this site's traffic. scripts/fullscreen-probe.mjs could not confirm that
   from a desktop -- WebKit allows it here -- so the overlay is chosen because
   it removes the question rather than because the engine forced it.
   -------------------------------------------------------------------------- */

/* THE SKY GETS THE WHOLE SCREEN, AND KEEPS IT.

   This was two rows, the sky above and the sentence below. But the sentence
   is a different length every time -- measured over every sentence the sky
   can say, it runs to two lines at 1440 and FIVE at 320 -- so the sky's row
   changed height whenever a reader touched something. The picture is scaled
   to cover that row, so it rescaled and slid: a mark measured at 346,171 was
   at 339,163 the instant its own sentence arrived, out from under the finger
   that had just touched it.

   One row now. The sky is covered once, when it opens, and the band floats
   over the bottom of it. What the band hides can be dragged out from under
   it; what moves under a finger cannot be got back. */
.skyfull {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  grid-template-rows: 1fr;
  background: var(--navy-deep);
}

.skyfull[hidden] {
  display: none;
}

/* The page underneath is pinned while the sky is up.

   overflow:hidden alone is not enough: the sky is lifted OUT of its frame to
   go full screen, so the page loses that height and reflows under a scroll
   offset that no longer means the same thing. Measured, it moved from 700 to
   2167 on opening. Fixed at minus its own offset, nothing can move. */
body.is-skybound {
  position: fixed;
  width: 100%;
  overflow: hidden;
}

.skyfull__stage {
  position: relative;
  min-height: 0;
  overflow: hidden;
  /* A drag across the sky was selecting the writing on it: the rasterised
     journey shows the anchor's name highlighted blue in the frame right after
     the pull that went looking for it. Moving the sky is not reading it. */
  user-select: none;
  -webkit-user-select: none;
}

/* The sky fills the screen. It is not a picture with margins around it: the
   circle is scaled to COVER, its edges run off the sides, and the horizon is
   something you go and find. preserveAspectRatio="slice" on the element does
   the scaling; this only hands it the whole screen to do it in. */
.skyfull__stage .skyband__disc {
  width: 100%;
  height: 100%;
  max-width: none;
}

/* The sentence, inside the sky rather than under it. Anchored to the bottom
   of the overlay so it is in the same glance as the thing it describes. */
/* Read at arm's length, on a phone, outdoors. It carries two lines when the
   sentence needs two, and it leaves room on the right for the zoom buttons
   rather than running under them. */
.skyfull__caption {
  margin: 0;
  padding: 20px 20px calc(20px + env(safe-area-inset-bottom));
  border-top: 1px solid rgba(200, 137, 42, 0.45);
  background: rgba(12, 26, 52, 0.97);
  color: #f2ece0;
  font: 400 19px/1.4 var(--body);
  text-align: left;
  text-wrap: pretty;
  /* Over the sky, not under it. It grows upward when a sentence needs three
     lines, and the sky beneath it does not move an inch. Two lines' worth
     even when empty, so the band is one steady shape at the bottom of the
     screen rather than something that flickers in and out. */
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 4;
  min-height: calc(2.8em + 40px);
  box-sizing: border-box;
}

.skyfull__caption[hidden] {
  display: none;
}

/* The second form: beside the thing rather than across the bottom. Both are
   inside the sky; this one is the shorter link and the busier picture. */
.skyfull[data-caption="near"] .skyfull__caption {
  position: absolute;
  z-index: 2;
  padding: 12px 14px;
  border: 1px solid rgba(200, 137, 42, 0.5);
  border-radius: var(--radius);
  background: rgba(18, 38, 74, 0.96);
  font-size: 15px;
  text-align: left;
}

/* Closer and further out, for a machine with no fingers to pinch with. */
/* Where a thumb already is.

   These sat in the top right, which is the far corner of a phone held in one
   hand -- the hardest place on the screen to reach and the place a thumb never
   rests. They are at the bottom now, just above the sentence, and bigger. */
/* At the bottom of the SKY, which puts them just above the sentence rather
   than on top of it -- the first version ran the words under the buttons.
   The band is no longer part of the layout, so its height is handed over as
   --band and the buttons stand on top of it. They move only when a sentence
   changes, which is never while a thumb is on them. */
.skyfull__tools {
  position: absolute;
  right: 14px;
  bottom: calc(var(--band, 94px) + 14px);
  z-index: 3;
  display: flex;
  gap: 12px;
}

.skyfull__tool {
  width: 56px;
  height: 56px;
  border: 1px solid rgba(200, 137, 42, 0.6);
  border-radius: 50%;
  background: rgba(18, 38, 74, 0.86);
  color: var(--on-navy);
  font: 400 26px/1 var(--body);
  cursor: pointer;
}

/* Visible at all times, which is the whole promise of opening something. */
.skyfull__close {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top));
  right: 12px;
  min-width: 44px;
  min-height: 44px;
  padding: 10px 18px;
  border: 1px solid rgba(200, 137, 42, 0.5);
  border-radius: var(--radius);
  background: rgba(18, 38, 74, 0.8);
  color: var(--on-navy);
  font: 400 15px/1.2 var(--body);
  cursor: pointer;
}

/* A thing that can be touched says so, and everything that can is marked the
   same way. A ring that means "tappable" on one object and decoration on
   another teaches nothing. */
.skyband__target {
  /* A FILLED HALO, NOT A RING.

     The scene already draws a gold ring around every body in the chart, and
     the first version of this drew a gold ring too -- so ten things looked
     touchable while four were, and there was no way to tell which. A ring
     means "here is an object"; a soft disc behind it means "this one answers".
     One treatment for every tappable thing, and nothing else wears it. */
  fill: rgba(230, 181, 102, 0.07);
  stroke: none;
  cursor: pointer;
}

.skyband__target.is-chosen {
  fill: rgba(240, 201, 135, 0.34);
  animation: none;
}

/* THE SECOND ANIMATION ON THIS SITE, AND THE REASON IS IN DESIGN.md.

   A slow breath on the things that answer. Not decoration: a 44px disc at 7%
   over a field of four thousand points is nearly invisible, and a visitor who
   cannot see where to touch has a picture rather than a sky. The motion is the
   only thing that separates a target from the stars behind it.

   It stops the moment something is chosen -- a hint that never stops is
   decoration after all -- and prefers-reduced-motion gets the bright end of
   the breath, held still, so the mark is stronger rather than absent. */
@keyframes sky-breathe {
  0%, 100% { fill: rgba(230, 181, 102, 0.07); }
  50%      { fill: rgba(230, 181, 102, 0.22); }
}

@media (prefers-reduced-motion: no-preference) {
  .skyfull:not(.is-answered) .skyband__target {
    animation: sky-breathe 3.2s ease-in-out infinite;
  }
}

@media (prefers-reduced-motion: reduce) {
  .skyfull:not(.is-answered) .skyband__target {
    fill: rgba(230, 181, 102, 0.18);
  }
}

.skyband__note {
  max-width: 40ch;
  margin: 14px auto 0;
  color: var(--on-navy-soft);
  font: 400 13px/1.5 var(--body);
  text-align: center;
}

@media (min-width: 900px) {
  .skyband {
    padding-block: 40px;
  }

  .skyband__line {
    font-size: 19px;
  }

  /* The wider layout's cardinals were sized here, which would override the
     attribute the navigator writes. The scene draws them scaled already. */
}
