/* Astral — shared foundation, v2.
   One place for the things all three surfaces agreed on in v1 but each wrote
   out for itself: the palette, the three faces, the labels, the buttons, the
   focus ring, and the reduced-motion contract. Pages own their layout; this
   file owns the vocabulary.

   The film grain that used to live here was removed on 7 August 2026 at the
   founder's instruction, from all four documents at once. It was a fixed
   full-window noise layer at z-index 60 — over the type, not behind it — and
   the warm field is the wrong ground for it twice over: a global haze on ivory
   is the cream-and-parchment wellness look §7.5 excludes, and the texture the
   grain was standing in for is now carried by real light and real surfaces.
   `docs/VISUAL_LANGUAGE_AND_DESIGN_SYSTEM.md` had already listed "constant
   global grain" among the things that weaken typography. */

/* The one face Astral serves itself: twenty-seven characters, six kilobytes,
   merged from two OFL faces because neither has all of them — the astrological
   block is in Noto Sans Symbols and ☉ U+2609 is in Noto Sans Symbols 2, which
   is a different font rather than a later version. Renamed on build so it can
   never be confused with either; the licence and provenance ship beside it at
   /static/astral-symbols-OFL.txt. Built by tools/build_glyph_font.py.

   `swap` rather than `block`: a sign drawn by the platform for one moment is a
   fairer trade than an empty box, and at six kilobytes the moment is short. The
   `unicode-range` is the exact set the file carries, so this face is never
   consulted for anything else. */
@font-face {
  font-family: "Astral Symbols";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/astral-symbols.woff2") format("woff2");
  unicode-range: U+2192, U+2609-260A, U+263D-2640, U+2642-2653, U+26B7-26B8;
}

/* ── tokens ─────────────────────────────────────────────────────────────── */
:root {
  /* Mineral daylight. Cool, not grey; day, not galaxy. */
  --sky-1: #B8CBD6;
  --sky-2: #D6E0E5;
  --sky-3: #EEF1F1;

  --ink: #0B1014;
  --ink-2: #33424B;
  --ink-3: #59666E;   /* AA at body sizes on the light field */
  --ink-4: #8E9AA1;   /* decoration only, never running text */
  --rule: rgba(11, 16, 20, .15);
  --rule-soft: rgba(11, 16, 20, .075);

  --night: #0A0E12;
  --night-2: #141B22;
  --on-night: #E9EDEE;
  --on-night-2: #97A4AC;
  --rule-night: rgba(233, 237, 238, .16);

  /* The one saturated colour. It marks what has been determined — by the
     chart, by the customer, by the reader — and it starts portraits. */
  --signal: #D6330D;
  --signal-bright: #F2451B;
  --signal-deep: #BE2A08;

  /* 'Noto Serif' sits between Spectral and the system serifs to carry exactly
     one character: ₮, U+20AE, the tugrik sign. Spectral does not have it — its
     Google subsets stop at U+20AB and pick up again at U+20AD — so in serif
     text the price fell out of the face and was drawn by the platform UI font:
     upright and light inside a serif italic, which is where it was caught.
     IBM Plex Sans does have it, so anything set in the sans was always right.

     The face is loaded ₮-only, and Google's `&text=` response pins it to
     `unicode-range: U+20AE`, so it can never displace Spectral for anything
     else. Keep the extra <link> when self-hosting the fonts, or the sign goes
     back to the system. Only 400 roman and 400 italic are served; a heavier
     weight is synthesised, which for one glyph is not worth another request. */
  --serif: 'Spectral', 'Noto Serif', 'Iowan Old Style', 'Palatino', Georgia, serif;
  --sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  /* 'Astral Symbols' first, and the platform stack kept behind it. The site
     draws twenty-seven characters no font service will actually deliver: ask
     Google for Noto Sans Symbols 2 over the astrological block and the returned
     stylesheet's `unicode-range` promises exactly those characters while the
     binary behind it holds three. Nothing errors, `document.fonts.check()`
     answers true — it reports whether a face by that name loaded, not whether
     it can draw a character — and every sign is quietly handed to the operating
     system instead. See tools/build_glyph_font.py.

     It matters more than it used to. As faint text a substituted glyph read as
     a slightly odd rendering; the close's band now cuts the twelve signs out of
     a solid ring, where a missing glyph punches a tofu box. */
  --sym: 'Astral Symbols', 'Noto Sans Symbols 2', 'Apple Symbols', 'Segoe UI Symbol', var(--serif);
  /* The wordmark's face, and nothing else on the site. Loaded subset to the
     six letters of ASTRAL, so it can never be reached for anything a fallback
     would have to match. See `.wordmark`. */
  --display-sc: 'Cormorant SC', var(--serif);

  /* Two label sizes, not five. */
  --label: 500 .7rem/1 var(--sans);
  --label-sm: 500 .64rem/1 var(--sans);
  --track: .13em;

  --gutter: clamp(22px, 5.4vw, 68px);

  /* Three curves, all tuned by hand rather than lifted.
     The page had been running Penner's easeOutCubic — cubic-bezier(.215, .61,
     .355, 1) — which is the most-copied curve on the web and reads as a
     default to anyone who looks at a lot of screens.

     --ease is the interface: off the mark a little quicker, and settling for
     noticeably longer. It ends at .99 rather than 1, so things arrive at rest
     instead of stopping.

     --ease-settle is for things that arrive on the page. Almost linear for the
     first fifth, then decisive, then a long quiet tail — weight coming to
     rest rather than a card being thrown in and braked.

     --ease-fade is for opacity alone. --ease reaches 68% of its output in the
     first fifth of its duration, which is right for a control responding to a
     press and wrong for something appearing: it arrives almost fully lit
     before it has finished moving, and reads as a snap rather than a fade.
     This one holds back at the start, does most of its work through the
     middle, and closes gently — so the text becomes legible on the way in
     rather than at the beginning. */
  --ease: cubic-bezier(.2, .68, .24, .99);
  --ease-settle: cubic-bezier(.32, .06, .18, .96);
  --ease-fade: cubic-bezier(.44, .04, .3, 1);
}

