/* ─────────────────────────────────────────────────────────────────────────
   Ardent — core.
   Tokens, reset, the night-sky backdrop, the instrument chrome, and the two
   page modes every page on this domain picks between.

   TOKENS: named `--tn-*`, the same vocabulary every other site in the network
   uses (see titanite-themes). Swapping a component between sites should not
   mean renaming a variable. The values are this domain's own.

   PAGE MODES — the important part of this file.

   `<html class="mode-fixed">`  the front door. One screen, never scrolls.
   `<html class="mode-scroll">` everything else. A normal scrolling document.

   The old stylesheet put `overflow: hidden` on `html, body` unconditionally,
   for the landing page's benefit, and every other page loaded it. `/updates`,
   `/terms` and `/privacy` were therefore clipped at one viewport with no way
   to reach the rest — the page had the content and the browser had been told
   not to let anyone at it. Scroll locking now lives ONLY under `.mode-fixed`,
   so a page that scrolls is the default and a page that does not has to ask.

   In both modes the decorative layers sit in one `position: fixed` backdrop
   and the content scrolls in front of them. Fixed to the initial containing
   block, which is the one box on the page that does NOT resize when a mobile
   browser hides its URL bar — so the starfield does not reflow mid-scroll.
   ───────────────────────────────────────────────────────────────────────── */

/* ── Self-hosted type ─────────────────────────────────────────────────────
   Was Google Fonts. Two reasons it is not any more: the privacy policy on
   this domain says no third party sees your visit, and a font CDN is a third
   party that sees every visit; and a strict CSP is much easier to write when
   nothing loads off-origin. 23 KB for both subsets. */
@font-face {
  font-family: "Marcellus";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/marcellus-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Marcellus";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/marcellus-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* Surfaces */
  --tn-void: #05050c;
  --tn-panel: rgba(17, 13, 34, 0.58);
  --tn-raised: rgba(26, 21, 48, 0.72);
  --tn-sunk: rgba(3, 2, 8, 0.55);

  /* Lines. One hairline colour and one for the moment something is focused. */
  --tn-line: rgba(214, 206, 255, 0.16);
  --tn-line-hi: rgba(190, 180, 255, 0.42);

  /* Text */
  --tn-hi: rgba(255, 255, 255, 0.94);
  --tn-mid: rgba(220, 214, 255, 0.66);
  /* The quiet voice for dates, labels and the footer links. At 0.44 it came
     out near #615d77 on the void, about 3.2:1, and it is only ever used for
     small text. 0.58 lands near 5:1 on the void and on a card, and still
     sits a step below --tn-mid. */
  --tn-lo: rgba(214, 206, 255, 0.58);

  /* Accent. The lifted indigo, not brand #373575 — the deep one is unreadable
     as text on this void, the same call titanite-themes made for dark
     TITANNET. The deep one stays available for fills. */
  --tn-accent: #9a97e8;
  --tn-accent-deep: #373575;
  --tn-accent-wash: rgba(120, 112, 230, 0.14);
  --tn-accent-edge: rgba(154, 151, 232, 0.42);
  --tn-alert: #e88f8c;

  /* The Titanite brand quadrant, used only for the 2px rule motif. */
  --tn-q-orange: #e77226;
  --tn-q-indigo: #373575;
  --tn-q-red: #a42729;
  --tn-q-charcoal: #4a4a48;

  /* Type. Display is the serif the wordmark is set in; body is the system
     stack. The old sheet set Marcellus on `body`, so the legal pages were
     served in a display face despite their own stylesheet saying in a comment
     that they should be the most legible thing on the domain. */
  --tn-font-display: "Marcellus", "Times New Roman", Times, serif;
  --tn-font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, sans-serif;
  --tn-font-mono: ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas,
    monospace;

  --tn-radius: 8px;
  --tn-radius-lg: 12px;

  /* Inset for the frame and the bars. One number, so they cannot drift. */
  --edge: clamp(14px, 3vmin, 34px);
  --orb-size: clamp(220px, 46vmin, 560px);

  color-scheme: dark;
  /* Keeps a mobile pinch-zoom from making the fixed chrome jump. */
  -webkit-text-size-adjust: 100%;
}

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

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

ul[class],
ol[class] {
  list-style: none;
  padding: 0;
}

img,
svg,
canvas {
  display: block;
  max-width: 100%;
}

html {
  background: var(--tn-void);
}

body {
  min-height: 100svh;
  background: var(--tn-void);
  color: var(--tn-hi);
  font-family: var(--tn-font-body);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ── Page mode: fixed ─────────────────────────────────────────────────────
   The front door. The only page that locks the viewport, and it does it here
   rather than in a global rule so nothing else inherits the lock. */
.mode-fixed,
.mode-fixed body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

/* ── Page mode: scroll ────────────────────────────────────────────────────
   Ordinary document scrolling, on purpose. No inner scroll container, no
   transformed ancestor, nothing that takes the scroll away from the browser
   and hands it to JavaScript. `overflow` is never touched. */
.mode-scroll body {
  /* Room under the fixed top bar. The bar is chrome, not layout, so it cannot
     push content on its own. */
  padding-top: calc(var(--edge) + 3.4rem);
}

.mode-scroll:focus-within {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  .mode-scroll:focus-within {
    scroll-behavior: auto;
  }
}

/* ── Skip link ───────────────────────────────────────────────────────────
   First thing in the tab order on every page. Cheap, and the alternative is
   tabbing past the whole header on every visit. */
.skip {
  position: absolute;
  left: 50%;
  top: 0;
  z-index: 40;
  transform: translate(-50%, -140%);
  padding: 0.7em 1.2em;
  background: #14102a;
  border: 1px solid var(--tn-line-hi);
  border-radius: 0 0 var(--tn-radius) var(--tn-radius);
  color: var(--tn-hi);
  font-size: 0.8rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  transition: transform 0.2s ease;
}
.skip:focus-visible {
  transform: translate(-50%, 0);
}

/* ── The backdrop ────────────────────────────────────────────────────────
   One fixed box holding every decorative layer. `inset: 0` on a fixed element
   resolves against the initial containing block, which does not change height
   when a mobile browser collapses its toolbar — so the canvas keeps its size
   across a scroll instead of resizing (and, in the old code, regenerating the
   whole starfield) on every toolbar animation. */
.sky {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  background: var(--tn-void);
}

/* Deep-field wash under the canvas. The moving nebula is painted into the
   canvas now; this is the still ground it sits on. */
.sky::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(58% 58% at 50% 34%, rgba(38, 28, 86, 0.42), transparent 74%),
    radial-gradient(120% 120% at 50% 50%, #0a0716 0%, #060410 56%, #030206 100%);
}

/* Vignette, over the canvas, under the content. */
.sky::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    128% 106% at 50% 47%,
    rgba(5, 5, 12, 0) 34%,
    rgba(5, 5, 12, 0.4) 74%,
    rgba(5, 5, 12, 0.88) 100%
  );
}

/* On a page with something to read, the sky steps back. At full strength a
   bright star sits directly behind a line of type and the line loses. `--sky`
   is a variable rather than a fixed number so a page with a full-bleed hero
   (the changelog) can hold it at 1 up there and hand it back on scroll. */
.mode-scroll .sky {
  opacity: var(--sky, 0.5);
}

.sky canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Everything that is not the backdrop sits above it. */
.topbar,
.botbar,
.frame,
main,
footer {
  position: relative;
  z-index: 2;
}

/* ── The frame ───────────────────────────────────────────────────────────
   Hairline inset box with reticle ticks at the corners. Decoration; built by
   chrome.js so the four corner spans are written once rather than pasted into
   five documents and left to drift. */
.frame {
  position: fixed;
  inset: var(--edge);
  z-index: 1;
  pointer-events: none;
  border: 1px solid var(--tn-line);
  border-radius: 4px;
  animation: fade-in 1.6s ease 0.9s both;
}
.frame .tick {
  position: absolute;
  width: 13px;
  height: 13px;
}
.frame .tick::before,
.frame .tick::after {
  content: "";
  position: absolute;
  background: rgba(224, 216, 255, 0.5);
}
.frame .tick::before {
  width: 13px;
  height: 1px;
  top: 0;
}
.frame .tick::after {
  width: 1px;
  height: 13px;
  left: 0;
}
.frame .tl { top: -1px; left: -1px; }
.frame .tr { top: -1px; right: -1px; }
.frame .tr::before, .frame .tr::after { right: 0; left: auto; }
.frame .bl { bottom: -1px; left: -1px; }
.frame .bl::before, .frame .bl::after { bottom: 0; top: auto; }
.frame .br { bottom: -1px; right: -1px; }
.frame .br::before, .frame .br::after { bottom: 0; top: auto; right: 0; left: auto; }

/* ── Scroll progress ─────────────────────────────────────────────────────
   A hairline across the very top in scroll mode. `--scroll` is written by
   chrome.js; `scale` on a transform-only property so it never lays out. */
.progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  z-index: 12;
  transform-origin: 0 50%;
  transform: scaleX(var(--scroll, 0));
  background: linear-gradient(
    to right,
    var(--tn-q-orange) 0 25%,
    var(--tn-q-indigo) 25% 50%,
    var(--tn-q-red) 50% 75%,
    var(--tn-q-charcoal) 75% 100%
  );
  opacity: 0.75;
  pointer-events: none;
}
.mode-fixed .progress { display: none; }

/* ── Bars ────────────────────────────────────────────────────────────────
   The top bar is fixed in both modes: it carries the only way home. The
   bottom bar is fixed on the front door and a real document footer
   everywhere else — a fixed strip over scrolling prose is a strip that eats
   the last line of every page on a short screen. */
.topbar {
  position: fixed;
  top: var(--edge);
  left: var(--edge);
  right: var(--edge);
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: clamp(0.45rem, 1.6vmin, 1rem) clamp(0.7rem, 2vmin, 1.4rem);
  animation: fade-in 1.6s ease 1.2s both;
}

/* On a scrolling page the bar is fixed and the content passes under it, so on
   a narrow screen the monogram lands on top of a line of body copy. Each item
   gets its own small backdrop rather than the whole bar getting a scrim: a
   full-width scrim would also cover the top edge of the frame. */
.mode-scroll .topbar > * {
  background: rgba(6, 5, 16, 0.74);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border-radius: 6px;
  padding: 0.32em 0.62em;
}

/* On a phone the reading panel is edge to edge, so there is no margin for the
   bar to sit in and the per-item chips stop being enough. The frame goes (it
   is desktop chrome and it eats 28px of a 390px screen), and the bar becomes a
   proper full-bleed header with a fade under it. */
@media (max-width: 620px) {
  .mode-scroll .frame {
    display: none;
  }
  .mode-scroll .topbar {
    top: 0;
    left: 0;
    right: 0;
    padding: 0.75rem var(--edge) 1.5rem;
    background: linear-gradient(
      to bottom,
      rgba(5, 5, 12, 1) 0%,
      rgba(5, 5, 12, 1) 62%,
      rgba(5, 5, 12, 0) 100%
    );
  }
  .mode-scroll .topbar > * {
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    padding: 0;
  }
}

.botbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: clamp(0.45rem, 1.6vmin, 1rem) clamp(0.7rem, 2vmin, 1.4rem);
}
.mode-fixed .botbar {
  position: fixed;
  bottom: var(--edge);
  left: var(--edge);
  right: var(--edge);
  z-index: 10;
  animation: fade-in 1.6s ease 1.5s both;
}
.mode-scroll .botbar {
  position: relative;
  z-index: 2;
  max-width: 74ch;
  margin: 0 auto;
  padding-block: 2rem calc(var(--edge) * 2);
  border-top: 1px solid var(--tn-line);
  margin-top: clamp(3rem, 8vmin, 6rem);
}

/* The micro-label voice: mono, upper, tracked out, quiet. Used for the
   coordinates, the established date, page eyebrows and legal links. */
.meta {
  font-family: var(--tn-font-mono);
  /* Never below about 11px: these carry the footer links to the legal
     pages, and tracked-out capitals at 9px are hard to read on a phone. */
  font-size: clamp(0.68rem, 1.1vmin, 0.72rem);
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--tn-lo);
  white-space: nowrap;
}
.meta--wrap { white-space: normal; }

/* ── Monogram ────────────────────────────────────────────────────────────
   On the front door it is a disclosure that reveals the identity lineage; on
   every other page it is the link home. Same mark, two jobs, so the element
   differs and the styling does not. */
.monogram {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.6em;
  padding: 0.15em 0.1em;
  font-family: var(--tn-font-display);
  font-size: clamp(1.05rem, 2.3vmin, 1.45rem);
  letter-spacing: 0.18em;
  text-indent: 0.18em;
  line-height: 1;
  color: rgba(255, 255, 255, 0.78);
  text-shadow: 0 0 18px rgba(150, 150, 255, 0.4);
  background: none;
  border: 0;
  border-radius: 3px;
  text-decoration: none;
  cursor: pointer;
  transition: color 0.35s ease, text-shadow 0.35s ease;
}
.monogram:hover,
.monogram:focus-visible {
  color: #fff;
  text-shadow: 0 0 22px rgba(150, 150, 255, 0.65);
}

/* ── Identity lineage ────────────────────────────────────────────────────
   A real disclosure: a <button> with aria-expanded driving a panel, opened by
   click or Enter and closed by Escape or an outside click. It used to open on
   hover only, on an element with role="button" and no key handler, which put
   the content behind a pointer no touch device and no keyboard has. */