/* ── reset ──────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--sky-3);
  color: var(--ink);
  font: 400 1.0625rem/1.62 var(--serif);
  font-feature-settings: "kern" 1, "liga" 1;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
@media (min-width: 720px) { body { font-size: 1.125rem; } }
img, svg, canvas { display: block; max-width: 100%; }
h1, h2, h3 { margin: 0; font-weight: 400; }
p, dl, dd, figure { margin: 0; }
a { color: inherit; }
button { font: inherit; color: inherit; }
[hidden] { display: none !important; }

:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 3px;
  border-radius: 1px;
}

.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 99;
  background: var(--ink); color: var(--sky-3);
  padding: .8em 1.2em; font: 500 .9rem var(--sans);
}
.skip:focus { left: 0; }

/* ── wordmark, labels ───────────────────────────────────────────────────────
   The official lockup, from `Astral logo design review.zip`: the Astral moon
   beside ASTRAL in Cormorant SC 700 at .025em. What it replaces was IBM Plex
   Sans at .17em — the interface face doing duty as a brand, which is the thing
   a wordmark exists not to be.

   Built as a mark plus live text rather than as one image file. The text stays
   selectable, scales with the page, sets its own colour, and — with the mark
   on `currentColor` — the whole lockup inverts on the night field from one
   declaration. An <img> of the lockup could do none of that, and an SVG in an
   <img> cannot reach a webfont at all: it would have fallen back to a system
   serif on every page.

   Cormorant SC is loaded subset to the six letters of the word, the same
   device --serif already uses for ₮. The brand name never translates, so no
   Cyrillic is needed and the request is a few hundred bytes. Both <link>s are
   repeated in every page head; see the note there.

   The size is set on the element and the mark is sized in em against it, so
   the lockup keeps its proportions wherever it is scaled. 1.05em is the mark's
   box: its ink fills 108 of its 120 viewBox units, which puts the moon at
   about 1.43 times the cap height — the ratio in the export. */
.wordmark {
  display: inline-flex; align-items: center; gap: .4em;
  font: 700 1.16rem/1 var(--display-sc);
  letter-spacing: .025em; text-transform: uppercase;
  text-decoration: none; color: var(--ink);
}
/* Measured against the export rather than eyeballed. There the moon's ink runs
   .945em against a cap height of .632em — a ratio of 1.495 — and the ink fills
   108 of its 120 viewBox units, which is where 1.05em comes from. Its centre
   sits .009em above the centre of the caps.

   No vertical nudge is needed to land that: `line-height: 1` on the lockup
   makes the word's box exactly 1em, and Cormorant's caps happen to centre in
   it to within .001em. If the face or the leading ever changes, re-measure
   before adding a margin — and halve whatever correction you calculate, since
   a negative margin on a centred flex item moves it by half its value. */
.wordmark__mark {
  flex: none;
  width: 1.05em; height: 1.05em;
}
.tag {
  display: inline-block;
  font: var(--label);
  letter-spacing: var(--track); text-transform: uppercase;
  color: var(--ink-3);
  margin-right: .7em;
  white-space: nowrap;
}
.tag--onDark { color: var(--on-night-2); }

/* ── language control ───────────────────────────────────────────────────────
   Subordinate to the wordmark on purpose: it is chrome, not an offer. It is
   also deliberately not the signal colour — the signal marks what the chart
   determined and the action that starts a portrait, and a language switch is
   neither. Both languages are shown rather than only the one you are not in,
   so a reader can see the site is bilingual without having to read the label
   they cannot read. Names are endonyms and never translate. */