.lineage {
  position: relative;
}
.lineage-panel {
  position: absolute;
  top: calc(100% + 14px);
  left: -6px;
  min-width: 208px;
  padding: 16px 18px;
  background: rgba(11, 9, 22, 0.86);
  border: 1px solid var(--tn-line);
  border-radius: var(--tn-radius);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 18px 54px rgba(0, 0, 0, 0.55);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: opacity 0.28s ease, transform 0.28s ease, visibility 0.28s;
  z-index: 11;
}
.lineage-panel[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
.lineage-title {
  display: block;
  font-family: var(--tn-font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--tn-lo);
  margin-bottom: 12px;
}
.lineage-list {
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.lineage-list li {
  position: relative;
  padding-left: 18px;
  font-size: 0.9rem;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.52);
}
.lineage-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 6px;
  height: 1px;
  background: rgba(214, 206, 255, 0.35);
}
.lineage-list li[aria-current="true"] {
  color: #fff;
  text-shadow: 0 0 16px rgba(150, 150, 255, 0.5);
}
.lineage-list li[aria-current="true"]::before {
  width: 10px;
  background: var(--tn-accent-edge);
}

/* ── Links ───────────────────────────────────────────────────────────────
   The quiet arrow link used for sign-in and for moving between pages. */
.arrow-link {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.3em 0.2em;
  border-radius: 4px;
  color: rgba(255, 255, 255, 0.8);
  text-decoration: none;
  transition: color 0.35s ease, text-shadow 0.35s ease;
}
.arrow-link .label {
  font-family: var(--tn-font-mono);
  font-size: clamp(0.62rem, 1.3vmin, 0.78rem);
  text-transform: uppercase;
  letter-spacing: 0.2em;
  text-indent: 0.2em;
}
.arrow-link .glyph {
  font-size: 1.1em;
  line-height: 1;
  transition: transform 0.35s ease;
}
.arrow-link:hover,
.arrow-link:focus-visible {
  color: #fff;
  text-shadow: 0 0 20px rgba(150, 150, 255, 0.55);
}
.arrow-link:hover .glyph,
.arrow-link:focus-visible .glyph {
  transform: translateX(3px);
}

/* On the front door the word is hidden until the glyph is hovered or focused.
   Everywhere else it reads as a normal link and stays visible: a page you
   arrive at from somewhere needs its way out to be legible. */
.mode-fixed .arrow-link .label {
  opacity: 0;
  transform: translateX(6px);
  transition: opacity 0.35s ease, transform 0.35s ease;
}
.mode-fixed .arrow-link:hover .label,
.mode-fixed .arrow-link:focus-visible .label {
  opacity: 0.92;
  transform: translateX(0);
}
/* A phone has no hover, so the word would never show and the only way in
   would be a bare arrow. Where there is no hover, it simply stays. */
@media (hover: none) {
  .mode-fixed .arrow-link .label {
    opacity: 0.92;
    transform: none;
  }
}

.link-row {
  display: flex;
  gap: 14px;
  align-items: center;
}
.link-row a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.18);
}
.link-row a:hover,
.link-row a:focus-visible {
  color: var(--tn-hi);
  border-bottom-color: var(--tn-line-hi);
}

/* ── Focus ───────────────────────────────────────────────────────────────
   One visible focus ring, defined once, for every focusable thing. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--tn-accent);
  outline-offset: 3px;
  border-radius: 3px;
}

/* ── Shared motion ───────────────────────────────────────────────────────── */
@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes breathe {
  0%, 100% { transform: translateY(0) scale(1); opacity: 0.97; }
  50% { transform: translateY(-1.1%) scale(1.03); opacity: 1; }
}

/* Reveal-on-scroll, driven by chrome.js via IntersectionObserver. The default
   is the visible state, so a browser that never runs the script — or a reader
   with reduced motion — gets the content rather than a blank page. */
.reveal {
  opacity: 1;
}
.js .reveal:not(.is-in) {
  opacity: 0;
  transform: translateY(18px);
}
.js .reveal {
  transition: opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    /* The delay has to go too. Collapsing only the duration leaves an entrance
       animation with `fill-mode: both` sitting on its FIRST keyframe for the
       whole delay — so the top bar, the frame, the wordmark and the footer
       were invisible for up to 1.5 seconds for exactly the readers who asked
       for less motion. Verified in a headless browser with the media feature
       forced on, which is the only way this shows up. */
    animation-delay: 0ms !important;
    transition-delay: 0ms !important;
  }
  .js .reveal:not(.is-in) {
    opacity: 1;
    transform: none;
  }
}