.lang {
  display: flex; align-items: baseline; gap: .42em;
  font: 500 .78rem/1 var(--sans);
  white-space: nowrap;
}
.lang__now { color: var(--ink); }
.lang__sep { color: var(--ink-4); }
.lang__alt {
  color: var(--ink-3);
  text-decoration: none;
  padding-bottom: 1px;
  border-bottom: 1px solid transparent;
  transition: color .18s var(--ease, ease), border-color .18s var(--ease, ease);
}
.lang__alt:hover { color: var(--ink); border-bottom-color: var(--rule); }
.lang__alt:focus-visible {
  outline: 2px solid var(--signal); outline-offset: 3px;
  border-radius: 2px; border-bottom-color: transparent;
}
/* On the night field the same control has to invert. */
.lang--onDark .lang__now { color: var(--on-night); }
.lang--onDark .lang__sep { color: var(--rule-night); }
.lang--onDark .lang__alt { color: var(--on-night-2); }
.lang--onDark .lang__alt:hover {
  color: var(--on-night); border-bottom-color: var(--rule-night);
}

/* ── buttons and links ──────────────────────────────────────────────────── */
/* The vertical padding is deliberately unequal, by .09em, on all three
   variants. Equal padding centres the *font box*, and Plex's is lopsided —
   1.02em of ascent against .27em of descent — while the ink a label actually
   draws runs from about .70em above the baseline to .20em below it. Centre the
   box and the ink lands .126em low, which on a small button reads as the label
   crowding the bottom edge; every Mongolian label here has a descender (р, у,
   ж) and so does most English (g, y, p). Lifting by .09em rather than the full
   .126em is the compromise: it relieves the crowding without leaving a label
   that has no descenders at all — "Continue", "Back" — sitting visibly high.
   Every button keeps the height it had, because the two paddings still sum to
   what the one used to be. */
.btn {
  appearance: none; border: 0; cursor: pointer;
  display: inline-block; position: relative;
  padding: .93em 1.7em 1.11em;
  background: var(--signal); color: #fff;
  font: 500 .98rem/1 var(--sans);
  letter-spacing: .005em;
  text-decoration: none;
  border-radius: 2px;
  transition: background .28s var(--ease), transform .28s var(--ease);
}
.btn::after {
  content: ""; position: absolute; inset: 0;
  border-radius: 2px;
  box-shadow: 0 1px 0 rgba(11, 16, 20, .18), 0 12px 26px -14px rgba(214, 51, 13, .75);
  pointer-events: none;
}
.btn:hover { background: var(--signal-deep); transform: translateY(-1px); }
.btn:active { transform: none; }
.btn--small { padding: .61em 1.15em .79em; font-size: .84rem; }
.btn--quiet {
  background: transparent; color: var(--ink);
  border: 1px solid var(--ink); padding: .69em 1.3em .87em; font-size: .9rem;
}
.btn--quiet::after { display: none; }
.btn--quiet:hover { background: var(--ink); color: var(--sky-3); transform: none; }
.btn.is-busy { pointer-events: none; opacity: .72; }

.link {
  appearance: none; background: none; border: 0; padding: 0; cursor: pointer;
  font: 400 .93rem/1.5 var(--sans);
  color: var(--ink-2);
  text-decoration: underline;
  text-decoration-color: var(--ink-4);
  text-decoration-thickness: 1px;
  text-underline-offset: .22em;
}
.link:hover { color: var(--ink); text-decoration-color: var(--signal); }
.link--sm { font-size: .82rem; }

/* ── reduced motion ─────────────────────────────────────────────────────── */
/* The still page is the finished composition, not a disabled one. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto !important; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}

/* ── provider-mode notice ───────────────────────────────────────────────── */
/* Written into the page by the server, and only in fake mode. It has to be
   unmissable without becoming the design: a disclosure the reader can scroll
   past without registering is not a disclosure. So it takes the signal colour,
   which everywhere else marks something determined — here what is determined is
   that none of this is real. It sits in flow rather than fixed, because a
   floating badge is exactly what a reader learns to ignore. */
.astral-mode-banner {
  margin: 0;
  padding: .62rem clamp(1rem, 5vw, 2.4rem);
  background: var(--ink);
  color: var(--sky-3, #DCE4E8);
  font: 400 .82rem/1.45 var(--sans);
  text-align: center;
  text-wrap: balance;
}

.astral-mode-banner__tag {
  display: inline-block;
  margin-inline-end: .5em;
  color: var(--signal);
  font-weight: 600;
  font-size: .78rem;
  letter-spacing: .09em;
  text-transform: uppercase;
}

@media (prefers-contrast: more) {
  .astral-mode-banner { border-block-end: 2px solid var(--signal); }
}
