/* ==========================================================================
   Eric Voss — personal site
   Palette: a 16-bit menu window open over the sky. At night that is royal
   blue glass over stars; by day it is parchment over an afternoon. Both run
   off the same tokens, so a single `data-theme` flip on <html> re-skins the
   whole site — see the day block below, and 10-skyline for the backdrop.

   How the tokens are organised, and the rule for adding one:

     --rgb-*     bare "R G B" triples, for any colour that appears at more
                 than one alpha. Rules spell them `rgb(var(--rgb-gold) / .4)`,
                 so a theme swaps the hue once and every alpha follows.
     --<name>    a finished value: a colour, a gradient, a whole box-shadow
                 list. Used where the night and day versions differ by more
                 than a hue — a four-stop gradient, say.

   Nothing outside this block and the day block below may hardcode a colour.
   If a new rule needs one, it wants a token.
   ========================================================================== */

:root {
  /* ------------------------------------------------------------ primitives */
  --rgb-gold: 255 215 107;
  --rgb-gold-deep: 192 138 30;
  --rgb-crystal: 126 240 224;
  --rgb-frame-lit: 230 235 255;
  --rgb-frame-mid: 108 122 196;
  --rgb-frame-dark: 5 6 15;
  --rgb-muted: 147 160 216;
  --rgb-window: 41 63 158;
  --rgb-window-deep: 11 19 74;
  /* the dark a sunken slot bottoms out at */
  --rgb-well: 4 7 26;
  /* every hard drop shadow */
  --rgb-shade: 0 0 0;
  /* the lit top edge on a bevel */
  --rgb-sheen: 255 255 255;
  /* the 1px offset under every glyph */
  --rgb-emboss: 0 0 0;

  /* surfaces */
  --bg: #070a1c;
  --bg-2: #0d1230;
  --window-top: rgb(var(--rgb-window) / .92);
  --window-bot: rgb(var(--rgb-window-deep) / .95);
  --window-flat: #121c52;
  --frame-lit: #e6ebff;
  --frame-mid: #6c7ac4;
  --frame-dark: #05060f;
  --sunken: rgb(var(--rgb-well) / .55);

  /* ink */
  --ink: #f2f5ff;
  --ink-soft: #cdd6f7;
  --ink-bright: #ffffff;
  --muted: #93a0d8;
  --faint: rgb(var(--rgb-muted) / .28);
  --sel-ink: #14183a;

  /* accents */
  --gold: #ffd76b;
  --gold-deep: #c08a1e;
  --crystal: #7ef0e0;
  --magic: #c79bff;
  --hp: #64d97a;
  --mp: #6aa8ff;
  --danger: #ff7a7a;

  /* The familiars. Each cat carries its own four colours because the problem
     is not the same for both: Artemis is white and needs a rim only to hold
     her internal edges together, while Apollo is very nearly the colour of
     this sky and would vanish into it without a light one. Both sets exist in
     both themes for exactly that reason — see the day block. */
  --artemis-fur: #f4f7ff;
  --artemis-shade: #c3cdf0;
  --artemis-line: #4a56a0;
  --artemis-eye: #2a3160;
  --apollo-fur: #171a35;
  --apollo-shade: #0c0e22;
  --apollo-line: #7d8ad4;
  --apollo-eye: #ffd76b;

  /* the hard shadow under every glyph */
  --text-shadow: 1px 1px 0 rgb(var(--rgb-emboss) / .85);
  --text-shadow-sm: 1px 1px 0 rgb(var(--rgb-emboss) / .6);
  /* display type carries a fatter one, plus a crystal bloom at night */
  --title-shadow: 2px 2px 0 rgb(var(--rgb-emboss) / .85), 0 0 18px rgb(var(--rgb-crystal) / .22);
  --title-shadow-flat: 2px 2px 0 rgb(var(--rgb-emboss) / .85);

  /* Chunky offset shadows instead of soft ones — a sprite dropped on a table,
     not a card floating over it. */
  --shadow: 0 0 0 2px var(--frame-dark), 0 8px 0 rgb(var(--rgb-shade) / .35);
  --shadow-sm: 0 0 0 2px var(--frame-dark), 0 4px 0 rgb(var(--rgb-shade) / .3);
  /* the inner bevel + vignette that makes .surface read as glass */
  --surface-bevel: inset 0 0 0 2px rgb(10 18 70 / .85), inset 0 0 26px rgb(4 8 40 / .55);

  /* ---------------------------------------------------- composite surfaces */

  /* Everything below is a whole background/gradient rather than a colour,
     because the day version is not the night version with a different hue. */

  /* the page itself: nebula haze over a deep vertical fall */
  --body-wash:
    radial-gradient(900px 520px at 12% 0%, rgb(60 48 140 / .5), transparent 68%),
    radial-gradient(760px 480px at 88% 6%, rgb(24 62 150 / .45), transparent 66%),
    radial-gradient(1100px 640px at 50% 108%, rgb(96 40 120 / .36), transparent 64%),
    linear-gradient(180deg, #060818 0%, #0a0f2c 42%, #0b1030 72%, #070a1c 100%);
  --scanline-ink: rgb(var(--rgb-shade) / .13);
  --scanline-opacity: .55;

  --navbar-fill: linear-gradient(180deg, rgb(36 55 140 / .96) 0%, rgb(20 32 96 / .96) 46%, rgb(9 14 48 / .97) 100%);
  --footer-fill: linear-gradient(180deg, rgb(16 25 76 / .97) 0%, rgb(7 11 38 / .98) 55%, rgb(4 6 24 / .99) 100%);
  /* the command window the nav links sit in */
  --cmd-fill: linear-gradient(180deg, rgb(9 15 54 / .8), rgb(4 7 30 / .88));
  /* an inner panel: the quest log, and anything else framed inside a window */
  --panel-fill: linear-gradient(180deg, rgb(18 28 82 / .72), rgb(8 13 52 / .8));
  /* the chapter plate a prose h2 sits on */
  --chapter-fill: linear-gradient(180deg, rgb(18 28 82 / .85), rgb(8 13 52 / .9));
  /* the ability badge behind a 16px icon */
  --badge-fill: linear-gradient(180deg, rgb(var(--rgb-window) / .6), rgb(8 14 56 / .85));
  /* the nameplate the character's name sits on */
  --nameplate-fill: linear-gradient(180deg, rgb(5 9 36 / .62), rgb(5 9 36 / .22));
  /* a party member's nameplate bar, resting and selected */
  --plate-fill: linear-gradient(180deg, rgb(var(--rgb-window) / .9), rgb(var(--rgb-window-deep) / .95));
  --plate-fill-hot: linear-gradient(180deg, rgb(70 96 205 / .95), rgb(20 32 108 / .95));
  /* the mat a letterboxed screenshot is hung on */
  --mat-fill: linear-gradient(155deg, rgb(var(--rgb-window) / .4), rgb(var(--rgb-window-deep) / .6));
  --mat-fill-soft: linear-gradient(155deg, rgb(var(--rgb-window) / .35), rgb(var(--rgb-window-deep) / .55));
  --code-fill: rgb(3 5 20 / .85);

  /* the colour the screen flashes through on a theme change */
  --wipe-ink: #070a1c;

  /* Pixel art has no round corners. Kept as tokens so any stray usage
     resolves to square rather than needing to be hunted down. */
  --radius: 0;
  --radius-sm: 0;

  --max: 1080px;
  --gutter: 22px;
  --lh: 1.75;

  --display: "Press Start 2P", ui-monospace, "Courier New", monospace;
  --ui: "Silkscreen", "Press Start 2P", ui-monospace, monospace;
  --body: "DotGothic16", "Silkscreen", ui-sans-serif, system-ui, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;

  /* Legacy aliases — the old theme's font tokens, remapped so nothing that
     still references them falls back to a system serif. */
  --serif: var(--display);
  --sans: var(--body);

  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
  --ease-step: steps(5, end);
}

/* ==========================================================================
   day

   Same menu, opened in the afternoon instead: the royal-blue glass becomes
   parchment, the white bevels become a bright page edge over a dark binding,
   and the ink inverts — dark brown glyphs lifted by a 1px highlight rather
   than dropped on a black one.

   Two constraints held every value here:

   1. Gold is the site's accent and it lands on labels as small as 10px. A
      bright #ffd76b on cream is around 1.5:1, so day gold is a brass —
      #845604 — chosen as the lightest amber that still clears 4.5:1 against
      the *darkest* thing it sits on (a sunken tan slot). Decoration and text
      share the token, so the studs and cursors go brass too. That is the
      right trade: an unreadable label is worse than a duller stud.
   2. The wells are tan, not grey. A recess in parchment is a darker patch of
      the same paper — a grey one would read as a hole cut in the page.
   ========================================================================== */

:root[data-theme="day"] {
  color-scheme: light;

  /* ------------------------------------------------------------ primitives */
  --rgb-gold: 132 86 4;
  --rgb-gold-deep: 85 55 2;
  --rgb-crystal: 14 107 116;
  --rgb-frame-lit: 255 252 238;
  --rgb-frame-mid: 179 135 63;
  --rgb-frame-dark: 61 44 21;
  --rgb-muted: 122 106 78;
  --rgb-window: 255 250 236;
  --rgb-window-deep: 243 230 200;
  --rgb-well: 205 178 128;
  --rgb-shade: 74 54 26;
  --rgb-sheen: 255 255 255;
  --rgb-emboss: 255 252 240;

  /* surfaces */
  --bg: #a9dbf2;
  --bg-2: #cbe9f7;
  --window-top: rgb(var(--rgb-window) / .95);
  --window-bot: rgb(var(--rgb-window-deep) / .96);
  --window-flat: #f7ecd4;
  --frame-lit: #fffcee;
  --frame-mid: #b3873f;
  --frame-dark: #3d2c15;
  /* Opaque, where the night value is translucent. Two reasons, and they pull
     the same way. A sunken slot is the commonest surface on the site after the
     window itself, and it carries gold and teal labels; letting the parchment
     tint through put those at 4.1-4.3:1. And a handful of these chips — the
     back link, the About pill row, the sky toggle's hint — sit on open sky
     rather than on a window, where a warm translucent tan turns sage-grey
     against the blue and stops reading as parchment at all. Fixing the tint
     fixes both: this is the .38 blend over the window, resolved once. */
  --sunken: #ecdec1;

  /* ink */
  --ink: #2b2110;
  --ink-soft: #40331f;
  --ink-bright: #171008;
  /* Darker than it looks like it needs to be against a mid-tone parchment:
     this also has to clear the *bottom* stop of the footer gradient, which is
     the darkest ground any muted text lands on. */
  --muted: #655640;
  --faint: rgb(var(--rgb-muted) / .34);
  --sel-ink: #fff6e0;

  /* accents */
  --gold: #845604;
  --gold-deep: #553702;
  --crystal: #0e6b74;
  --magic: #6b3fa0;
  --hp: #1f7a35;
  --mp: #1c5aa8;
  --danger: #a32626;

  /* The familiars, with the problem inverted: on parchment it is Artemis who
     needs a real outline to exist at all, and Apollo who reads on his own. His
     eye does not follow day gold down to brass — it is a 3px decorative dot on
     near-black fur, not a label, and the brass would disappear into him. */
  --artemis-fur: #fffdf7;
  --artemis-shade: #ddcaa4;
  --artemis-line: #6b4f22;
  --artemis-eye: #3a2a10;
  --apollo-fur: #2b2438;
  --apollo-shade: #191428;
  --apollo-line: #0d0a18;
  --apollo-eye: #d9a13c;

  /* No crystal bloom by day — a glow needs something dark to glow against. */
  --title-shadow: 2px 2px 0 rgb(var(--rgb-emboss) / .95);
  --title-shadow-flat: 2px 2px 0 rgb(var(--rgb-emboss) / .95);

  --surface-bevel: inset 0 0 0 2px rgb(150 112 52 / .4), inset 0 0 26px rgb(176 140 82 / .28);

  /* ---------------------------------------------------- composite surfaces */

  /* Sun bloom in the top right, where the sprite actually hangs, and a warm
     band along the horizon the ridges rise out of. */
  --body-wash:
    radial-gradient(880px 520px at 92% 8%, rgb(255 244 190 / .85), transparent 58%),
    radial-gradient(760px 460px at 14% 4%, rgb(255 255 255 / .3), transparent 62%),
    radial-gradient(1200px 700px at 50% 106%, rgb(255 226 164 / .55), transparent 66%),
    linear-gradient(180deg, #3f9ad8 0%, #6ebde9 34%, #a3d9f2 64%, #dcf0f6 100%);
  --scanline-ink: rgb(120 92 40 / .08);
  --scanline-opacity: .4;

  --navbar-fill: linear-gradient(180deg, rgb(255 249 232 / .97) 0%, rgb(246 234 205 / .97) 46%, rgb(228 208 168 / .97) 100%);
  --footer-fill: linear-gradient(180deg, rgb(252 243 220 / .98) 0%, rgb(240 224 188 / .98) 55%, rgb(228 206 162 / .99) 100%);
  --cmd-fill: linear-gradient(180deg, rgb(236 220 184 / .85), rgb(224 203 160 / .9));
  /* Opaque, and lighter than it looks like it needs to be. The quest log is
     the one inner panel that stands on the sky with nothing behind it, and a
     warm fill at 82% over that blue comes out sage-grey — measured
     rgb(217,223,213) — which drops its muted labels and links to 3.2:1 and
     stops it reading as parchment at all. */
  --panel-fill: linear-gradient(180deg, #fefaee, #f6ecd6);
  --chapter-fill: linear-gradient(180deg, rgb(248 236 206 / .92), rgb(238 221 182 / .95));
  --badge-fill: linear-gradient(180deg, rgb(255 248 226 / .92), rgb(234 214 172 / .95));
  /* Shallower than the night plate. It still has to read as sunken, but gold
     sits on it, and every extra point of tan costs the label contrast. */
  --nameplate-fill: linear-gradient(180deg, rgb(210 180 124 / .34), rgb(210 180 124 / .09));
  /* Lighter than the night plate is dark: the cat's name on it is gold, and
     gold needs a pale ground. */
  --plate-fill: linear-gradient(180deg, #fbf1d8, #f0e0bc);
  --plate-fill-hot: linear-gradient(180deg, #fff9e6, #f8ead2);
  --mat-fill: linear-gradient(155deg, rgb(224 206 168 / .6), rgb(198 174 128 / .72));
  --mat-fill-soft: linear-gradient(155deg, rgb(228 211 175 / .5), rgb(204 181 137 / .62));
  --code-fill: rgb(238 229 207 / .95);

  --wipe-ink: #bfe6f7;
}

/* Section headings are the one piece of running text that sits on the open sky
   rather than on a window, and a brass glyph on an afternoon blue lands around
   3:1 — the single place where day gold cannot be made to work without
   dragging it so dark it stops being gold everywhere else. So on the sky it
   stops being gold: dark ink clears 4.7:1 against every part of that gradient,
   and the accent survives in the pixel tab beside it. The same heading inside
   a window keeps the brass, where it reads at 4.6:1 on parchment. */
:root[data-theme="day"] .section-h2 {
  color: var(--ink);
}

:root[data-theme="day"] .surface .section-h2 {
  color: var(--gold);
}

/* The selected command inverts with the palette. At night it lights up: a warm
   band laid over dark glass. By day the command window is already a mid tan,
   and laying gold over gold leaves the label at 3.7:1 — so instead the
   selected row goes *pale*, a lit plate pressed into the tan, which is how a
   light menu marks its selection anyway. The gold-deep underscore and the
   cursor keep doing the marking; only the ground changes. */
:root[data-theme="day"] .nav-link.is-active {
  background: rgb(255 251 238 / .96);
}

:root[data-theme="day"] .nav-link.is-active:hover {
  background: rgb(255 253 246 / .98);
}

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

html {
  height: 100%;
  color-scheme: dark;
  -webkit-text-size-adjust: 100%;
}

/* Night sky: a deep gradient with a haze of nebula colour near the horizon.
   Fixed, so the stars sit still while the page scrolls past them. */
body {
  min-height: 100%;
  margin: 0;
  font-family: var(--body);
  font-size: 17px;
  line-height: var(--lh);
  color: var(--ink);
  background-color: var(--bg);
  background-image: var(--body-wash);
  background-attachment: fixed;
  text-shadow: var(--text-shadow);
}

/* A faint CRT scanline wash over everything. Low enough that you read it as
   texture rather than as stripes. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 100;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    var(--scanline-ink) 0 1px,
    transparent 1px 3px
  );
  opacity: var(--scanline-opacity);
  mix-blend-mode: multiply;
}

a {
  color: inherit;
  text-decoration: none;
}

a:focus-visible,
button:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
  border-radius: 0;
}

::selection {
  background: var(--gold);
  color: var(--sel-ink);
  text-shadow: none;
}

img {
  max-width: 100%;
}

/* --------------------------------------------------------------- structure */

.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  position: relative;
  z-index: 1;
  /* The sky below has one live control in it — the moon. .app stacks above the
     sky and its box covers the viewport, so it would take that click first
     even though nothing is painted there. Pointer events are handed back to
     the chrome and the content column only, which leaves the margins either
     side of the column transparent to the pointer. Anything added to .app
     outside those three needs its own `pointer-events: auto`. */
  pointer-events: none;
}

.navbar,
.footer,
.page > .shell {
  pointer-events: auto;
}

.shell {
  width: min(var(--max), calc(100% - var(--gutter) * 2));
  margin: 0 auto;
}

.page {
  flex: 1;
  padding: 34px 0 120px;
}

/* ------------------------------------------------------ the menu window */

/* The one primitive everything else is built from: a translucent royal-blue
   box, a white inner bevel, a hard black ring outside it. */
.surface {
  position: relative;
  background:
    linear-gradient(180deg, var(--window-top), var(--window-bot));
  border: 2px solid var(--frame-lit);
  box-shadow: var(--surface-bevel), var(--shadow);
}

/* Gold studs pinned to the four corners of the frame. One element, four
   background layers — cheaper than four pseudo-elements we don't have. */
.surface::before {
  content: "";
  position: absolute;
  inset: -2px;
  pointer-events: none;
  z-index: 2;
  background-image:
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold));
  background-size: 4px 4px;
  background-repeat: no-repeat;
  background-position: 0 0, 100% 0, 0 100%, 100% 100%;
}

/* ------------------------------------------------------------- typography */

/* Silkscreen's ampersand is nearly indistinguishable from a dollar sign, and
   "AI & Data Science" is the first line on the site — so labels that can carry
   an "&" use the body face uppercased instead. Still a bitmap font, still on
   theme, but it spells the word correctly. */
.kicker {
  font-family: var(--body);
  font-size: 13px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--gold);
  text-shadow: var(--text-shadow);
}

.h1 {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(19px, 2.9vw, 33px);
  line-height: 1.36;
  letter-spacing: 0.01em;
  margin: 12px 0 0;
  color: var(--ink-bright);
  text-shadow: var(--title-shadow);
}

.lede {
  margin: 18px 0 0;
  font-size: 17px;
  line-height: 1.72;
  color: var(--ink-soft);
  max-width: 58ch;
}

.section-h2 {
  font-family: var(--display);
  font-weight: 400;
  font-size: 15px;
  line-height: 1.5;
  letter-spacing: 0.02em;
  margin: 4px 0 10px;
  color: var(--gold);
  text-shadow: var(--text-shadow);
}

.section-h3 {
  font-family: var(--display);
  font-weight: 400;
  font-size: 12px;
  line-height: 1.6;
  margin: 0;
  color: var(--ink);
}

.muted {
  color: var(--muted);
}

/* A row of pixels rather than a hairline — a 2px dotted rail. */
.rule {
  height: 2px;
  background-image: linear-gradient(90deg, var(--frame-mid) 50%, transparent 50%);
  background-size: 6px 2px;
  opacity: 0.6;
  margin: 28px 0;
  border: 0;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ------------------------------------------------------------ theme parts */

/* Ship both strings, render one. Used for the toggles' labels, which have to
   name the command rather than the state: `display: none` also takes the
   losing copy out of the accessible name, so the button reads correctly with
   no script involved. Written as :not() so it also covers the no-JS case,
   where <html> never gets a data-theme at all. */
:root:not([data-theme="day"]) .when-day,
:root[data-theme="day"] .when-night {
  display: none;
}

/* Neither toggle does anything without a script, so neither one appears
   without one. `.js` is set inline in <head>, before first paint. */
.nav-toggle {
  display: none;
}

.js .nav-toggle {
  display: inline-flex;
}

/* The screen flash between themes: opacity in four steps, the way a 16-bit
   scene change faded out. Built by site.js on the first toggle and thrown away
   after; it sits under the CRT wash (z-index 100) so the scanlines ride over
   it. Its fill is a token, so it flashes through the outgoing sky's colour and
   clears through the incoming one. */
.theme-wipe {
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
  opacity: 0;
  background: var(--wipe-ink);
}

.theme-wipe.is-in {
  animation: theme-wipe-in 0.18s steps(4, end) forwards;
}

.theme-wipe.is-out {
  animation: theme-wipe-out 0.24s steps(4, end) forwards;
}

@keyframes theme-wipe-in {
  from { opacity: 0; }
  to { opacity: 0.94; }
}

@keyframes theme-wipe-out {
  from { opacity: 0.94; }
  to { opacity: 0; }
}

/* An inline link that fills with a band of gold, the way a selected menu
   entry lights up. */
.marker-link {
  position: relative;
  display: inline-block;
  color: var(--crystal);
  text-decoration: underline;
  text-decoration-style: dotted;
  text-decoration-color: rgb(var(--rgb-crystal) / .5);
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
  z-index: 0;
}

.marker-link::after {
  content: "";
  position: absolute;
  left: -0.12em;
  right: -0.12em;
  bottom: 0.04em;
  height: 0.5em;
  background: rgb(var(--rgb-gold) / .34);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.24s var(--ease-step);
  z-index: -1;
}

.marker-link:hover,
.marker-link:focus-visible {
  color: var(--gold);
  text-decoration-color: var(--gold);
}

.marker-link:hover::after,
.marker-link:focus-visible::after {
  transform: scaleX(1);
}

/* ------------------------------------------------------- the menu cursor */

/* The blinking pointer that sits beside whatever is selected. Drawn as a
   clipped square so it stays crisp at any zoom. */
.has-cursor {
  position: relative;
}

.has-cursor::after {
  content: "";
  position: absolute;
  left: -4px;
  top: 50%;
  width: 10px;
  height: 12px;
  margin-top: -6px;
  background: var(--gold);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity 0.12s var(--ease-step), transform 0.12s var(--ease-step);
  pointer-events: none;
}

.has-cursor:hover::after,
.has-cursor:focus-visible::after,
.has-cursor.is-active::after {
  opacity: 1;
  transform: translateX(0);
  animation: cursor-blink 1s steps(1, end) infinite;
}

@keyframes cursor-blink {
  0%,
  60% {
    opacity: 1;
  }
  61%,
  100% {
    opacity: 0.25;
  }
}

/* -------------------------------------------------------------------- nav */

.navbar {
  position: sticky;
  top: 0;
  z-index: 20;
  border-bottom: 2px solid var(--frame-mid);
  background: var(--navbar-fill);
  box-shadow: 0 4px 0 rgb(var(--rgb-shade) / .4);
}

.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 0;
}

.brand {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.brand-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: 13px;
  line-height: 1.2;
  color: var(--ink-bright);
  text-shadow: var(--text-shadow);
}

.brand-sub {
  font-family: var(--ui);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 4px;
}

.nav-link {
  position: relative;
  font-family: var(--ui);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 9px 12px;
  border: 2px solid transparent;
  transition: color 0.14s var(--ease-step), background 0.14s var(--ease-step),
    border-color 0.14s var(--ease-step);
}

.nav-link:hover {
  background: rgb(var(--rgb-gold) / .12);
  border-color: rgb(var(--rgb-gold) / .4);
  color: var(--ink-bright);
}

.nav-link.is-active {
  color: var(--gold);
  background: rgb(var(--rgb-gold) / .1);
  border-color: rgb(var(--rgb-gold) / .55);
}

/* ----------------------------------------------------------------- footer */

.footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 10;
  border-top: 2px solid var(--frame-mid);
  background: var(--footer-fill);
  box-shadow: 0 -4px 0 rgb(var(--rgb-shade) / .35);
}

.footer-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 11px 0;
  color: var(--muted);
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: 0.06em;
}

.footer-links {
  display: flex;
  align-items: center;
  gap: 16px;
}

.footer-link {
  color: var(--muted);
  transition: color 0.14s var(--ease-step);
}

.footer-link:hover {
  color: var(--gold);
}

/* ------------------------------------------------------------ small parts */

.pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

/* An item slot: sunken chip, white frame, gold when you point at it. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 9px 14px;
  border: 2px solid var(--frame-mid);
  background: var(--sunken);
  color: var(--ink-soft);
  font-family: var(--ui);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  box-shadow: 0 3px 0 rgb(var(--rgb-shade) / .4);
  transition: transform 0.12s var(--ease-step), border-color 0.12s var(--ease-step),
    color 0.12s var(--ease-step), background 0.12s var(--ease-step),
    box-shadow 0.12s var(--ease-step);
}

.pill:hover {
  transform: translateY(-2px);
  background: rgb(var(--rgb-gold) / .14);
  border-color: var(--gold);
  color: var(--ink-bright);
  box-shadow: 0 5px 0 rgb(var(--rgb-shade) / .45);
}

.pill:active {
  transform: translateY(1px);
  box-shadow: 0 1px 0 rgb(var(--rgb-shade) / .4);
}

.tag {
  display: inline-flex;
  align-items: center;
  padding: 4px 8px;
  border: 2px solid rgb(var(--rgb-gold) / .42);
  background: rgb(var(--rgb-gold) / .1);
  font-family: var(--ui);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gold);
  line-height: 1.4;
}

.icon {
  width: 16px;
  height: 16px;
  display: inline-block;
  flex: 0 0 auto;
}

.icon-flip {
  transform: rotate(180deg);
}

/* ------------------------------------------------------------------- home */

.home {
  display: grid;
  gap: 14px;
}

.hero {
  padding: 26px;
}

.hero-grid {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 28px;
  align-items: center;
}

.portrait-wrap {
  display: grid;
  place-items: center;
  position: relative;
}

/* The character portrait, framed like a party member's window slot. */
.portrait {
  width: 208px;
  height: 208px;
  object-fit: cover;
  object-position: center 22%;
  border: 2px solid var(--frame-lit);
  background: var(--sunken);
  box-shadow: 0 0 0 2px var(--frame-dark), 0 6px 0 rgb(var(--rgb-shade) / .4);
  image-rendering: pixelated;
}

.panel {
  padding: 22px 24px;
}

.copy {
  max-width: 72ch;
}

.copy p {
  margin: 0 0 16px;
  font-size: 16.5px;
  line-height: 1.8;
  color: var(--ink-soft);
}

.copy p:last-child {
  margin-bottom: 0;
}

.home-links {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  margin-top: 20px;
}

.home-link {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-family: var(--ui);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--crystal);
  padding: 5px 0;
  transition: color 0.14s var(--ease-step), transform 0.14s var(--ease-step);
}

.home-link:hover {
  color: var(--gold);
  transform: translateX(3px);
}

.section {
  display: grid;
  gap: 14px;
  margin-top: 26px;
}

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
}

/* --------------------------------------------------------------- timeline */

/* A quest log: entries stacked down a lit rail, each with a node on it. */
.timeline {
  display: grid;
  gap: 4px;
  padding: 14px 14px 14px 42px;
  position: relative;
  border: 2px solid var(--frame-mid);
  background: var(--panel-fill);
  box-shadow: var(--shadow-sm);
}

/* the rail */
.timeline::before {
  content: "";
  position: absolute;
  left: 20px;
  top: 18px;
  bottom: 18px;
  width: 2px;
  background-image: linear-gradient(180deg, var(--gold) 50%, transparent 50%);
  background-size: 2px 6px;
  opacity: 0.7;
}

.tl-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 14px;
  align-items: baseline;
  padding: 11px 10px;
  position: relative;
  transition: background 0.12s var(--ease-step);
}

.tl-row:hover {
  background: rgb(var(--rgb-gold) / .09);
}

/* the node on the rail */
.tl-row::before {
  content: "";
  position: absolute;
  left: -27px;
  top: 20px;
  width: 8px;
  height: 8px;
  background: var(--window-flat);
  border: 2px solid var(--gold);
  transition: background 0.12s var(--ease-step);
}

.tl-row:hover::before {
  background: var(--gold);
}

.tl-primary {
  font-size: 16px;
  color: var(--ink-bright);
  font-weight: 400;
}

.tl-secondary {
  margin-top: 5px;
  font-size: 15px;
  line-height: 1.7;
  color: var(--ink-soft);
  max-width: 64ch;
}

.tl-meta {
  margin-top: 8px;
  font-family: var(--ui);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.tl-date {
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gold);
  white-space: nowrap;
  text-align: right;
}

/* ------------------------------------------------------------- disclosure */

.career-chevron {
  display: inline-block;
  font-family: var(--ui);
  font-size: 12px;
  color: var(--gold);
  transition: transform 0.16s var(--ease-step);
}

/* ------------------------------------------------------------- simple list */

.simple-list {
  display: grid;
  gap: 4px;
}

.simple-item {
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: 14px;
  align-items: start;
  padding: 10px 10px 10px 12px;
  transition: background 0.12s var(--ease-step);
}

.simple-item:hover {
  background: rgb(var(--rgb-gold) / .08);
}

.simple-icon {
  color: var(--crystal);
  margin-top: 4px;
}

.simple-body {
  display: grid;
  gap: 4px;
}

.simple-category {
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--gold);
}

.simple-title {
  font-size: 16px;
  color: var(--ink-bright);
}

.simple-desc {
  font-size: 15px;
  line-height: 1.7;
  color: var(--ink-soft);
}

/* ------------------------------------------------------------------ about */

.about {
  display: grid;
  gap: 24px;
}

.about-grid {
  display: grid;
  grid-template-columns: 1fr 320px;
  gap: 22px;
  align-items: start;
}

.about-main {
  display: grid;
  gap: 24px;
}

.about-aside {
  display: grid;
  gap: 16px;
  position: sticky;
  top: 86px;
}

.about-card {
  padding: 16px;
}

.cat-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.cat {
  margin: 0;
  display: grid;
  gap: 9px;
  justify-items: center;
}

.cat-icon {
  display: inline-flex;
  color: var(--gold);
}

.cat img {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  display: block;
  border: 2px solid var(--frame-mid);
  background: var(--sunken);
  image-rendering: pixelated;
}

.cat-plate {
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gold);
}

.about-card-body {
  padding-top: 14px;
  font-size: 15px;
  line-height: 1.7;
  color: var(--ink-soft);
}

.about-card-divider {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 2px solid var(--faint);
}

/* A stat line: label left, value right, dotted leader between. */
.fact-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  font-size: 15px;
  border-bottom: 2px dotted rgb(var(--rgb-muted) / .22);
}

.fact-row:last-child {
  border-bottom: 0;
}

.fact-label {
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.fact-value {
  text-align: right;
  color: var(--gold);
}

/* --------------------------------------------------------------- projects */

.projects {
  display: grid;
  gap: 22px;
}

.page-header {
  padding: 26px;
}

.page-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(16px, 2.2vw, 24px);
  line-height: 1.45;
  margin: 12px 0 0;
  color: var(--ink-bright);
  text-shadow: var(--title-shadow-flat);
}

.page-sub {
  margin: 16px 0 0;
  color: var(--ink-soft);
  max-width: 66ch;
  font-size: 16px;
  line-height: 1.75;
}

.projects-section {
  display: grid;
  gap: 14px;
}

.projects-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
}

.project-card {
  display: grid;
  grid-template-rows: auto 1fr;
  color: inherit;
  transition: transform 0.14s var(--ease-step), border-color 0.14s var(--ease-step),
    box-shadow 0.14s var(--ease-step);
}

.project-card:hover {
  transform: translateY(-3px);
  border-color: var(--gold);
  box-shadow: 0 0 0 2px var(--frame-dark), 0 11px 0 rgb(var(--rgb-shade) / .4);
}

/* A lone featured project reads better laid out sideways than stretched
   across the full grid. */
.project-card--wide {
  grid-column: 1 / -1;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  grid-template-rows: auto;
}

.project-card--wide .project-img {
  height: 100%;
  min-height: 280px;
}

.project-card--wide .project-body {
  padding: 24px 26px 26px;
  align-content: center;
}

.project-card--wide .project-name {
  font-size: 15px;
}

.project-media {
  background: var(--sunken);
  line-height: 0;
  height: 100%;
  overflow: hidden;
  border-right: 2px solid rgb(var(--rgb-frame-mid) / .4);
}

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

/* UI screenshots and charts get letterboxed rather than cropped — a sliver of
   a dashboard tells you nothing. */
.project-img--contain {
  object-fit: contain;
  padding: 16px;
  background: var(--mat-fill);
}

.project-body {
  padding: 18px 18px 20px;
  display: grid;
  gap: 12px;
  align-content: start;
}

.project-name {
  font-family: var(--display);
  font-weight: 400;
  font-size: 13px;
  line-height: 1.5;
  color: var(--gold);
  text-shadow: var(--text-shadow);
}

.project-meta {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.project-desc {
  font-size: 15px;
  line-height: 1.7;
  color: var(--ink-soft);
}

.project-links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-self: end;
  padding-top: 4px;
}

.project-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 12px;
  border: 2px solid var(--frame-mid);
  background: var(--sunken);
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-soft);
  transition: border-color 0.12s var(--ease-step), background 0.12s var(--ease-step),
    color 0.12s var(--ease-step);
}

.project-card:hover .project-link,
.project-link:hover {
  border-color: var(--gold);
  background: rgb(var(--rgb-gold) / .14);
  color: var(--ink-bright);
}

.archive {
  padding: 18px 20px;
}

.archive-summary {
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
  color: var(--gold);
  font-family: var(--body); /* see .kicker — this label contains an "&" */
  font-size: 15px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 10px;
}

.archive-summary::-webkit-details-marker {
  display: none;
}

.archive[open] .career-chevron {
  transform: rotate(180deg);
}

.archive-list {
  margin-top: 16px;
  display: grid;
  gap: 8px;
}

.archive-row {
  display: grid;
  gap: 7px;
  padding: 12px 14px;
  color: inherit;
  background: var(--sunken);
  border: 2px solid rgb(var(--rgb-frame-mid) / .35);
  transition: border-color 0.12s var(--ease-step), background 0.12s var(--ease-step);
}

.archive-row:hover {
  border-color: var(--gold);
  background: rgb(var(--rgb-gold) / .09);
}

.archive-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.archive-name {
  font-family: var(--ui);
  font-size: 14px;
  letter-spacing: 0.06em;
  color: var(--ink-bright);
}

.archive-desc {
  font-size: 15px;
  line-height: 1.7;
  color: var(--ink-soft);
}

/* ------------------------------------------------------- article / prose */

.article {
  display: grid;
  gap: 20px;
}

.article-hero {
  overflow: hidden;
  padding: 0;
}

.article-hero img {
  width: 100%;
  display: block;
  max-height: 320px;
  object-fit: cover;
}

/* Defined after the rule above so it wins on source order. */
.article-hero--contain img {
  object-fit: contain;
  max-height: 420px;
  padding: 22px;
  background: var(--mat-fill-soft);
}

.article-head {
  padding: 24px 26px;
}

.article-links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 18px;
}

/* The dialogue box: long-form text still has to be comfortable to read, so
   the body face stays at a generous size and line-height. */
.prose {
  padding: 26px 28px;
  font-size: 17px;
  line-height: 1.85;
  color: var(--ink-soft);
}

.prose > *:first-child {
  margin-top: 0;
}

.prose > *:last-child {
  margin-bottom: 0;
}

.prose p {
  margin: 0 0 20px;
  max-width: 70ch;
}

.prose h2 {
  font-family: var(--display);
  font-weight: 400;
  font-size: 14px;
  line-height: 1.6;
  color: var(--gold);
  margin: 38px 0 16px;
  padding-bottom: 10px;
  border-bottom: 2px solid var(--faint);
  text-shadow: var(--text-shadow);
}

.prose h3 {
  font-family: var(--display);
  font-weight: 400;
  font-size: 12px;
  line-height: 1.6;
  color: var(--crystal);
  margin: 30px 0 12px;
}

.prose h4 {
  font-family: var(--ui);
  font-size: 14px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-bright);
  margin: 24px 0 8px;
}

.prose ul,
.prose ol {
  margin: 0 0 20px;
  padding-left: 24px;
  max-width: 70ch;
}

.prose li {
  margin-bottom: 8px;
}

.prose li::marker {
  color: var(--gold);
}

.prose a {
  color: var(--crystal);
  text-decoration: underline;
  text-decoration-style: dotted;
  text-decoration-thickness: 2px;
  text-decoration-color: rgb(var(--rgb-crystal) / .5);
  text-underline-offset: 4px;
  transition: color 0.14s var(--ease-step), text-decoration-color 0.14s var(--ease-step);
}

.prose a:hover {
  color: var(--gold);
  text-decoration-color: var(--gold);
}

.prose img {
  border: 2px solid var(--frame-mid);
  box-shadow: var(--shadow-sm);
  margin: 6px 0;
  background: var(--sunken);
}

.prose blockquote {
  margin: 0 0 20px;
  padding: 4px 0 4px 18px;
  border-left: 4px solid var(--gold);
  color: var(--muted);
}

.prose code {
  font-family: var(--mono);
  font-size: 0.84em;
  background: var(--sunken);
  border: 2px solid rgb(var(--rgb-frame-mid) / .35);
  padding: 1px 5px;
  color: var(--crystal);
  text-shadow: none;
}

.prose pre {
  background: var(--code-fill);
  color: var(--ink-soft);
  border: 2px solid var(--frame-mid);
  padding: 16px 18px;
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 13.5px;
  line-height: 1.65;
  text-shadow: none;
}

.prose pre code {
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
}

.prose hr {
  border: 0;
  height: 2px;
  background-image: linear-gradient(90deg, var(--frame-mid) 50%, transparent 50%);
  background-size: 6px 2px;
  opacity: 0.6;
  margin: 32px 0;
}

.prose table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 20px;
  font-size: 15px;
  border: 2px solid var(--frame-mid);
}

.prose th,
.prose td {
  text-align: left;
  padding: 10px 12px;
  border-bottom: 2px solid var(--faint);
}

.prose th {
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold);
  background: var(--sunken);
}

/* Side-by-side figures, as used in the project write-ups. They get the full
   column width — text stays at a readable measure, charts don't need to. */
.prose p:has(> img + img) {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 16px;
  align-items: center;
  max-width: none;
  margin-bottom: 26px;
}

.prose p:has(> img) {
  max-width: none;
}

/* -------------------------------------------------------------- 404 page */

.notfound {
  padding: 64px 26px;
  text-align: center;
  display: grid;
  gap: 14px;
  justify-items: center;
}

.notfound-num {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(44px, 9vw, 76px);
  line-height: 1;
  color: var(--gold);
  margin: 0;
  text-shadow: 4px 4px 0 rgb(var(--rgb-shade) / .7);
}

/* ---------------------------------------------------------------- reveal */

/* Sections open the way a menu window does — a stepped wipe out from the
   middle, not a smooth fade. */
.js .reveal {
  opacity: 0;
  clip-path: inset(42% 0 42% 0);
}

.js .reveal.is-visible {
  opacity: 1;
  clip-path: inset(0 0 0 0);
  transition: opacity 0.18s var(--ease-step), clip-path 0.3s var(--ease-step);
}

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

@media (max-width: 980px) {
  .projects-grid {
    grid-template-columns: 1fr;
  }
  /* Badge over copy — side by side it squeezes the sentence to a column. */
  .wip-notice {
    grid-template-columns: 1fr;
    justify-items: start;
    gap: 12px;
  }
  .project-card--wide {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
  }
  .project-card--wide .project-img {
    height: 240px;
    min-height: 0;
  }
  .project-card--wide .project-body {
    padding: 18px 18px 20px;
  }
  .project-media {
    border-right: 0;
    border-bottom: 2px solid rgb(var(--rgb-frame-mid) / .4);
  }
  .about-grid {
    grid-template-columns: 1fr;
  }
  .about-aside {
    position: static;
  }
}

@media (max-width: 860px) {
  .hero-grid {
    grid-template-columns: 1fr;
    justify-items: start;
    gap: 20px;
  }
  .hero {
    padding: 22px;
  }
  .portrait-wrap {
    justify-items: start;
  }
  .portrait {
    width: 150px;
    height: 150px;
  }
}

@media (max-width: 720px) {
  :root {
    --gutter: 14px;
  }
  body {
    font-size: 16px;
  }
  .nav-links {
    gap: 2px;
  }
  .nav-link {
    padding: 8px 6px;
    letter-spacing: 0.06em;
    font-size: 11px;
  }
  .brand-sub {
    display: none;
  }
  .footer-inner {
    font-size: 10px;
    gap: 10px;
  }
  .footer-links {
    gap: 12px;
  }
  .timeline {
    padding-left: 34px;
  }
  .tl-row {
    grid-template-columns: 1fr;
    gap: 4px;
  }
  .tl-date {
    order: -1;
    text-align: left;
  }
  .prose,
  .article-head,
  .page-header {
    padding: 20px 18px;
  }
  .page {
    padding-bottom: 130px;
  }
}

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


/* ==========================================================================
   skyline
   ========================================================================== */

/* ==========================================================================
   10-skyline — the sky the menu windows are open over.

   Fixed behind everything at z-index 0 (.app is 1, the CRT scanline wash is
   100). Back to front: the starfields or the cloud banks, the moon or the sun,
   the comet or the flock, then the horizon — haze, a distant range, near
   foothills, and a castle keeping watch in the left gutter.

   Every sprite is a data-URI SVG on integer coordinates with crispEdges, tiled
   at its natural size so nothing is ever resampled. There is no per-frame
   script and nothing here reflows: the animations touch opacity and transform
   only.

   Both skies ship in the markup and CSS picks one, so the swap is a repaint
   rather than a rebuild — no sprite is fetched or decoded at toggle time.
   Anything that differs between them is a token, declared just below.

   The one live control on the site's chrome lives here too: .sky-toggle, laid
   over the moon/sun sprite. See "the celestial toggle" below for how a button
   inside a fixed backdrop stays clickable.
   ========================================================================== */

/* Backdrop tokens. Kept here rather than with the palette because they are
   scenery — nothing outside this section reads them. */
:root {
  --glow-fill: radial-gradient(circle at 50% 50%,
    rgb(var(--rgb-frame-lit) / .22) 0%,
    rgb(var(--rgb-crystal) / .11) 24%,
    rgb(var(--rgb-window) / .13) 44%,
    rgb(var(--rgb-window) / 0) 68%);
  --haze-fill: linear-gradient(180deg,
    rgb(96 40 120 / 0) 0%,
    rgb(96 40 120 / .13) 46%,
    rgb(var(--rgb-window) / .18) 78%,
    rgb(var(--rgb-window-deep) / .26) 100%);
  --castle-fill: var(--frame-dark);
  --castle-face-opacity: 0;
  --castle-window: var(--gold);
  /* matches the near ridge's fill, so the keep sits *on* the land */
  --castle-ground: var(--frame-dark);
}

:root[data-theme="day"] {
  /* A hotter, tighter bloom — the sun is a light source, not a reflector. */
  --glow-fill: radial-gradient(circle at 50% 50%,
    rgb(255 248 214 / .85) 0%,
    rgb(255 233 150 / .5) 18%,
    rgb(255 214 120 / .22) 38%,
    rgb(255 214 120 / 0) 66%);
  --haze-fill: linear-gradient(180deg,
    rgb(255 236 180 / 0) 0%,
    rgb(255 236 180 / .3) 44%,
    rgb(255 228 168 / .46) 74%,
    rgb(246 214 152 / .58) 100%);
  --castle-fill: #6a6f86;
  --castle-face-opacity: 1;
  --castle-window: #2b3150;
  --castle-ground: #3a6b39;
}

.skyline {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  /* the backdrop is a leaf as far as the rest of the page is concerned */
  contain: layout paint style;
}

/* ------------------------------------------------------------- starfield */

/* The stars thin out toward the horizon, the way haze eats them in every
   16-bit overworld. Masking the whole group keeps the five layers in step. */
.sky-stars {
  position: absolute;
  inset: 0;
  -webkit-mask-image: linear-gradient(
    180deg,
    #000 0%,
    #000 56%,
    rgb(var(--rgb-shade) / .55) 78%,
    rgb(var(--rgb-shade) / .14) 93%,
    transparent 100%
  );
  mask-image: linear-gradient(
    180deg,
    #000 0%,
    #000 56%,
    rgb(var(--rgb-shade) / .55) 78%,
    rgb(var(--rgb-shade) / .14) 93%,
    transparent 100%
  );
}

.sky-field {
  position: absolute;
  inset: 0;
  background-repeat: repeat;
}

/* Tile widths are mutually prime-ish (168 / 233 / 311 / 347 / 419) so the
   combined pattern only truly repeats far outside any viewport — you never
   catch the grid. */

/* The densest layer: ~1400 pixels across a 1440x900 viewport. Held down to
   0.7 so it reads as dust behind the real stars rather than as noise — this
   is the one knob to turn if the sky feels busy. */
.sky-field--far {
  background-size: 168px 168px;
  background-position: 0 0;
  opacity: 0.7;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='168' height='168' viewBox='0 0 168 168' shape-rendering='crispEdges'%3E%3Crect x='12' y='141' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='34' y='75' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='96' y='25' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='6' y='111' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='36' y='45' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='18' y='35' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='88' y='161' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='86' y='7' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='132' y='45' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='10' y='155' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='8' y='121' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='158' y='143' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='68' y='77' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='122' y='139' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='64' y='105' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='62' y='119' width='1' height='1' fill='%23cdd6f7' fill-opacity='.55'/%3E%3Crect x='76' y='93' width='1' height='1' fill='%23cdd6f7' fill-opacity='.34'/%3E%3Crect x='114' y='35' width='1' height='1' fill='%23cdd6f7' fill-opacity='.34'/%3E%3Crect x='46' y='119' width='1' height='1' fill='%23cdd6f7' fill-opacity='.34'/%3E%3Crect x='60' y='69' width='1' height='1' fill='%23cdd6f7' fill-opacity='.34'/%3E%3Crect x='42' y='3' width='1' height='1' fill='%23cdd6f7' fill-opacity='.34'/%3E%3Crect x='8' y='65' width='1' height='1' fill='%23cdd6f7' fill-opacity='.34'/%3E%3Crect x='30' y='55' width='1' height='1' fill='%23cdd6f7' fill-opacity='.34'/%3E%3Crect x='76' y='165' width='1' height='1' fill='%23cdd6f7' fill-opacity='.34'/%3E%3Crect x='114' y='99' width='1' height='1' fill='%23cdd6f7' fill-opacity='.34'/%3E%3Crect x='128' y='1' width='1' height='1' fill='%23cdd6f7' fill-opacity='.34'/%3E%3Crect x='46' y='79' width='1' height='1' fill='%237ef0e0' fill-opacity='.5'/%3E%3Crect x='100' y='125' width='1' height='1' fill='%237ef0e0' fill-opacity='.5'/%3E%3Crect x='34' y='131' width='1' height='1' fill='%237ef0e0' fill-opacity='.5'/%3E%3Crect x='142' y='159' width='1' height='1' fill='%23c79bff' fill-opacity='.45'/%3E%3Crect x='138' y='3' width='1' height='1' fill='%23c79bff' fill-opacity='.45'/%3E%3C/svg%3E");
}

.sky-field--mid {
  background-size: 233px 233px;
  background-position: 61px 37px;
  opacity: 0.85;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='233' height='233' viewBox='0 0 233 233' shape-rendering='crispEdges'%3E%3Crect x='198' y='222' width='1' height='1' fill='%23f2f5ff' fill-opacity='.8'/%3E%3Crect x='189' y='33' width='1' height='1' fill='%23f2f5ff' fill-opacity='.8'/%3E%3Crect x='202' y='169' width='1' height='1' fill='%23f2f5ff' fill-opacity='.8'/%3E%3Crect x='184' y='60' width='1' height='1' fill='%23f2f5ff' fill-opacity='.8'/%3E%3Crect x='93' y='226' width='1' height='1' fill='%23f2f5ff' fill-opacity='.8'/%3E%3Crect x='109' y='168' width='1' height='1' fill='%23f2f5ff' fill-opacity='.8'/%3E%3Crect x='49' y='170' width='1' height='1' fill='%23f2f5ff' fill-opacity='.8'/%3E%3Crect x='208' y='201' width='2' height='2' fill='%23cdd6f7' fill-opacity='.6'/%3E%3Crect x='94' y='111' width='2' height='2' fill='%23cdd6f7' fill-opacity='.6'/%3E%3Crect x='76' y='45' width='2' height='2' fill='%23cdd6f7' fill-opacity='.6'/%3E%3Crect x='130' y='3' width='2' height='2' fill='%23cdd6f7' fill-opacity='.6'/%3E%3Crect x='112' y='41' width='2' height='2' fill='%23cdd6f7' fill-opacity='.6'/%3E%3Crect x='102' y='191' width='2' height='2' fill='%237ef0e0' fill-opacity='.55'/%3E%3Crect x='44' y='45' width='2' height='2' fill='%237ef0e0' fill-opacity='.55'/%3E%3C/svg%3E");
}

.sky-field--near {
  background-size: 311px 311px;
  background-position: 143px 89px;
  opacity: 0.95;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='311' height='311' viewBox='0 0 311 311' shape-rendering='crispEdges'%3E%3Crect x='308' y='31' width='2' height='2' fill='%23f2f5ff' fill-opacity='.9'/%3E%3Crect x='168' y='65' width='2' height='2' fill='%23f2f5ff' fill-opacity='.9'/%3E%3Crect x='306' y='203' width='2' height='2' fill='%23f2f5ff' fill-opacity='.9'/%3E%3Crect x='13' y='174' width='3' height='3' fill='%23f2f5ff' fill-opacity='.75'/%3E%3Crect x='266' y='20' width='3' height='3' fill='%23f2f5ff' fill-opacity='.75'/%3E%3Crect x='30' y='231' width='2' height='2' fill='%23ffd76b' fill-opacity='.6'/%3E%3Crect x='148' y='1' width='2' height='2' fill='%23ffd76b' fill-opacity='.6'/%3E%3C/svg%3E");
}

/* Two sparse layers that actually blink out, on prime-ish periods so the two
   groups never fall into step with each other. */
.sky-field--blink-a {
  background-size: 347px 347px;
  background-position: 23px 191px;
  animation: sky-blink 7s steps(1, end) infinite;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='347' height='347' viewBox='0 0 347 347' shape-rendering='crispEdges'%3E%3Crect x='74' y='203' width='2' height='2' fill='%23f2f5ff' fill-opacity='.95'/%3E%3Crect x='246' y='85' width='2' height='2' fill='%23f2f5ff' fill-opacity='.95'/%3E%3Crect x='228' y='325' width='2' height='2' fill='%23f2f5ff' fill-opacity='.95'/%3E%3Crect x='283' y='42' width='1' height='1' fill='%237ef0e0' fill-opacity='.85'/%3E%3Crect x='76' y='62' width='1' height='1' fill='%237ef0e0' fill-opacity='.85'/%3E%3C/svg%3E");
}

.sky-field--blink-b {
  background-size: 419px 419px;
  background-position: 211px 47px;
  animation: sky-blink 11s steps(1, end) -4.5s infinite;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='419' height='419' viewBox='0 0 419 419' shape-rendering='crispEdges'%3E%3Crect x='2' y='35' width='2' height='2' fill='%23ffd76b' fill-opacity='.8'/%3E%3Crect x='316' y='157' width='2' height='2' fill='%23ffd76b' fill-opacity='.8'/%3E%3Crect x='5' y='150' width='1' height='1' fill='%23f2f5ff' fill-opacity='.9'/%3E%3Crect x='17' y='305' width='1' height='1' fill='%23f2f5ff' fill-opacity='.9'/%3E%3Crect x='324' y='254' width='1' height='1' fill='%23f2f5ff' fill-opacity='.9'/%3E%3C/svg%3E");
}

/* The three dense layers stay still — animating a full-viewport layer costs a
   compositor texture each, so only the two sparse ones move. */
@keyframes sky-blink {
  0%,
  38% {
    opacity: 1;
  }
  40%,
  47% {
    opacity: 0.08;
  }
  49%,
  71% {
    opacity: 0.9;
  }
  73%,
  78% {
    opacity: 0.22;
  }
  80%,
  100% {
    opacity: 1;
  }
}

/* ------------------------------------------------------- the bright ones */

/* A four-point sparkle: one lit pixel with four arms hung off it as hard
   box-shadows. Three pixels across, no anti-aliasing anywhere. */
.sky-sparks {
  position: absolute;
  inset: 0;
}

.sky-spark {
  position: absolute;
  width: 1px;
  height: 1px;
  background: #fff;
  box-shadow:
    -1px 0 0 0 rgb(var(--rgb-gold) / .85),
    1px 0 0 0 rgb(var(--rgb-gold) / .85),
    0 -1px 0 0 rgb(var(--rgb-gold) / .85),
    0 1px 0 0 rgb(var(--rgb-gold) / .85);
  animation: sky-twinkle 5s steps(1, end) infinite;
}

/* every third one is crystal-tinted instead of gold */
.sky-spark:nth-child(3n) {
  box-shadow:
    -1px 0 0 0 rgb(var(--rgb-crystal) / .8),
    1px 0 0 0 rgb(var(--rgb-crystal) / .8),
    0 -1px 0 0 rgb(var(--rgb-crystal) / .8),
    0 1px 0 0 rgb(var(--rgb-crystal) / .8);
}

/* and every fourth grows a longer, fainter pair of arms — five across */
.sky-spark:nth-child(4n + 1) {
  box-shadow:
    -1px 0 0 0 rgb(var(--rgb-gold) / .9),
    1px 0 0 0 rgb(var(--rgb-gold) / .9),
    0 -1px 0 0 rgb(var(--rgb-gold) / .9),
    0 1px 0 0 rgb(var(--rgb-gold) / .9),
    -2px 0 0 0 rgb(var(--rgb-gold) / .4),
    2px 0 0 0 rgb(var(--rgb-gold) / .4),
    0 -2px 0 0 rgb(var(--rgb-gold) / .4),
    0 2px 0 0 rgb(var(--rgb-gold) / .4);
}

/* Scattered by hand in whole pixels — fractional offsets would put a 1px star
   across two device pixels and smear it. Negative delays start each one
   mid-cycle so nothing blinks in unison. */
.sky-spark:nth-child(1)  { left: 92px;   top: 126px; animation-duration: 5.2s;  animation-delay: -0.4s; }
.sky-spark:nth-child(2)  { left: 246px;  top: 212px; animation-duration: 6.7s;  animation-delay: -2.9s; }
.sky-spark:nth-child(3)  { right: 322px; top: 104px; animation-duration: 4.3s;  animation-delay: -1.6s; }
.sky-spark:nth-child(4)  { left: 44px;   top: 338px; animation-duration: 7.9s;  animation-delay: -5.2s; }
.sky-spark:nth-child(5)  { right: 106px; top: 274px; animation-duration: 5.9s;  animation-delay: -0.9s; }
.sky-spark:nth-child(6)  { left: 430px;  top: 98px;  animation-duration: 8.6s;  animation-delay: -3.7s; }
.sky-spark:nth-child(7)  { right: 52px;  top: 436px; animation-duration: 4.9s;  animation-delay: -2.2s; }
.sky-spark:nth-child(8)  { left: 158px;  top: 472px; animation-duration: 6.1s;  animation-delay: -4.4s; }
.sky-spark:nth-child(9)  { right: 392px; top: 346px; animation-duration: 7.3s;  animation-delay: -1.1s; }
.sky-spark:nth-child(10) { left: 628px;  top: 246px; animation-duration: 5.5s;  animation-delay: -3.3s; }
.sky-spark:nth-child(11) { right: 184px; top: 540px; animation-duration: 9.2s;  animation-delay: -6.8s; }
.sky-spark:nth-child(12) { left: 322px;  top: 596px; animation-duration: 6.4s;  animation-delay: -0.2s; }

@keyframes sky-twinkle {
  0%,
  54% {
    opacity: 1;
  }
  56%,
  66% {
    opacity: 0.15;
  }
  68%,
  100% {
    opacity: 0.8;
  }
}

/* --------------------------------------------------------------- clouds */

/* The day counterpart to the starfield: three banks of pixel cumulus on
   mutually prime-ish tiles, drifting left at their own speeds. Each bank is
   one tile wider than the viewport and travels exactly one tile before
   looping, so the seam never lands on screen.

   steps() rather than a linear slide — these move a whole pixel at a time,
   the way a scrolling background layer did when it was made of tiles. The
   step counts equal the tile widths, so every frame is a 1px move. */
.sky-clouds {
  position: absolute;
  inset: 0;
  display: none;
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 62%, rgb(0 0 0 / .5) 84%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 0%, #000 62%, rgb(0 0 0 / .5) 84%, transparent 100%);
}

[data-theme="day"] .sky-clouds {
  display: block;
}

[data-theme="day"] .sky-stars {
  display: none;
}

.sky-bank {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  background-repeat: repeat;
  background-position: 0 0;
  will-change: transform;
}

/* highest and faintest, and the slowest across */
.sky-bank--far {
  width: calc(100% + 640px);
  background-size: 640px 880px;
  opacity: 0.75;
  animation: cloud-drift-far 300s steps(640, end) infinite;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='640' height='880' viewBox='0 0 640 880' shape-rendering='crispEdges'%3E%3Crect x='73' y='60' width='12' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='67' y='63' width='24' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='58' y='66' width='39' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='97' y='66' width='12' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='52' y='69' width='51' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='94' y='69' width='18' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='46' y='72' width='66' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='43' y='75' width='72' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='40' y='78' width='78' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='40' y='81' width='78' height='3' fill='%23a9c9e2' fill-opacity='.45'/%3E%3Crect x='70' y='57' width='18' height='3' fill='%23ffffff' fill-opacity='.62'/%3E%3Crect x='333' y='180' width='12' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='327' y='183' width='24' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='318' y='186' width='39' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='357' y='186' width='12' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='312' y='189' width='51' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='354' y='189' width='18' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='306' y='192' width='66' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='303' y='195' width='72' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='300' y='198' width='78' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='300' y='201' width='78' height='3' fill='%23a9c9e2' fill-opacity='.45'/%3E%3Crect x='330' y='177' width='18' height='3' fill='%23ffffff' fill-opacity='.62'/%3E%3Crect x='503' y='40' width='12' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='497' y='43' width='24' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='488' y='46' width='39' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='527' y='46' width='12' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='482' y='49' width='51' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='524' y='49' width='18' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='476' y='52' width='66' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='473' y='55' width='72' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='470' y='58' width='78' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='470' y='61' width='78' height='3' fill='%23a9c9e2' fill-opacity='.45'/%3E%3Crect x='500' y='37' width='18' height='3' fill='%23ffffff' fill-opacity='.62'/%3E%3Crect x='183' y='300' width='12' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='177' y='303' width='24' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='168' y='306' width='39' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='207' y='306' width='12' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='162' y='309' width='51' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='204' y='309' width='18' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='156' y='312' width='66' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='153' y='315' width='72' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='150' y='318' width='78' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='150' y='321' width='78' height='3' fill='%23a9c9e2' fill-opacity='.45'/%3E%3Crect x='180' y='297' width='18' height='3' fill='%23ffffff' fill-opacity='.62'/%3E%3Crect x='593' y='250' width='12' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='587' y='253' width='24' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='578' y='256' width='39' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='617' y='256' width='12' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='572' y='259' width='51' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='614' y='259' width='18' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='566' y='262' width='66' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='563' y='265' width='72' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='560' y='268' width='78' height='3' fill='%23ffffff' fill-opacity='.5'/%3E%3Crect x='560' y='271' width='78' height='3' fill='%23a9c9e2' fill-opacity='.45'/%3E%3Crect x='590' y='247' width='18' height='3' fill='%23ffffff' fill-opacity='.62'/%3E%3C/svg%3E");
}

.sky-bank--mid {
  width: calc(100% + 820px);
  background-size: 820px 940px;
  opacity: 0.9;
  animation: cloud-drift-mid 220s steps(820, end) infinite;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='820' height='940' viewBox='0 0 820 940' shape-rendering='crispEdges'%3E%3Crect x='114' y='96' width='16' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='106' y='100' width='32' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='94' y='104' width='52' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='146' y='104' width='16' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='86' y='108' width='68' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='142' y='108' width='24' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='78' y='112' width='88' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='74' y='116' width='96' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='70' y='120' width='104' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='70' y='124' width='104' height='4' fill='%23bdd6ea' fill-opacity='.6'/%3E%3Crect x='110' y='92' width='24' height='4' fill='%23fffdf4' fill-opacity='.9'/%3E%3Crect x='474' y='250' width='16' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='466' y='254' width='32' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='454' y='258' width='52' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='506' y='258' width='16' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='446' y='262' width='68' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='502' y='262' width='24' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='438' y='266' width='88' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='434' y='270' width='96' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='430' y='274' width='104' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='430' y='278' width='104' height='4' fill='%23bdd6ea' fill-opacity='.6'/%3E%3Crect x='470' y='246' width='24' height='4' fill='%23fffdf4' fill-opacity='.9'/%3E%3Crect x='684' y='40' width='16' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='676' y='44' width='32' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='664' y='48' width='52' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='716' y='48' width='16' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='656' y='52' width='68' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='712' y='52' width='24' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='648' y='56' width='88' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='644' y='60' width='96' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='640' y='64' width='104' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='640' y='68' width='104' height='4' fill='%23bdd6ea' fill-opacity='.6'/%3E%3Crect x='680' y='36' width='24' height='4' fill='%23fffdf4' fill-opacity='.9'/%3E%3Crect x='294' y='360' width='16' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='286' y='364' width='32' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='274' y='368' width='52' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='326' y='368' width='16' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='266' y='372' width='68' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='322' y='372' width='24' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='258' y='376' width='88' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='254' y='380' width='96' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='250' y='384' width='104' height='4' fill='%23ffffff' fill-opacity='.78'/%3E%3Crect x='250' y='388' width='104' height='4' fill='%23bdd6ea' fill-opacity='.6'/%3E%3Crect x='290' y='356' width='24' height='4' fill='%23fffdf4' fill-opacity='.9'/%3E%3C/svg%3E");
}

.sky-bank--near {
  width: calc(100% + 1080px);
  background-size: 1080px 1020px;
  animation: cloud-drift-near 150s steps(1080, end) infinite;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1080' height='1020' viewBox='0 0 1080 1020' shape-rendering='crispEdges'%3E%3Crect x='156' y='130' width='24' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='144' y='136' width='48' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='126' y='142' width='78' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='204' y='142' width='24' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='114' y='148' width='102' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='198' y='148' width='36' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='102' y='154' width='132' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='96' y='160' width='144' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='90' y='166' width='156' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='90' y='172' width='156' height='6' fill='%23c8daea' fill-opacity='.72'/%3E%3Crect x='150' y='124' width='36' height='6' fill='%23fffbec' fill-opacity='.96'/%3E%3Crect x='626' y='330' width='24' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='614' y='336' width='48' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='596' y='342' width='78' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='674' y='342' width='24' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='584' y='348' width='102' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='668' y='348' width='36' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='572' y='354' width='132' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='566' y='360' width='144' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='560' y='366' width='156' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='560' y='372' width='156' height='6' fill='%23c8daea' fill-opacity='.72'/%3E%3Crect x='620' y='324' width='36' height='6' fill='%23fffbec' fill-opacity='.96'/%3E%3Crect x='896' y='50' width='24' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='884' y='56' width='48' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='866' y='62' width='78' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='944' y='62' width='24' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='854' y='68' width='102' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='938' y='68' width='36' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='842' y='74' width='132' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='836' y='80' width='144' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='830' y='86' width='156' height='6' fill='%23ffffff' fill-opacity='.94'/%3E%3Crect x='830' y='92' width='156' height='6' fill='%23c8daea' fill-opacity='.72'/%3E%3Crect x='890' y='44' width='36' height='6' fill='%23fffbec' fill-opacity='.96'/%3E%3C/svg%3E");
}

@keyframes cloud-drift-far {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-640px, 0, 0); }
}

@keyframes cloud-drift-mid {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-820px, 0, 0); }
}

@keyframes cloud-drift-near {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-1080px, 0, 0); }
}

/* ------------------------------------------------- the moon, and the sun */

/* Both sprites hang in the same 96px slot with one bloom behind them, so the
   swap reads as the same object turning over rather than two things trading
   places. The bloom is a radial gradient sitting *behind* the sprite — the
   sprites themselves stay two or three flat colours each. */
.sky-body {
  position: absolute;
  top: 104px;
  right: 56px;
  width: 96px;
  height: 96px;
}

.sky-glow {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 300px;
  height: 300px;
  margin: -150px 0 0 -150px;
  background: var(--glow-fill);
}

/* 32-unit viewBox rendered at exactly 3x, so every source pixel lands on a
   3x3 block of device pixels with nothing in between. */
.sky-moon-sprite,
.sky-sun-sprite {
  position: absolute;
  inset: 0;
  display: block;
  width: 96px;
  height: 96px;
}

.sky-sun-sprite {
  display: none;
}

[data-theme="day"] .sky-moon-sprite {
  display: none;
}

[data-theme="day"] .sky-sun-sprite {
  display: block;
}

.moon-body {
  fill: var(--frame-lit);
}

.moon-crater {
  fill: var(--frame-mid);
  opacity: 0.5;
}

.sun-body {
  fill: #ffd447;
}

.sun-core {
  fill: #fff6cf;
}

.sun-rays {
  fill: #ffe27a;
}

/* Two rings of rays, alternating on a two-frame cycle. Offsetting the second
   ring by half the period is what makes it read as a shimmer instead of a
   blink — at any moment one ring is out and the other is in. */
.sun-rays--a {
  animation: sun-flare 2.4s steps(1, end) infinite;
}

.sun-rays--b {
  animation: sun-flare 2.4s steps(1, end) -1.2s infinite;
}

@keyframes sun-flare {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0.35; }
}

/* ------------------------------------------------- the celestial toggle */

/* The button that owns day/night, laid exactly over whichever sprite is up.
   Two things make a control inside a fixed backdrop work:

   1. .skyline is pointer-events: none, so the button takes them back.
   2. .app sits above the sky at z-index 1 and its box covers the viewport, so
      it would swallow the click first. Core hands pointer events back to the
      chrome and the content column only (see "structure"), which leaves the
      margins either side of the column — where the moon actually hangs —
      transparent to the pointer.

   The consequence: the sprite is clickable wherever it is clear of the 1080px
   column, and once the column reaches it the nav toggle takes over. That
   handover is the media query at the foot of this section, and it is why the
   nav carries a toggle at all.

   Off by default, on two conditions — a script to wire it to, and room for it.
   The enabling rule is the one media query rather than a `display: none`
   override, because media queries carry no specificity of their own and an
   override would lose to `.js .sky-toggle` at every width.

   Pointer-only by design: the markup takes it out of the tab order and the
   accessibility tree, because the nav toggle is the same command in a place
   the keyboard can always reach. Hence hover states here and no :focus-visible
   ones — there is no focus to style. */
.sky-toggle {
  display: none;
  position: absolute;
  top: 96px;
  right: 48px;
  width: 112px;
  height: 112px;
  padding: 0;
  border: 0;
  background: none;
  pointer-events: auto;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

/* A gold bracket at each corner of the slot — the frame a menu draws around
   whatever is selected. Absent until you point at it. */
.sky-toggle-frame {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background-image:
    linear-gradient(var(--gold), var(--gold)), linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)), linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)), linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)), linear-gradient(var(--gold), var(--gold));
  background-repeat: no-repeat;
  background-size: 12px 3px, 3px 12px, 12px 3px, 3px 12px, 12px 3px, 3px 12px, 12px 3px, 3px 12px;
  background-position:
    0 0, 0 0, 100% 0, 100% 0,
    0 100%, 0 100%, 100% 100%, 100% 100%;
  transition: opacity 0.12s var(--ease-step);
}

/* The label under it names the state you are switching *to*, the way a menu
   names the command rather than the current setting. Text comes from site.js. */
.sky-toggle-hint {
  position: absolute;
  left: 50%;
  top: 100%;
  transform: translate(-50%, 6px);
  padding: 4px 8px;
  white-space: nowrap;
  font-family: var(--ui);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold);
  background: var(--sunken);
  border: 2px solid rgb(var(--rgb-gold) / .5);
  text-shadow: var(--text-shadow-sm);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s var(--ease-step);
}

.sky-toggle:hover .sky-toggle-frame,
.sky-toggle:hover .sky-toggle-hint {
  opacity: 1;
}

/* Pointing at the sky body lifts it a couple of pixels, the way a selected
   sprite does. :has() is the only way to reach backwards from the button to
   the sprite before it; where it is unsupported the bracket alone carries the
   affordance, which is enough. */
.skyline:has(.sky-toggle:hover) .sky-body {
  transform: translateY(-3px);
}

.skyline:has(.sky-toggle:active) .sky-body {
  transform: translateY(1px);
}

.sky-body {
  transition: transform 0.12s var(--ease-step);
}

/* ---------------------------------------------------------- shooting star */

/* Sits invisible for most of a 78-second loop, then crosses in about four
   seconds. steps() makes it hop pixel to pixel rather than glide. */
.sky-comet {
  position: absolute;
  left: -60px;
  top: 132px;
  width: 2px;
  height: 2px;
  background: #fff;
  opacity: 0;
  box-shadow:
    -2px -2px 0 0 rgb(var(--rgb-gold) / .9),
    -4px -4px 0 0 rgb(var(--rgb-gold) / .55),
    -6px -6px 0 0 rgb(var(--rgb-frame-lit) / .38),
    -8px -8px 0 0 rgb(var(--rgb-frame-lit) / .22),
    -10px -10px 0 0 rgb(var(--rgb-frame-lit) / .12);
  animation: sky-comet 78s steps(64, end) infinite;
}

[data-theme="day"] .sky-comet {
  display: none;
}

/* ------------------------------------------------------------------ flock */

/* Three birds in loose formation, each a 2px body with two wings hung off it
   as box-shadows. The wing beat is a hard swap between an up shape and a down
   one — two frames, no tween, which is all a bird ever got at this size. */
.sky-birds {
  position: absolute;
  left: 0;
  top: 176px;
  width: 100%;
  height: 120px;
  display: none;
  color: #4a5570;
}

[data-theme="day"] .sky-birds {
  display: block;
}

.sky-bird {
  position: absolute;
  left: -40px;
  width: 2px;
  height: 2px;
  background: currentColor;
  animation:
    bird-cross 96s steps(96, end) infinite,
    bird-flap 0.6s steps(1, end) infinite;
}

.sky-bird:nth-child(1) { top: 0; animation-delay: 0s, -0.1s; }
.sky-bird:nth-child(2) { top: 26px; animation-delay: -1.4s, -0.3s; }
.sky-bird:nth-child(3) { top: 46px; animation-delay: -2.6s, -0.45s; }

@keyframes bird-flap {
  0%, 49% {
    box-shadow:
      -3px -2px 0 0 currentColor, -6px -3px 0 0 currentColor,
      3px -2px 0 0 currentColor, 6px -3px 0 0 currentColor;
  }
  50%, 100% {
    box-shadow:
      -3px 1px 0 0 currentColor, -6px 2px 0 0 currentColor,
      3px 1px 0 0 currentColor, 6px 2px 0 0 currentColor;
  }
}

/* Out of frame for four fifths of the loop, then a slow drift across and up. */
@keyframes bird-cross {
  0% { transform: translate3d(0, 40px, 0); opacity: 0; }
  3% { opacity: 0.75; }
  76% { opacity: 0.75; }
  80%, 100% { transform: translate3d(112vw, -30px, 0); opacity: 0; }
}

@keyframes sky-comet {
  0%,
  90% {
    transform: translate3d(0, 0, 0);
    opacity: 0;
  }
  90.3% {
    transform: translate3d(0, 0, 0);
    opacity: 1;
  }
  95% {
    transform: translate3d(72vw, 54vh, 0);
    opacity: 1;
  }
  95.8%,
  100% {
    transform: translate3d(80vw, 60vh, 0);
    opacity: 0;
  }
}

/* ---------------------------------------------------------------- horizon */

/* The fixed footer is ~40px tall and nearly opaque, so the ranges are sized to
   stand well clear of it: the far peaks reach ~144px and the castle towers
   ~110px, leaving 70-100px of silhouette reading above the footer bar. */
.sky-horizon {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 260px;
}

.sky-haze {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 260px;
  background: var(--haze-fill);
}

.sky-ridge {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background-repeat: repeat-x;
  background-position: left bottom;
}

/* pale and tall, the range you never reach */
.sky-ridge--far {
  height: 176px;
  background-size: 480px 176px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='176' viewBox='0 0 480 176' shape-rendering='crispEdges'%3E%3Cpolygon points='0,176 0,97 8,97 8,93 16,93 16,88 24,88 24,84 32,84 32,80 40,80 40,75 48,75 48,79 56,79 56,85 64,85 64,92 72,92 72,90 80,90 80,84 88,84 88,78 96,78 96,72 104,72 104,67 112,67 112,61 120,61 120,55 128,55 128,49 136,49 136,43 144,43 144,37 152,37 152,36 160,36 160,40 168,40 168,43 176,43 176,47 184,47 184,51 192,51 192,54 200,54 200,58 208,58 208,62 216,62 216,65 224,65 224,69 232,69 232,73 240,73 240,76 248,76 248,80 256,80 256,79 264,79 264,76 272,76 272,72 280,72 280,69 288,69 288,65 296,65 296,62 304,62 304,58 312,58 312,55 320,55 320,54 328,54 328,61 336,61 336,67 344,67 344,74 352,74 352,81 360,81 360,88 368,88 368,95 376,95 376,102 384,102 384,109 392,109 392,111 400,111 400,106 408,106 408,100 416,100 416,97 424,97 424,101 432,101 432,105 440,105 440,110 448,110 448,114 456,114 456,108 464,108 464,103 472,103 472,97 480,97 480,176' fill='%231b2666' fill-opacity='.7'/%3E%3C/svg%3E");
}

/* near-black foothills the castle stands on */
.sky-ridge--near {
  height: 96px;
  background-size: 384px 96px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='384' height='96' viewBox='0 0 384 96' shape-rendering='crispEdges'%3E%3Cpolygon points='0,96 0,75 4,75 4,72 8,72 8,68 12,68 12,65 16,65 16,61 20,61 20,57 24,57 24,54 28,54 28,55 32,55 32,60 36,60 36,66 40,66 40,71 44,71 44,76 48,76 48,81 52,81 52,76 56,76 56,72 60,72 60,67 64,67 64,63 68,63 68,59 72,59 72,54 76,54 76,50 80,50 80,45 84,45 84,41 88,41 88,37 92,37 92,32 96,32 96,31 100,31 100,34 104,34 104,37 108,37 108,40 112,40 112,43 116,43 116,46 120,46 120,49 124,49 124,52 128,52 128,54 132,54 132,57 136,57 136,60 140,60 140,63 144,63 144,66 148,66 148,69 152,69 152,72 156,72 156,72 160,72 160,67 164,67 164,61 168,61 168,60 172,60 172,63 176,63 176,67 180,67 180,70 184,70 184,74 188,74 188,73 192,73 192,70 196,70 196,67 200,67 200,64 204,64 204,61 208,61 208,58 212,58 212,55 216,55 216,52 220,52 220,48 224,48 224,45 228,45 228,42 232,42 232,39 236,39 236,36 240,36 240,41 244,41 244,46 248,46 248,51 252,51 252,56 256,56 256,61 260,61 260,66 264,66 264,71 268,71 268,76 272,76 272,81 276,81 276,82 280,82 280,78 284,78 284,74 288,74 288,70 292,70 292,66 296,66 296,62 300,62 300,58 304,58 304,54 308,54 308,50 312,50 312,51 316,51 316,56 320,56 320,62 324,62 324,68 328,68 328,73 332,73 332,79 336,79 336,84 340,84 340,90 344,90 344,90 348,90 348,90 352,90 352,90 356,90 356,90 360,90 360,90 364,90 364,90 368,90 368,90 372,90 372,85 376,85 376,80 380,80 380,75 384,75 384,96' fill='%2305060f' fill-opacity='.95'/%3E%3C/svg%3E");
}

/* Parked in the left gutter — at 1440px the content column leaves ~180px of
   margin, so the keep sits clear of everything. 48-unit viewBox at 3x. */
.sky-castle {
  position: absolute;
  left: 28px;
  bottom: 0;
  display: block;
  width: 144px;
  height: 120px;
}

.castle-body {
  fill: var(--castle-fill);
}

/* The sunlit face, drawn over the body's left edge. Zero-opacity at night,
   where the keep is one flat silhouette. */
.castle-face {
  fill: #9aa0b8;
  opacity: var(--castle-face-opacity);
}

.castle-ground {
  fill: var(--castle-ground);
}

.castle-lit {
  fill: var(--castle-window);
}

/* Somebody is still awake up there. */
.castle-lit--flicker {
  animation: sky-window 6.5s steps(1, end) infinite;
}

.castle-lit--flicker-b {
  animation: sky-window 9.5s steps(1, end) -3.2s infinite;
}

@keyframes sky-window {
  0%,
  82% {
    opacity: 1;
  }
  84%,
  88% {
    opacity: 0.4;
  }
  90%,
  100% {
    opacity: 1;
  }
}

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

@media (max-width: 980px) {
  /* Half-size sprite — still an exact 2x of the 32-unit grid. */
  .sky-body {
    top: 88px;
    right: 40px;
    width: 64px;
    height: 64px;
  }
  .sky-moon-sprite,
  .sky-sun-sprite {
    width: 64px;
    height: 64px;
  }
  .sky-glow {
    width: 208px;
    height: 208px;
    margin: -104px 0 0 -104px;
  }
  /* 2x castle, tucked into the corner so it can't crowd the column. */
  .sky-castle {
    left: 8px;
    width: 96px;
    height: 80px;
  }
}

@media (max-width: 720px) {
  /* The far range carries too much weight on a short viewport. 144px is the
     shortest the box can be without shearing the tallest summit (142px) —
     everything cropped above that is empty tile. */
  .sky-ridge--far {
    height: 144px;
    opacity: 0.75;
  }
  .sky-horizon,
  .sky-haze {
    height: 200px;
  }
  /* Pull the four sparks that would fall off a 375px screen back inside. */
  .sky-spark:nth-child(6)  { left: 300px; right: auto; top: 108px; }
  .sky-spark:nth-child(9)  { right: 36px; left: auto;  top: 352px; }
  .sky-spark:nth-child(10) { left: 206px; right: auto; top: 262px; }
  .sky-spark:nth-child(11) { right: 60px; left: auto;  top: 528px; }
  .sky-spark:nth-child(12) { left: 118px; right: auto; top: 580px; }
}

@media (prefers-reduced-motion: reduce) {
  /* Core already clamps every animation; make it explicit here so nothing is
     left frozen mid-blink. Base styles are the resting state. */
  .sky-field,
  .sky-spark,
  .sky-comet,
  .castle-lit {
    animation: none !important;
  }
}

/* ------------------------------------------------------------ day horizon */

/* Same silhouettes, repainted: the far range goes to a hazed blue and the
   foothills the castle stands on turn green. The castle's own windows are
   dark by day — nobody lights a lamp at noon — so the flicker stops too. */
[data-theme="day"] .sky-ridge--far {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='176' viewBox='0 0 480 176' shape-rendering='crispEdges'%3E%3Cpolygon points='0,176 0,97 8,97 8,93 16,93 16,88 24,88 24,84 32,84 32,80 40,80 40,75 48,75 48,79 56,79 56,85 64,85 64,92 72,92 72,90 80,90 80,84 88,84 88,78 96,78 96,72 104,72 104,67 112,67 112,61 120,61 120,55 128,55 128,49 136,49 136,43 144,43 144,37 152,37 152,36 160,36 160,40 168,40 168,43 176,43 176,47 184,47 184,51 192,51 192,54 200,54 200,58 208,58 208,62 216,62 216,65 224,65 224,69 232,69 232,73 240,73 240,76 248,76 248,80 256,80 256,79 264,79 264,76 272,76 272,72 280,72 280,69 288,69 288,65 296,65 296,62 304,62 304,58 312,58 312,55 320,55 320,54 328,54 328,61 336,61 336,67 344,67 344,74 352,74 352,81 360,81 360,88 368,88 368,95 376,95 376,102 384,102 384,109 392,109 392,111 400,111 400,106 408,106 408,100 416,100 416,97 424,97 424,101 432,101 432,105 440,105 440,110 448,110 448,114 456,114 456,108 464,108 464,103 472,103 472,97 480,97 480,176' fill='%237e9ec9' fill-opacity='.78'/%3E%3C/svg%3E");
}

[data-theme="day"] .sky-ridge--near {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='384' height='96' viewBox='0 0 384 96' shape-rendering='crispEdges'%3E%3Cpolygon points='0,96 0,75 4,75 4,72 8,72 8,68 12,68 12,65 16,65 16,61 20,61 20,57 24,57 24,54 28,54 28,55 32,55 32,60 36,60 36,66 40,66 40,71 44,71 44,76 48,76 48,81 52,81 52,76 56,76 56,72 60,72 60,67 64,67 64,63 68,63 68,59 72,59 72,54 76,54 76,50 80,50 80,45 84,45 84,41 88,41 88,37 92,37 92,32 96,32 96,31 100,31 100,34 104,34 104,37 108,37 108,40 112,40 112,43 116,43 116,46 120,46 120,49 124,49 124,52 128,52 128,54 132,54 132,57 136,57 136,60 140,60 140,63 144,63 144,66 148,66 148,69 152,69 152,72 156,72 156,72 160,72 160,67 164,67 164,61 168,61 168,60 172,60 172,63 176,63 176,67 180,67 180,70 184,70 184,74 188,74 188,73 192,73 192,70 196,70 196,67 200,67 200,64 204,64 204,61 208,61 208,58 212,58 212,55 216,55 216,52 220,52 220,48 224,48 224,45 228,45 228,42 232,42 232,39 236,39 236,36 240,36 240,41 244,41 244,46 248,46 248,51 252,51 252,56 256,56 256,61 260,61 260,66 264,66 264,71 268,71 268,76 272,76 272,81 276,81 276,82 280,82 280,78 284,78 284,74 288,74 288,70 292,70 292,66 296,66 296,62 300,62 300,58 304,58 304,54 308,54 308,50 312,50 312,51 316,51 316,56 320,56 320,62 324,62 324,68 328,68 328,73 332,73 332,79 336,79 336,84 340,84 340,90 344,90 344,90 348,90 348,90 352,90 352,90 356,90 356,90 360,90 360,90 364,90 364,90 368,90 368,90 372,90 372,85 376,85 376,80 380,80 380,75 384,75 384,96' fill='%233a6b39' fill-opacity='.97'/%3E%3C/svg%3E");
}

[data-theme="day"] .castle-lit {
  animation: none;
  opacity: 0.9;
}

/* Below roughly 1320px the 1080px content column reaches into the sprite's
   corner. It then reads as a hard shape half-swallowed by a menu window — a
   glitch rather than a scene — so it steps back to a dim glow that sits behind
   the UI the way the rest of the sky does. */
@media (max-width: 1320px) {
  .sky-body {
    opacity: 0.5;
  }
}

/* The column reaches the sprite's outer edge at 1080 + 2*152 = 1384px, and
   swallows it entirely a little under 1300. Between those two the sprite is
   half clickable, which is worse than not clickable at all — so the handover
   to the nav toggle happens at the top of that band, while the whole sprite is
   still in the clear. Below it the sprite is scenery again: no pointer, no
   focus stop, no bracket. */
@media (min-width: 1385px) {
  .js .sky-toggle {
    display: block;
  }
}

/* Reduced motion: the sky holds still. Base styles are the resting state, so
   the clouds park where they start and the flock stays off frame. */
@media (prefers-reduced-motion: reduce) {
  .sky-bank,
  .sky-bird,
  .sun-rays {
    animation: none !important;
  }
  .sky-bird {
    display: none;
  }
}


/* ==========================================================================
   chrome
   ========================================================================== */

/* ==========================================================================
   20-chrome — site chrome.

   Top bar: the command menu. A hard-bevelled window edge pinned to the top of
   the screen, with the three page links sitting inside their own little
   command window. Point at one and the gold cursor snaps beside it; the page
   you are on keeps the cursor lit.

   Bottom bar: the status HUD. A slim strip that has to stay readable over the
   skyline behind it, so it is near-opaque with a lit top bevel and a gold
   pixel rail.
   ========================================================================== */

/* ------------------------------------------------------------------ navbar */

/* No soft shadow — a lit bevel at the top, a rail and a black line at the
   bottom, the way a menu window's top edge is drawn. */
.navbar {
  border-bottom: 2px solid var(--frame-dark);
  background: var(--navbar-fill);
  box-shadow:
    inset 0 2px 0 rgb(var(--rgb-frame-lit) / .5),
    inset 0 -2px 0 rgb(var(--rgb-frame-mid) / .9),
    0 2px 0 rgb(var(--rgb-shade) / .6),
    0 5px 0 rgb(var(--rgb-shade) / .22);
}

.nav-inner {
  gap: 14px;
  padding: 10px 0 11px;
}

/* --------------------------------------------------------- the nameplate */

/* The nameplate is the only part allowed to give up room — the command
   window keeps its full width so the menu never overflows the screen. */
.brand {
  flex-direction: row;
  align-items: center;
  gap: 9px;
  min-width: 0;
  flex: 0 1 auto;
}

/* The crest sits in its own slot, like a party member's portrait frame. */
.brand-crest {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border: 2px solid rgb(var(--rgb-frame-mid) / .8);
  background: var(--sunken);
  box-shadow: 0 0 0 2px var(--frame-dark);
  transition: border-color 0.14s var(--ease-step), background 0.14s var(--ease-step);
}

.crest {
  display: block;
  width: 16px;
  height: 16px;
}

.crest-edge {
  fill: var(--gold-deep);
}

.crest-core {
  fill: var(--gold);
}

.crest-glint {
  fill: var(--frame-lit);
}

.brand-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.brand-title,
.brand-sub {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.brand-title {
  transition: color 0.14s var(--ease-step);
}

.brand-sub {
  font-size: 9px;
  letter-spacing: 0.2em;
  color: var(--gold);
  text-shadow: var(--text-shadow-sm);
}

.brand:hover .brand-crest,
.brand:focus-visible .brand-crest {
  border-color: var(--gold);
  background: rgb(var(--rgb-gold) / .14);
}

.brand:hover .brand-title,
.brand:focus-visible .brand-title {
  color: var(--gold);
}

/* Two frames of sparkle, not a fade. */
.brand:hover .crest-glint {
  animation: crest-sparkle 0.5s steps(2, end) infinite;
}

@keyframes crest-sparkle {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.2;
  }
}

/* ------------------------------------------------------ the command window */

.nav-links {
  position: relative;
  flex: 0 0 auto;
  gap: 2px;
  padding: 3px;
  border: 2px solid var(--frame-mid);
  background: var(--cmd-fill);
  box-shadow:
    inset 0 0 0 1px rgb(var(--rgb-well) / .7),
    0 3px 0 rgb(var(--rgb-shade) / .42);
}

/* Gold studs pinned to the window's four corners — the same trick .surface
   uses, at a smaller size to suit a 40px-tall box. */
.nav-links::before {
  content: "";
  position: absolute;
  inset: -2px;
  pointer-events: none;
  background-image:
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold));
  background-size: 3px 3px;
  background-repeat: no-repeat;
  background-position: 0 0, 100% 0, 0 100%, 100% 100%;
}

/* A command row, not a button: the left padding is the cursor's gutter, and
   selection is a lit band rather than an outline. */
.nav-link {
  padding: 8px 12px 8px 24px;
  border-color: transparent;
  letter-spacing: 0.12em;
  white-space: nowrap;
  text-shadow: var(--text-shadow-sm);
}

/* Reposition the shared .has-cursor pointer into that gutter so it never
   lands on the glyphs. Everything else about it comes from core. */
.nav-link::after {
  left: 8px;
  width: 8px;
  height: 10px;
  margin-top: -5px;
}

.nav-link:hover,
.nav-link:focus-visible {
  background: rgb(var(--rgb-gold) / .16);
  border-color: transparent;
  color: var(--ink-bright);
}

.nav-link.is-active {
  color: var(--gold);
  background: rgb(var(--rgb-gold) / .13);
  border-color: transparent;
  box-shadow: inset 0 -2px 0 rgb(var(--rgb-gold-deep) / .9);
}

.nav-link.is-active:hover {
  background: rgb(var(--rgb-gold) / .2);
  color: var(--gold);
}

/* ----------------------------------------------------------- theme toggle */

/* Drawn as an equipment slot rather than a button — the same sunken well and
   hard black ring the brand crest sits in, so it reads as part of the HUD
   instead of a widget bolted onto it. It carries the sun by night and the
   moon by day: what you would be switching to, matching the label. */
.nav-toggle {
  position: relative;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  color: var(--gold);
  border: 2px solid rgb(var(--rgb-frame-mid) / .8);
  background: var(--sunken);
  box-shadow: 0 0 0 2px var(--frame-dark), 0 2px 0 rgb(var(--rgb-shade) / .4);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  transition: color 0.12s var(--ease-step), border-color 0.12s var(--ease-step),
    background 0.12s var(--ease-step), transform 0.12s var(--ease-step),
    box-shadow 0.12s var(--ease-step);
}

.nav-toggle-glyph {
  display: block;
  line-height: 0;
}

.nav-toggle .icon {
  width: 16px;
  height: 16px;
}

.nav-toggle:hover,
.nav-toggle:focus-visible {
  transform: translateY(-2px);
  border-color: var(--gold);
  background: rgb(var(--rgb-gold) / .16);
  box-shadow: 0 0 0 2px var(--frame-dark), 0 4px 0 rgb(var(--rgb-shade) / .45);
}

.nav-toggle:active {
  transform: translateY(1px);
  box-shadow: 0 0 0 2px var(--frame-dark), 0 1px 0 rgb(var(--rgb-shade) / .4);
}

/* --------------------------------------------------------------- status HUD */

/* Near-opaque so the skyline behind it never eats the text, with the white
   menu bevel along the top edge. */
.footer {
  border-top: 2px solid rgb(var(--rgb-frame-lit) / .72);
  background: var(--footer-fill);
  box-shadow:
    inset 0 2px 0 rgb(var(--rgb-window) / .9),
    0 -3px 0 rgb(var(--rgb-shade) / .5);
}

/* A dashed gold rail tucked under the bevel — the HUD divider. */
.footer::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 2px;
  pointer-events: none;
  background-image: linear-gradient(90deg, rgb(var(--rgb-gold) / .5) 50%, transparent 50%);
  background-size: 8px 2px;
}

.footer-inner {
  padding: 9px 0 10px;
  font-size: 10px;
  letter-spacing: 0.1em;
}

.footer-status {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  flex: 0 1 auto;
  white-space: nowrap;
}

/* The indicator lamp: a gold square with a punched-out dark centre. */
.footer-pip {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  background: var(--gold);
  box-shadow: inset 0 0 0 2px rgb(var(--rgb-frame-dark) / .85);
}

.footer-text {
  overflow: hidden;
  text-overflow: ellipsis;
  text-shadow: var(--text-shadow);
}

.footer-links {
  flex: 0 0 auto;
  gap: 18px;
}

.footer-link {
  position: relative;
  font-family: var(--ui);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-soft);
  white-space: nowrap;
  padding: 2px 0;
  text-shadow: var(--text-shadow);
}

/* A 3px gold square in the gap instead of empty space. */
.footer-link + .footer-link::before {
  content: "";
  position: absolute;
  left: -10px;
  top: 50%;
  margin-top: -1px;
  width: 3px;
  height: 3px;
  background: var(--gold-deep);
  pointer-events: none;
}

/* Selection underscores the entry, the way a HUD marks the live slot. */
.footer-link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.14s var(--ease-step);
  pointer-events: none;
}

.footer-link:hover,
.footer-link:focus-visible {
  color: var(--gold);
}

.footer-link:hover::after,
.footer-link:focus-visible::after {
  transform: scaleX(1);
}

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

@media (max-width: 720px) {
  .nav-inner {
    gap: 10px;
    padding: 9px 0 10px;
  }
  .brand {
    gap: 7px;
  }
  .brand-crest {
    width: 18px;
    height: 18px;
  }
  .crest {
    width: 14px;
    height: 14px;
  }
  .brand-title {
    font-size: 11px;
  }
  .nav-links {
    gap: 0;
    padding: 2px;
  }
  .nav-link {
    padding: 8px 7px 8px 17px;
    font-size: 11px;
    letter-spacing: 0.06em;
  }
  .nav-link::after {
    left: 5px;
    width: 7px;
    height: 9px;
    margin-top: -4px;
  }
  .nav-toggle {
    width: 27px;
    height: 27px;
  }
  .footer-inner {
    padding: 8px 0 9px;
  }
  .footer-links {
    gap: 15px;
  }
  .footer-link + .footer-link::before {
    left: -9px;
  }
}

/* Phone widths: everything gets one notch tighter so the menu stays on a
   single line at 375px. */
@media (max-width: 420px) {
  .nav-inner {
    gap: 8px;
  }
  .brand {
    gap: 6px;
  }
  .brand-title {
    font-size: 10px;
  }
  .nav-link {
    padding: 7px 5px 7px 14px;
    font-size: 10px;
    letter-spacing: 0.04em;
  }
  .nav-link::after {
    left: 4px;
    width: 6px;
    height: 8px;
    margin-top: -4px;
  }
  /* Down to a 14px glyph in a 24px slot — the smallest the sun reads at
     before its rays close up into the disc. */
  .nav-toggle {
    width: 24px;
    height: 24px;
  }
  .nav-toggle .icon {
    width: 14px;
    height: 14px;
  }
  /* The toggle costs this row about 32px, and .brand is the flex item that
     gives way — without this the wordmark clipped to "Eric V…" from 414px
     down. The crest is the cheapest 24px to give up (the name is the site's
     identity and a gem beside it is not), and another 9 comes off the command
     gutters, which still leave the cursor 2px clear of the glyphs. */
  .brand-crest {
    display: none;
  }
  .nav-link {
    padding: 7px 4px 7px 12px;
  }
  .footer-pip {
    display: none;
  }
  .footer-status {
    gap: 0;
  }
  .footer-link {
    font-size: 9px;
    letter-spacing: 0.1em;
  }
  .footer-links {
    gap: 13px;
  }
  .footer-link + .footer-link::before {
    left: -8px;
  }
}

/* Below about 340px the three commands alone want 199px of a 292px row, and
   the wordmark cannot have the 90px it needs however hard the gutters are
   squeezed. So the row stops being a row: brand on one line, commands and
   toggle beneath it, both centred. The nav is a few pixels taller on a phone
   from 2016 and the name is whole, which is the better trade — the old layout
   clipped it at this width too, just less. */
@media (max-width: 340px) {
  .nav-inner {
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 7px;
  }
  .brand {
    flex: 0 0 auto;
  }
}

@media (max-width: 360px) {
  .footer-inner {
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 3px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .brand:hover .crest-glint {
    animation: none;
  }
}


/* ==========================================================================
   home
   ========================================================================== */

/* ==========================================================================
   30-home — the landing page as a character status window.

   Three pieces:
     1. .status-window  — portrait in a sprite slot, name on a nameplate bar.
     2. .crest          — the gold rail + crystals that replaces the old sprig.
     3. .simple-list    — the ability list (shared with the About page, so
                          those rules stay generic; home-only rules are
                          scoped under .home).
   ========================================================================== */

/* ------------------------------------------------- 1. the status window */

.home .status-window {
  padding: 28px 28px 30px;
}

/* Wider left column so the portrait's slot frame has room to breathe. */
.home .hero-grid {
  grid-template-columns: 244px 1fr;
  gap: 30px;
}

/* The sprite slot: a sunken well with a faint checker weave showing through
   the margin around the portrait, the way an empty equipment square looks
   before you drop something into it. */
.home .sprite-slot {
  padding: 12px;
  justify-self: center;
  border: 2px solid var(--frame-mid);
  background-color: rgb(var(--rgb-well) / .72);
  background-image:
    linear-gradient(45deg, rgb(var(--rgb-frame-mid) / .14) 25%, transparent 25% 75%, rgb(var(--rgb-frame-mid) / .14) 75%),
    linear-gradient(45deg, rgb(var(--rgb-frame-mid) / .14) 25%, transparent 25% 75%, rgb(var(--rgb-frame-mid) / .14) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
  box-shadow:
    inset 0 0 0 2px rgb(var(--rgb-well) / .8),
    0 0 0 2px var(--frame-dark),
    0 5px 0 rgb(var(--rgb-shade) / .4);
  transition: border-color 0.14s var(--ease-step);
}

/* Four gold rivets pinned into the slot's corners — one element, four
   background layers, same trick core uses on .surface. */
.home .sprite-slot::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: 2;
  pointer-events: none;
  background-image:
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold));
  background-size: 4px 4px;
  background-repeat: no-repeat;
  background-position: 0 0, 100% 0, 0 100%, 100% 100%;
}

.home .sprite-slot .portrait {
  position: relative;
  z-index: 1;
  box-shadow: 0 0 0 2px var(--frame-dark);
}

.home .sprite-slot:hover {
  border-color: var(--gold);
}

/* ---------------------------------------------------------- nameplate */

/* The bar the character's name sits on: a shallow sunken plate with a lit
   top edge, a hard bottom shadow, and gold brackets on opposite corners. */
.home .nameplate {
  position: relative;
  display: block;
  width: -moz-fit-content;
  width: fit-content;
  max-width: 100%;
  padding: 12px 22px 15px 20px;
  background: var(--nameplate-fill);
  border-top: 2px solid rgb(var(--rgb-frame-lit) / .3);
  border-bottom: 2px solid rgb(var(--rgb-frame-dark) / .8);
}

.home .nameplate::after,
.home .nameplate::before {
  content: "";
  position: absolute;
  width: 16px;
  height: 16px;
  pointer-events: none;
  background-image:
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold));
  background-repeat: no-repeat;
  background-size: 16px 2px, 2px 16px;
}

.home .nameplate::after {
  top: -2px;
  right: -2px;
  background-position: 100% 0, 100% 0;
}

.home .nameplate::before {
  bottom: -2px;
  left: -2px;
  background-position: 0 100%, 0 100%;
}

/* The kicker reads as the class line above the name, so it gets a menu
   bullet in front of it. */
.home .hero-class {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 11.5px;
  letter-spacing: 0.2em;
}

.home .hero-class::before {
  content: "";
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  background: var(--gold);
  box-shadow: 0 0 0 2px rgb(var(--rgb-frame-dark) / .65);
}

.home .hero-name {
  margin-top: 10px;
}

.home .status-window .lede {
  margin-top: 20px;
}

/* --------------------------------------------------- 2. the crest rail */

.home .crest {
  display: block;
  width: 100%;
  max-width: 384px;
  height: auto;
  margin: 24px 0 2px;
  overflow: visible;
}

.home .crest-rail {
  fill: var(--gold-deep);
  transform-box: fill-box;
  transform-origin: center;
  animation: crest-open 0.45s var(--ease-step) both;
}

.home .crest-caps {
  fill: var(--gold);
  animation: crest-fade 0.2s steps(2, end) both;
  animation-delay: 0.44s;
}

.home .crest-gem {
  transform-box: fill-box;
  transform-origin: center;
  animation: crest-gem-pop 0.28s steps(4, end) both;
}

.home .crest-gem--crystal {
  fill: var(--crystal);
}

.home .crest-gem--magic {
  fill: var(--magic);
}

.home .crest-gem--gold {
  fill: var(--gold);
}

.home .crest-glint {
  fill: #fff;
  opacity: 0.85;
}

@keyframes crest-open {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

@keyframes crest-fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes crest-gem-pop {
  0% {
    opacity: 0;
    transform: scale(0);
  }
  60% {
    opacity: 1;
    transform: scale(1.45);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

/* ------------------------------------------------------ the dialogue box */

/* Body copy inside the intro window reads as the dialogue line: generous
   leading, the opening paragraph in the brighter ink. */
.home .copy {
  max-width: 68ch;
  font-size: 16.5px;
}

.home .copy p {
  line-height: 1.85;
}

.home .copy p:first-child {
  color: var(--ink);
}

/* ---------------------------------------------- the command menu row */

/* The social links become a row of menu commands, fenced off from the
   dialogue above by the same pixel rail core uses for .rule. */
.home-links {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 24px;
  padding-top: 22px;
}

.home-links::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 2px;
  background-image: linear-gradient(90deg, var(--frame-mid) 50%, transparent 50%);
  background-size: 6px 2px;
  opacity: 0.6;
}

/* A selectable command slot. The left gutter is reserved for the cursor so
   nothing shifts when it appears. */
.cmd-link {
  padding: 9px 14px 9px 28px;
  border: 2px solid var(--frame-mid);
  background: var(--sunken);
  color: var(--ink-soft);
  font-size: 11.5px;
  letter-spacing: 0.12em;
  box-shadow: 0 3px 0 rgb(var(--rgb-shade) / .4);
  transition: transform 0.12s var(--ease-step), border-color 0.12s var(--ease-step),
    color 0.12s var(--ease-step), background 0.12s var(--ease-step),
    box-shadow 0.12s var(--ease-step);
}

.cmd-link .icon {
  color: var(--crystal);
  transition: color 0.12s var(--ease-step);
}

.cmd-link:hover,
.cmd-link:focus-visible {
  transform: translateY(-2px);
  background: rgb(var(--rgb-gold) / .14);
  border-color: var(--gold);
  color: var(--ink-bright);
  box-shadow: 0 5px 0 rgb(var(--rgb-shade) / .45);
}

.cmd-link:hover .icon,
.cmd-link:focus-visible .icon {
  color: var(--gold);
}

.cmd-link:active {
  transform: translateY(1px);
  box-shadow: 0 1px 0 rgb(var(--rgb-shade) / .4);
}

.cmd-link.has-cursor::after {
  left: 9px;
}

.home .section-head {
  align-items: center;
}

/* --------------------------------------------- 3. the ability list rows */

/* Shared with the About page's skills section — keep these generic. The
   markup contract is only what core already assumes: an optional
   .simple-icon, then .simple-body. */

.simple-list {
  display: grid;
  gap: 6px;
}

.simple-item {
  position: relative;
  grid-template-columns: 34px minmax(0, 1fr);
  gap: 14px;
  padding: 12px 14px 13px 28px;
  background: var(--sunken);
  border: 2px solid rgb(var(--rgb-frame-mid) / .32);
  box-shadow: inset 0 2px 0 rgb(var(--rgb-frame-lit) / .06);
  transition: background 0.12s var(--ease-step), border-color 0.12s var(--ease-step),
    box-shadow 0.12s var(--ease-step);
}

.simple-item:hover,
.simple-item:focus-within {
  background: rgb(var(--rgb-gold) / .1);
  border-color: var(--gold);
  box-shadow: inset 0 2px 0 rgb(var(--rgb-gold) / .14);
}

/* Align the cursor with the category label rather than the row's midpoint —
   these rows run two or three lines tall. */
.simple-item.has-cursor::after {
  left: 9px;
  top: 22px;
  margin-top: 0;
}

/* The icon gets its own framed slot, like an ability's element badge. */
.simple-icon {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  margin-top: 0;
  color: var(--crystal);
  border: 2px solid var(--frame-mid);
  background: var(--badge-fill);
  box-shadow: inset 0 0 0 1px rgb(var(--rgb-frame-dark) / .7), 0 2px 0 rgb(var(--rgb-shade) / .4);
  transition: color 0.12s var(--ease-step), border-color 0.12s var(--ease-step),
    background 0.12s var(--ease-step);
}

.simple-icon .icon {
  width: 18px;
  height: 18px;
}

.simple-item:hover .simple-icon,
.simple-item:focus-within .simple-icon {
  color: var(--gold);
  border-color: var(--gold);
  background: rgb(var(--rgb-gold) / .16);
}

.simple-body {
  gap: 6px;
  padding-top: 1px;
}

.simple-category {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-shadow: var(--text-shadow);
}

.simple-desc {
  max-width: 68ch;
}

/* The rows carry their own frames, so the window around them sits tight. */
.home .section .panel {
  padding: 14px;
}

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

@media (max-width: 860px) {
  .home .hero-grid {
    grid-template-columns: 1fr;
    gap: 22px;
  }
  .home .status-window {
    padding: 22px 22px 24px;
  }
  .home .sprite-slot {
    justify-self: start;
    padding: 10px;
  }
  .home .nameplate {
    padding: 11px 18px 13px 16px;
  }
}

@media (max-width: 720px) {
  .home .crest {
    margin-top: 20px;
  }
  .simple-item {
    padding: 11px 12px 12px 26px;
    grid-template-columns: 30px minmax(0, 1fr);
    gap: 12px;
  }
  .simple-icon {
    width: 30px;
    height: 30px;
  }
  .cmd-link {
    padding: 8px 12px 8px 26px;
    font-size: 11px;
    letter-spacing: 0.09em;
  }
}

/* ---------------------------------------------------------- reduced motion */

/* Core already clamps every animation to a hair's duration; these land on
   their finished state explicitly so nothing can be left invisible. */
@media (prefers-reduced-motion: reduce) {
  .home .crest-rail,
  .home .crest-caps,
  .home .crest-gem {
    animation: none;
    opacity: 1;
    transform: none;
  }
}


/* ==========================================================================
   about
   ========================================================================== */

/* ===========================================================================
   40-about — the About page.

   Two ideas, both borrowed from a 16-bit menu:

   1. The career / education timelines are a QUEST LOG. Every entry is a
      record hung off the lit rail, selectable with the menu cursor, its date
      stamped on a sunken plate.
   2. The cats are the PARTY WINDOW: two sprite slots with nameplate bars.

   Core (site.css) already draws the rail, the rail nodes, the cursor and the
   menu window. Everything here is character on top of that.
   ======================================================================== */

/* ------------------------------------------------------ section headings */

/* A gold pixel tab in front of each heading, the way a menu marks the
   section you are looking at. */
.about .section-h2 {
  position: relative;
  padding-left: 17px;
}

.about .section-h2::before {
  content: "";
  position: absolute;
  left: 0;
  top: 3px;
  width: 6px;
  height: 12px;
  background: var(--gold);
  box-shadow: 0 0 0 2px var(--frame-dark);
}

/* =============================================================== quest log

   Geometry, so the pieces stay on the rail:
     .timeline padding-left        46px   record starts here
     .tl-row border                 2px   -> padding box starts at 48px
     rail (core .timeline::before) 20-22px, centre 21
     node (core .tl-row::before)   12px wide, left: -33px -> 15-27, centre 21
     tick                          29-44px, the dotted run into the record
   ------------------------------------------------------------------------ */

.timeline {
  gap: 2px;
  padding: 16px 14px 16px 46px;
  border-color: rgb(var(--rgb-frame-mid) / .7);
  background-color: transparent;
  /* layer 1: the sunken groove the rail sits in. layer 2: the window fill. */
  background-image:
    linear-gradient(rgb(var(--rgb-well) / .6), rgb(var(--rgb-well) / .6)),
    var(--panel-fill);
  background-repeat: no-repeat, repeat;
  background-position: 18px 14px, 0 0;
  background-size: 6px calc(100% - 28px), auto;
}

/* the rail: a touch brighter than core, so it reads as lit */
.timeline::before {
  opacity: 0.85;
}

/* ------------------------------------------------------------ the record */

.tl-row {
  --stud: rgb(var(--rgb-gold) / .3);
  align-items: start;
  gap: 12px;
  padding: 12px 12px 13px 14px;
  border: 2px solid transparent;
  background-color: rgb(var(--rgb-sheen) / .014);
  /* two gold corner studs + a faint top-lit fill */
  background-image:
    linear-gradient(var(--stud), var(--stud)),
    linear-gradient(var(--stud), var(--stud)),
    linear-gradient(180deg, rgb(var(--rgb-sheen) / .035), rgb(var(--rgb-shade) / .16));
  background-repeat: no-repeat, no-repeat, repeat;
  background-position: 0 0, 100% 100%, 0 0;
  background-size: 3px 3px, 3px 3px, auto;
  transition:
    background-color 0.12s var(--ease-step),
    border-color 0.12s var(--ease-step);
}

.tl-row:hover,
.tl-row:focus-within {
  --stud: var(--gold);
  background-color: rgb(var(--rgb-gold) / .09);
  border-color: rgb(var(--rgb-gold) / .55);
}

/* the node, re-hung for the border I just added */
.tl-row::before {
  left: -33px;
  top: 15px;
  transition:
    background 0.12s var(--ease-step),
    box-shadow 0.12s var(--ease-step);
}

.tl-row:hover::before,
.tl-row:focus-within::before {
  background: var(--gold);
  box-shadow: 0 0 0 2px rgb(var(--rgb-gold) / .22);
}

/* the dotted run from the rail node into the record */
.tl-tick {
  position: absolute;
  left: -19px;
  top: 20px;
  width: 15px;
  height: 2px;
  background-image: linear-gradient(90deg, var(--gold) 50%, transparent 50%);
  background-size: 4px 2px;
  opacity: 0.45;
  pointer-events: none;
  transition: opacity 0.12s var(--ease-step);
}

.tl-row:hover .tl-tick,
.tl-row:focus-within .tl-tick {
  opacity: 1;
}

/* the menu cursor: pinned to the first line, level with the node */
.tl-row.has-cursor::after {
  left: 2px;
  top: 15px;
  margin-top: 0;
}

.tl-row:focus-within::after {
  opacity: 1;
  transform: translateX(0);
  animation: cursor-blink 1s steps(1, end) infinite;
}

/* ------------------------------------------------------- record contents */

.tl-body {
  min-width: 0;
}

.tl-primary {
  line-height: 1.5;
  text-shadow: var(--text-shadow-sm);
}

/* the org reads as the small label under the quest name */
.tl-primary > .muted {
  font-family: var(--ui);
  font-size: 11.5px;
  letter-spacing: 0.04em;
}

.tl-secondary {
  padding-left: 11px;
  border-left: 2px solid rgb(var(--rgb-frame-mid) / .4);
  transition: border-color 0.12s var(--ease-step);
}

.tl-row:hover .tl-secondary,
.tl-row:focus-within .tl-secondary {
  border-left-color: rgb(var(--rgb-gold) / .45);
}

/* meta reads as an equipped-item chip */
.tl-meta {
  display: inline-block;
  padding: 4px 8px;
  background: var(--sunken);
  border: 2px solid rgb(var(--rgb-frame-mid) / .38);
  text-shadow: var(--text-shadow-sm);
  transition:
    border-color 0.12s var(--ease-step),
    color 0.12s var(--ease-step);
}

.tl-row:hover .tl-meta,
.tl-row:focus-within .tl-meta {
  border-color: rgb(var(--rgb-gold) / .4);
  color: var(--ink-soft);
}

/* --------------------------------------------------------- the date stamp */

.tl-date {
  display: grid;
  justify-items: end;
  align-content: start;
  gap: 6px;
  padding-top: 1px;
}

.tl-date-plate {
  display: inline-block;
  padding: 5px 8px;
  background: var(--sunken);
  border: 2px solid rgb(var(--rgb-gold) / .32);
  box-shadow: inset 0 0 0 1px rgb(var(--rgb-well) / .55);
  text-shadow: var(--text-shadow);
  white-space: nowrap;
  transition:
    background 0.12s var(--ease-step),
    border-color 0.12s var(--ease-step),
    color 0.12s var(--ease-step);
}

.tl-row:hover .tl-date-plate,
.tl-row:focus-within .tl-date-plate {
  background: rgb(var(--rgb-gold) / .16);
  border-color: var(--gold);
  color: var(--ink-bright);
}

.tl-loc {
  font-family: var(--ui);
  font-size: 10px;
  letter-spacing: 0.1em;
  line-height: 1.5;
}

/* ============================================================ party window

   Each cat is a sprite slot with a nameplate bolted under it — and a toggle
   button, because pressing one sends that familiar out to follow you around
   the site. See 70-familiar for the cat that walks out of here.
   ------------------------------------------------------------------------ */

.about-card {
  padding: 14px;
}

.cat-split {
  gap: 10px;
}

.cat {
  position: relative;
  display: grid;
  gap: 0;
  justify-items: stretch;
  /* It is a <button> now: undo the parts of the UA style that survive
     `display: grid`, and inherit the type rather than shipping Helvetica. */
  appearance: none;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: inherit;
  cursor: pointer;
  background: var(--window-flat);
  border: 2px solid var(--frame-mid);
  box-shadow: 0 0 0 2px var(--frame-dark), 0 4px 0 rgb(var(--rgb-shade) / .35);
  transition:
    border-color 0.12s var(--ease-step),
    transform 0.12s var(--ease-step),
    box-shadow 0.12s var(--ease-step);
}

/* four gold studs, same trick as .surface */
.cat::before {
  content: "";
  position: absolute;
  inset: -2px;
  z-index: 2;
  pointer-events: none;
  background-image:
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold));
  background-repeat: no-repeat;
  background-size: 3px 3px;
  background-position: 0 0, 100% 0, 0 100%, 100% 100%;
  opacity: 0.55;
  transition: opacity 0.12s var(--ease-step);
}

.cat:hover {
  border-color: var(--gold);
  transform: translateY(-2px);
  box-shadow: 0 0 0 2px var(--frame-dark), 0 6px 0 rgb(var(--rgb-shade) / .4);
}

.cat:hover::before {
  opacity: 1;
}

/* the slot the sprite sits in: a dark well with a hard inner edge */
.cat-slot {
  position: relative;
  display: block;
  padding: 4px;
  background: rgb(var(--rgb-well) / .62);
  border-bottom: 2px solid var(--frame-mid);
  box-shadow: inset 0 0 0 1px rgb(var(--rgb-well) / .9);
  transition: border-color 0.12s var(--ease-step);
}

.cat:hover .cat-slot {
  border-bottom-color: var(--gold);
}

.cat img {
  border: 0;
  background: transparent;
}

/* the nameplate bar */
.cat-plate {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 7px 5px;
  background: var(--plate-fill);
  text-shadow: var(--text-shadow);
  transition: background 0.12s var(--ease-step);
}

.cat:hover .cat-plate {
  background: var(--plate-fill-hot);
}

/* the slot is sized for a 16px icon in the plate */
.cat-icon {
  width: 16px;
  height: 16px;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}

.cat-name {
  line-height: 1;
}

/* ------------------------------------------------------------------- summoned

   The chosen cat's photo goes grey — but grey usually means *disabled*, and
   this is the opposite: the slot is empty because the cat is out with you. So
   the frame stays lit rather than dimming, and an OUT stamp lands over the
   photo to say which of the two readings is meant.

   Keyed off data-familiar on <html>, not off the button's aria-pressed. Both
   say the same thing, but only the root attribute is set before first paint —
   aria-pressed is written by site.js, which is deferred, so styling on it would
   show the card un-greyed for a frame on every load with a cat already out.
   aria-pressed still carries the state to a screen reader; this is the paint.
   ------------------------------------------------------------------------ */

:root[data-familiar="artemis"] .cat[data-summon="artemis"],
:root[data-familiar="apollo"] .cat[data-summon="apollo"] {
  border-color: var(--gold);
}

:root[data-familiar="artemis"] .cat[data-summon="artemis"]::before,
:root[data-familiar="apollo"] .cat[data-summon="apollo"]::before {
  opacity: 1;
}

:root[data-familiar="artemis"] .cat[data-summon="artemis"] .cat-slot,
:root[data-familiar="apollo"] .cat[data-summon="apollo"] .cat-slot {
  border-bottom-color: var(--gold);
}

:root[data-familiar="artemis"] .cat[data-summon="artemis"] img,
:root[data-familiar="apollo"] .cat[data-summon="apollo"] img {
  filter: grayscale(1) brightness(0.72);
}

:root[data-familiar="artemis"] .cat[data-summon="artemis"] .cat-plate,
:root[data-familiar="apollo"] .cat[data-summon="apollo"] .cat-plate {
  background: var(--plate-fill-hot);
}

:root[data-familiar="artemis"] .cat[data-summon="artemis"] .cat-slot::after,
:root[data-familiar="apollo"] .cat[data-summon="apollo"] .cat-slot::after {
  content: "OUT";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  padding: 3px 6px;
  font-family: var(--ui);
  font-size: 10px;
  letter-spacing: 0.18em;
  color: var(--gold);
  background: rgb(var(--rgb-well) / .88);
  box-shadow: 0 0 0 2px var(--gold);
  text-shadow: var(--text-shadow);
}

/* No script, no familiar. The heading would otherwise promise something that
   cannot happen, and the buttons would be two photographs that depress when
   you click them and do nothing else. Left alone, the card is what it has
   always been: the two cats. */
html:not(.js) .familiar-card .section-h2 {
  display: none;
}

html:not(.js) .cat {
  cursor: default;
  pointer-events: none;
}

/* the line under the party window */
.about-card-body {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 2px solid rgb(var(--rgb-frame-mid) / .32);
  font-size: 14.5px;
}

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

@media (max-width: 720px) {
  .timeline {
    /* record starts at 38px, padding box at 40px; node centre stays on 21 */
    padding-left: 38px;
  }
  .tl-row {
    gap: 7px;
  }
  .tl-row::before {
    left: -25px;
  }
  .tl-tick {
    display: none;
  }
  .tl-date {
    justify-items: start;
    padding-top: 0;
  }
}


/* ==========================================================================
   projects
   ========================================================================== */

/* ==========================================================================
   50-projects — the shop window and the lore page.

   Two rooms:
   1. /projects/  — a list of treasure. Each card is an item slot: a rarity
      rail down the left gutter, a landing strip for the menu cursor, and a
      framed portrait of the goods. The archive below it is the records
      drawer you pull open at the bottom of the same menu.
   2. /projects/<x>/ — the write-up. Furniture gets the 16-bit treatment
      (section plates, sunken wells, stat lines) but the running text stays
      at a size you can read for ten minutes. Charts are never pixelated:
      a blurred ROC curve is just a lie.
   ========================================================================== */

/* --------------------------------------------------------- index: header */

/* The kicker reads as the window's tab rather than a floating label. */
.projects .page-header .kicker {
  display: inline-block;
  padding: 5px 10px;
  border: 2px solid rgb(var(--rgb-gold) / .45);
  background: rgb(var(--rgb-gold) / .1);
  box-shadow: 0 2px 0 rgb(var(--rgb-shade) / .35);
}

.projects .page-header .page-sub {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 2px solid var(--faint);
}

/* --------------------------------------------------------- index: notice */

/* The "back shortly" sign taped to the shop window. Sits between the header
   and the grid, so it is the first thing read after the page title. Sunken
   rather than a .surface of its own: it is a note about the room, not another
   window in it. Temporary — see the matching comment in _pages/projects.html. */
.wip-notice {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 16px;
  padding: 14px 18px;
  background: var(--sunken);
  border: 2px solid rgb(var(--rgb-gold) / .45);
  box-shadow: var(--shadow-sm);
}

.wip-badge {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 6px 10px;
  border: 2px solid rgb(var(--rgb-gold) / .5);
  background: rgb(var(--rgb-gold) / .12);
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold);
  text-shadow: var(--text-shadow-sm);
  white-space: nowrap;
}

/* A single pixel ticking on the same beat as the menu cursor, so the sign
   reads as live rather than abandoned. Killed by the global reduced-motion
   reset. */
.wip-blip {
  width: 6px;
  height: 6px;
  flex: 0 0 auto;
  background: var(--gold);
  animation: cursor-blink 1s steps(1, end) infinite;
}

.wip-copy {
  margin: 0;
  max-width: 72ch;
  font-size: 14.5px;
  line-height: 1.7;
  color: var(--ink-soft);
}

/* ---------------------------------------------------------- index: cards */

/* Every card carries a left gutter — the strip the cursor lands in. The
   card itself stays a grid, so the gutter is padding plus one absolutely
   positioned rail rather than a third column that would fight the
   stacked/wide switch. */
.projects-grid .project-card {
  padding-left: 38px;
}

.projects-grid .card-rail {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 38px;
  pointer-events: none;
  background: var(--sunken);
  border-right: 2px solid rgb(var(--rgb-frame-mid) / .35);
  transition: background 0.14s var(--ease-step), border-color 0.14s var(--ease-step);
}

/* The rarity bar: a dashed column of pixels that fills solid gold when the
   entry is selected. */
.projects-grid .card-rail::before {
  content: "";
  position: absolute;
  left: 7px;
  top: 14px;
  bottom: 14px;
  width: 5px;
  background-image: linear-gradient(180deg, var(--gold-deep) 50%, transparent 50%);
  background-size: 5px 8px;
  opacity: 0.5;
  transition: opacity 0.14s var(--ease-step);
}

/* End caps, so the rail terminates instead of just stopping. */
.projects-grid .card-rail::after {
  content: "";
  position: absolute;
  left: 7px;
  top: 0;
  bottom: 0;
  width: 5px;
  background-image:
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold));
  background-size: 5px 4px;
  background-repeat: no-repeat;
  background-position: 0 5px, 0 calc(100% - 5px);
  opacity: 0.55;
  transition: opacity 0.14s var(--ease-step);
}

.projects-grid .project-card:hover .card-rail,
.projects-grid .project-card:focus-visible .card-rail {
  background: rgb(var(--rgb-gold) / .09);
  border-right-color: rgb(var(--rgb-gold) / .5);
}

.projects-grid .project-card:hover .card-rail::before,
.projects-grid .project-card:focus-visible .card-rail::before {
  background-image: linear-gradient(180deg, var(--gold), var(--gold));
  opacity: 1;
}

.projects-grid .project-card:hover .card-rail::after,
.projects-grid .project-card:focus-visible .card-rail::after {
  opacity: 1;
}

/* Park the shared cursor primitive in the gutter, clear of the rarity bar,
   instead of hanging off the card's left edge. */
.projects-grid .project-card.has-cursor::after {
  left: 19px;
  top: 50%;
  z-index: 3;
}

/* Chunkier lift than the baseline: item slots press in when you commit. */
.projects-grid .project-card:active {
  transform: translateY(0);
  box-shadow: 0 0 0 2px var(--frame-dark), 0 4px 0 rgb(var(--rgb-shade) / .4);
}

/* ---------------------------------------------------------- index: media */

.projects-grid .project-media {
  position: relative;
}

/* A mat inside the picture slot, with a gold rivet pinned to each corner.
   The chart itself is never touched — no pixelation, no filters. */
.projects-grid .media-frame {
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 0 2px rgb(var(--rgb-well) / .5);
  background-image:
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold));
  background-size: 3px 3px;
  background-repeat: no-repeat;
  background-position:
    6px 6px,
    calc(100% - 6px) 6px,
    6px calc(100% - 6px),
    calc(100% - 6px) calc(100% - 6px);
  opacity: 0.65;
  transition: opacity 0.14s var(--ease-step);
}

.projects-grid .project-card:hover .media-frame {
  opacity: 1;
}

/* ----------------------------------------------------------- index: body */

.projects-grid .project-body {
  gap: 13px;
}

/* An item glyph beside the name, the way every shop list has one. */
.projects-grid .project-name {
  display: flex;
  align-items: flex-start;
  gap: 11px;
}

.projects-grid .project-name::before {
  content: "";
  flex: 0 0 auto;
  width: 10px;
  height: 10px;
  margin-top: 6px;
  background: var(--crystal);
  box-shadow:
    0 0 0 2px var(--frame-dark),
    inset 0 0 0 2px rgb(var(--rgb-well) / .55);
  transition: background 0.14s var(--ease-step);
}

.projects-grid .project-card:hover .project-name::before {
  background: var(--gold);
}

/* The confirm button. It lifts when the entry is selected. */
.projects-grid .project-link {
  box-shadow: 0 3px 0 rgb(var(--rgb-shade) / .4);
  transition: border-color 0.12s var(--ease-step), background 0.12s var(--ease-step),
    color 0.12s var(--ease-step), box-shadow 0.12s var(--ease-step),
    transform 0.12s var(--ease-step);
}

.projects-grid .project-card:hover .project-link {
  transform: translateY(-2px);
  box-shadow: 0 5px 0 rgb(var(--rgb-shade) / .45);
}

.projects-grid .project-link .icon {
  color: var(--gold);
}

/* -------------------------------------------------- index: records drawer */

/* Closed, the summary reads as one more menu entry; open, it is the lid of
   the drawer below it. */
.archive .archive-summary {
  padding: 11px 13px;
  border: 2px solid var(--frame-mid);
  background: var(--sunken);
  box-shadow: 0 3px 0 rgb(var(--rgb-shade) / .35);
  transition: border-color 0.14s var(--ease-step), background 0.14s var(--ease-step),
    box-shadow 0.14s var(--ease-step);
}

.archive .archive-summary:hover {
  border-color: var(--gold);
  background: rgb(var(--rgb-gold) / .11);
}

.archive[open] .archive-summary {
  border-color: rgb(var(--rgb-gold) / .6);
  box-shadow: 0 2px 0 rgb(var(--rgb-shade) / .3);
}

/* The chevron is a pixel triangle now, not a text glyph — but it keeps the
   class name so core's open-state 180° flip still finds it. */
.archive .career-chevron {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  line-height: 0;
}

.archive .career-chevron .pixel-chevron {
  display: block;
  width: 14px;
  height: 14px;
}

/* The drawer pull, on the far right of the lid. */
.archive .archive-handle {
  margin-left: auto;
  flex: 0 0 auto;
  width: 26px;
  height: 10px;
  background-image: linear-gradient(90deg, var(--frame-mid) 50%, transparent 50%);
  background-size: 6px 10px;
  opacity: 0.5;
  transition: opacity 0.14s var(--ease-step);
}

.archive .archive-summary:hover .archive-handle,
.archive[open] .archive-handle {
  background-image: linear-gradient(90deg, var(--gold) 50%, transparent 50%);
  opacity: 0.8;
}

.archive .archive-list {
  margin-top: 14px;
  gap: 6px;
}

/* Compact list entries: a left accent that lights on selection, and room in
   the gutter for the cursor. */
.archive .archive-row {
  gap: 5px;
  padding: 10px 14px 11px 32px;
}

.archive .archive-row::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 5px;
  background: rgb(var(--rgb-frame-mid) / .4);
  transition: background 0.12s var(--ease-step);
}

.archive .archive-row:hover::before {
  background: var(--gold);
}

.archive .archive-row.has-cursor::after {
  left: 12px;
  top: 21px;
  margin-top: 0;
}

.archive .archive-top {
  gap: 10px;
}

.archive .archive-name {
  transition: color 0.12s var(--ease-step);
}

.archive .archive-row:hover .archive-name {
  color: var(--gold);
}

.archive .archive-desc {
  font-size: 14.5px;
  line-height: 1.65;
}

.archive .archive-foot {
  margin: 16px 0 0;
  padding-top: 14px;
  border-top: 2px solid var(--faint);
}

/* --------------------------------------------------------- article: hero */

/* A mat inside the frame, so the header art reads as something hung in the
   window rather than bleeding to its edges. */
.article-hero .hero-plate {
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 0 2px rgb(var(--rgb-well) / .5);
}

.article-hero--contain .hero-plate {
  inset: 12px;
  border: 2px solid rgb(var(--rgb-frame-mid) / .38);
  box-shadow: none;
  background-image:
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(var(--gold), var(--gold));
  background-size: 3px 3px;
  background-repeat: no-repeat;
  background-position:
    4px 4px,
    calc(100% - 4px) 4px,
    4px calc(100% - 4px),
    calc(100% - 4px) calc(100% - 4px);
  opacity: 0.8;
}

/* --------------------------------------------------------- article: head */

.article-head .kicker {
  display: inline-block;
  padding: 5px 10px;
  border: 2px solid rgb(var(--rgb-gold) / .45);
  background: rgb(var(--rgb-gold) / .1);
  box-shadow: 0 2px 0 rgb(var(--rgb-shade) / .35);
}

.article-head .page-title {
  margin-top: 16px;
}

.article-head .page-sub {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 2px solid var(--faint);
}

.article-head .article-tags {
  margin-top: 16px;
}

/* The command row. */
.article-head .article-links {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 2px dotted rgb(var(--rgb-muted) / .24);
}

.article-head .article-links .pill {
  padding: 10px 14px;
  box-shadow: 0 4px 0 rgb(var(--rgb-shade) / .45);
}

.article-head .article-links .pill .icon {
  color: var(--crystal);
  transition: color 0.12s var(--ease-step);
}

.article-head .article-links .pill:hover .icon {
  color: var(--gold);
}

/* The status window: a sunken well of stat lines. `.fact-row` is shared with
   the About page, so every tweak here stays scoped to the article head. */
.article-head .article-facts {
  margin-top: 20px;
  padding: 4px 14px 6px;
  background: var(--sunken);
  border: 2px solid rgb(var(--rgb-frame-mid) / .4);
  box-shadow: inset 0 0 0 2px rgb(var(--rgb-well) / .45);
}

.article-head .article-facts .fact-row {
  padding: 9px 0;
  border-bottom: 0;
  gap: 10px;
}

.article-head .article-facts .fact-lead {
  flex: 1 1 auto;
  min-width: 20px;
  height: 2px;
  align-self: center;
  background-image: linear-gradient(90deg, rgb(var(--rgb-muted) / .5) 50%, transparent 50%);
  background-size: 6px 2px;
}

.article-head .article-facts .fact-value {
  flex: 0 1 auto;
}

/* -------------------------------------------------------- article: prose */

/* Running text is left alone on purpose — core's 17px/1.85 is the readable
   setting and the theme does not get to argue with it. Everything below is
   furniture. */

/* Chapter plate. */
.prose h2 {
  margin: 40px 0 18px;
  padding: 11px 14px;
  border: 2px solid rgb(var(--rgb-frame-mid) / .45);
  border-left-width: 6px;
  border-left-color: var(--gold);
  border-bottom: 2px solid rgb(var(--rgb-frame-mid) / .45);
  background: var(--chapter-fill);
  box-shadow: 0 3px 0 rgb(var(--rgb-shade) / .35);
}

.prose h3 {
  position: relative;
  padding-left: 20px;
}

.prose h3::before {
  content: "";
  position: absolute;
  left: 0;
  top: 6px;
  width: 9px;
  height: 9px;
  background: var(--crystal);
  box-shadow: 0 0 0 2px var(--frame-dark);
}

.prose h4 {
  max-width: 70ch;
  padding-bottom: 6px;
  border-bottom: 2px dotted rgb(var(--rgb-muted) / .28);
}

/* Square bullets rather than round ones. `list-style-type` keeps the list
   semantics intact, which `list-style: none` would put at risk. */
.prose ul {
  list-style-type: square;
}

.prose ol li::marker {
  font-family: var(--ui);
  font-size: 0.85em;
  color: var(--gold);
}

/* An aside in the dialogue box. */
.prose blockquote {
  max-width: 70ch;
  padding: 14px 16px;
  border: 2px solid rgb(var(--rgb-frame-mid) / .35);
  border-left: 6px solid var(--gold);
  background: var(--sunken);
  color: var(--ink-soft);
  box-shadow: inset 0 0 0 2px rgb(var(--rgb-well) / .4);
}

.prose blockquote > *:last-child {
  margin-bottom: 0;
}

/* Code wells get a heavy lip along the top, like a terminal panel dropped
   into the window. */
.prose pre {
  border-top-width: 6px;
  box-shadow: inset 0 0 0 2px rgb(var(--rgb-well) / .6), var(--shadow-sm);
}

/* A divider with a gem set into the middle of the rail. */
.prose hr {
  height: 8px;
  opacity: 0.75;
  background-image:
    linear-gradient(var(--gold), var(--gold)),
    linear-gradient(90deg, var(--frame-mid) 50%, transparent 50%);
  background-size: 8px 8px, 6px 2px;
  background-repeat: no-repeat, repeat-x;
  background-position: center center, left center;
}

/* Data tables read as a stat screen: plated header, alternating rows. */
.prose table {
  box-shadow: var(--shadow-sm);
}

.prose thead th {
  border-bottom: 2px solid var(--frame-mid);
}

.prose tbody tr:nth-child(even) {
  background: rgb(var(--rgb-well) / .3);
}

.prose tbody tr:hover {
  background: rgb(var(--rgb-gold) / .09);
}

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

/* --------------------------------------------------- article: chart slots */

/* Charts, ROC curves and confusion matrices are evidence. They get a sunken
   slot and a bevel around them and nothing done TO them — no
   `image-rendering: pixelated`, no filters. Pixelating a confusion matrix
   destroys the only thing it is there to communicate. */
.prose p:has(> img) {
  padding: 14px;
  background: var(--sunken);
  border: 2px solid rgb(var(--rgb-frame-mid) / .4);
  border-top-width: 6px;
  box-shadow: inset 0 0 0 2px rgb(var(--rgb-well) / .5), 0 4px 0 rgb(var(--rgb-shade) / .3);
}

.prose p:has(> img) img {
  display: block;
  margin: 0 auto;
  border: 2px solid var(--frame-mid);
  background: var(--sunken);
  box-shadow: 0 3px 0 rgb(var(--rgb-shade) / .35);
}

/* ------------------------------------------------------- article: return */

/* The back-out command, styled as a slot rather than a bare link. Core's
   `.home-link` nudges right on hover; this arrow points left, so it leaves. */
.article-back {
  margin: 6px 0 0;
}

.article-back .home-link {
  padding: 10px 16px;
  border: 2px solid var(--frame-mid);
  background: var(--sunken);
  box-shadow: 0 4px 0 rgb(var(--rgb-shade) / .4);
  transition: color 0.14s var(--ease-step), transform 0.14s var(--ease-step),
    border-color 0.14s var(--ease-step), background 0.14s var(--ease-step);
}

.article-back .home-link:hover {
  transform: translateX(-3px);
  border-color: var(--gold);
  background: rgb(var(--rgb-gold) / .14);
}

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

/* Below 980px the wide card stacks (art on top, body below). The gutter is
   card padding, so it keeps running the full height and the cursor lands in
   the same place either way — nothing to undo. */

@media (max-width: 720px) {
  .projects-grid .project-card {
    padding-left: 28px;
  }
  .projects-grid .card-rail {
    width: 28px;
  }
  .projects-grid .card-rail::before,
  .projects-grid .card-rail::after {
    left: 5px;
    width: 4px;
  }
  .projects-grid .card-rail::before {
    background-size: 4px 8px;
  }
  .projects-grid .card-rail::after {
    background-size: 4px 4px;
  }
  .projects-grid .project-card.has-cursor::after {
    left: 14px;
  }
  .archive .archive-summary {
    padding: 10px 11px;
  }
  .archive .archive-handle {
    display: none;
  }
  .archive .archive-row {
    padding: 10px 12px 11px 28px;
  }
  .archive .archive-row.has-cursor::after {
    left: 10px;
  }
  .prose h2 {
    padding: 10px 12px;
  }
  .prose p:has(> img) {
    padding: 10px;
  }
}


/* ==========================================================================
   icons-misc
   ========================================================================== */

/* ==========================================================================
   60-icons-misc — pixel icon set, icon slots, and the 404 "empty chest" screen
   Loads after site.css. Never redefines :root tokens. No radius. No blur.
   ========================================================================== */

/* ------------------------------------------------------------------- icons */

/* The glyphs are 16x16 pixel art drawn on integer coordinates. Core already
   sets width/height/display on .icon — keep this deliberately thin so the
   other sections stay in charge of colour and layout. */
.icon {
  shape-rendering: crispEdges;
}

/* Opt-in sizes. Only multiples of 16 so the pixel grid stays whole. */
.icon-lg {
  width: 32px;
  height: 32px;
}

.icon-xl {
  width: 48px;
  height: 48px;
}

/* A framed "item slot" that an icon sits inside — the little sunken square
   FF menus park an equipment glyph in. Usable anywhere: wrap an .icon in
   <span class="icon-slot">. */
.icon-slot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex: 0 0 auto;
  color: var(--crystal);
  background: var(--sunken);
  border: 2px solid var(--frame-mid);
  box-shadow: inset 0 0 0 1px rgb(var(--rgb-shade) / .55);
  transition: color 0.12s var(--ease-step), border-color 0.12s var(--ease-step),
    background 0.12s var(--ease-step);
}

.icon-slot > .icon {
  display: block;
}

.icon-slot-gold {
  color: var(--gold);
  border-color: rgb(var(--rgb-gold) / .5);
  background: rgb(var(--rgb-gold) / .08);
}

a:hover > .icon-slot,
a:focus-visible > .icon-slot {
  color: var(--gold);
  border-color: var(--gold);
  background: rgb(var(--rgb-gold) / .12);
}

/* --------------------------------------------------------------------- 404 */

.notfound {
  padding-top: 44px;
  padding-bottom: 52px;
}

/* The void: a sunken star-lit well the numeral and the looted chest sit in. */
.notfound-void {
  position: relative;
  width: min(100%, 460px);
  display: grid;
  justify-items: center;
  gap: 4px;
  padding: 22px 16px 16px;
  margin-bottom: 6px;
  background: var(--sunken);
  border: 2px solid var(--frame-mid);
  box-shadow: inset 0 0 0 2px var(--frame-dark);
  overflow: hidden;
}

.notfound-stars {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* Hard 2px square stars — box-shadow copies, so they stay crisp. */
.notfound-stars::before,
.notfound-stars::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 2px;
  height: 2px;
}

.notfound-stars::before {
  background: var(--muted);
  opacity: 0.55;
  box-shadow:
    22px 18px 0 var(--muted), 58px 46px 0 var(--muted),
    96px 12px 0 var(--muted), 132px 70px 0 var(--muted),
    168px 30px 0 var(--muted), 204px 96px 0 var(--muted),
    238px 22px 0 var(--muted), 272px 62px 0 var(--muted),
    308px 14px 0 var(--muted), 344px 88px 0 var(--muted),
    378px 40px 0 var(--muted), 412px 74px 0 var(--muted),
    442px 26px 0 var(--muted), 40px 124px 0 var(--muted),
    118px 148px 0 var(--muted), 262px 136px 0 var(--muted),
    398px 152px 0 var(--muted), 150px 190px 0 var(--muted),
    352px 198px 0 var(--muted);
}

.notfound-stars::after {
  background: var(--gold);
  box-shadow:
    74px 92px 0 var(--gold), 188px 138px 0 var(--gold),
    330px 54px 0 var(--gold), 430px 116px 0 var(--gold),
    16px 178px 0 var(--crystal), 286px 186px 0 var(--crystal);
  animation: nf-twinkle 1.9s steps(1, end) infinite;
}

@keyframes nf-twinkle {
  0%,
  49% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0.18;
  }
}

/* The opened, emptied treasure chest. */
.notfound-chest {
  position: relative;
  z-index: 1;
  width: clamp(72px, 17vw, 104px);
  height: auto;
  display: block;
  animation: nf-bob 2.4s steps(1, end) infinite;
}

.nf-dark {
  fill: var(--frame-dark);
}

.nf-wood {
  fill: var(--gold-deep);
}

.nf-gold {
  fill: var(--gold);
}

.nf-lit {
  fill: var(--frame-lit);
}

.nf-spark {
  fill: var(--crystal);
}

@keyframes nf-bob {
  0%,
  24% {
    transform: translateY(0);
  }
  25%,
  49% {
    transform: translateY(-2px);
  }
  50%,
  74% {
    transform: translateY(-4px);
  }
  75%,
  100% {
    transform: translateY(-2px);
  }
}

/* Game-over numeral: gold face, deep-gold bevel on all four sides, hard drop. */
.notfound-num {
  position: relative;
  z-index: 1;
  margin-top: 4px;
  letter-spacing: 0.08em;
  text-shadow:
    2px 0 0 var(--gold-deep),
    -2px 0 0 var(--gold-deep),
    0 2px 0 var(--gold-deep),
    0 -2px 0 var(--gold-deep),
    4px 6px 0 rgb(var(--rgb-shade) / .75);
}

.notfound .section-h2 {
  margin: 0;
}

.notfound .muted {
  max-width: 42ch;
}

.notfound-actions {
  margin-top: 10px;
  justify-content: center;
}

@media (max-width: 560px) {
  .notfound {
    padding-left: 16px;
    padding-right: 16px;
  }

  .notfound-void {
    padding: 18px 12px 14px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .notfound-chest,
  .notfound-stars::after {
    animation: none;
  }

  .notfound-chest {
    transform: none;
  }

  .notfound-stars::after {
    opacity: 1;
  }
}


/* ==========================================================================
   Content-face corrections

   Silkscreen has no usable ampersand — its "&" is all but identical to a "$",
   so "AI & Machine Learning" renders as "AI $ Machine Learning". That is fine
   for the fixed chrome (nav, dates, footer, button labels never contain one),
   but any label fed from _data/ can, and several do: five skills categories,
   a focus category, and "Texas A&M University".

   So: labels that render authored content use the body bitmap face, which has
   a correct ampersand and is still a pixel font. Sizes are nudged up because
   DotGothic16 sets smaller than Silkscreen at the same px.

   Rule of thumb for future work — Silkscreen is only safe for strings written
   in the template. Anything coming out of a data file belongs here.
   ========================================================================== */

.simple-category {
  font-family: var(--body);
  font-size: 13px;
}

.tl-primary > .muted {
  font-family: var(--body);
  font-size: 13.5px;
}

/* ===========================================================================
   70-familiar — the cat that follows you.

   Summoned from the party window on the About page, then it lives in the
   viewport on every page until you send it home. Three things keep it honest:

   1. It is fixed to the viewport, not the document, so scrolling does not drag
      it and it never has to know how tall the page is.
   2. z-index 60 puts it over .app and the sticky navbar but under the theme
      wipe (90) and the CRT wash (100) — so the scanlines ride over the cat the
      way they ride over everything else, and a theme change flashes it out
      with the rest of the scene rather than leaving it stranded on top.
   3. The sprite animation is CSS. site.js moves the cat; it does not draw it.
      The walk cycle is two frames alternated by a stepped opacity keyframe,
      which is why there is still no per-frame script anywhere on this site.

   Transforms are split deliberately. .familiar carries the position and only
   the position, set from script. The sprite inside carries the facing (`scale`)
   and the hop (`translate`) as separate CSS properties, so neither one fights
   the other and the hint tag never ends up mirrored.
   ======================================================================== */

.familiar {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 60;
  width: 60px;
  height: 48px;
  pointer-events: none;
  will-change: transform;
}

/* Belt and braces: this element is display-less by default so [hidden] works,
   but it is fixed-position decoration and a stray display rule would strand a
   cat on screen with no way to dismiss it. */
.familiar[hidden] {
  display: none;
}

/* 20x16 at exactly 3x. Only the sprite takes pointer events — the box around
   it does not, or the hint tag would eat clicks meant for the page. */
.familiar-sprite {
  display: block;
  width: 60px;
  height: 48px;
  pointer-events: auto;
  cursor: pointer;
  transform-origin: 50% 100%;
}

.familiar[data-facing="left"] .familiar-sprite {
  scale: -1 1;
}

/* ------------------------------------------------------------------ palette */

.familiar[data-cat="artemis"] {
  --fam-fur: var(--artemis-fur);
  --fam-shade: var(--artemis-shade);
  --fam-line: var(--artemis-line);
  --fam-eye: var(--artemis-eye);
}

.familiar[data-cat="apollo"] {
  --fam-fur: var(--apollo-fur);
  --fam-shade: var(--apollo-shade);
  --fam-line: var(--apollo-line);
  --fam-eye: var(--apollo-eye);
}

.fam-fur { fill: var(--fam-fur); }
.fam-shade { fill: var(--fam-shade); }
.fam-line { fill: var(--fam-line); }
.fam-eye { fill: var(--fam-eye); }
/* the collar tag and the Zzz are site accents, not cat colours */
.fam-tag { fill: var(--gold); }
.fam-zzz { fill: var(--crystal); }

/* --------------------------------------------------------- which cat, which
   pose

   Both cats and all six of their frames are in the document at once; this is
   the whole of the switching logic.
   ------------------------------------------------------------------------ */

.fam-cat,
.fam-frame {
  display: none;
}

.familiar[data-cat="artemis"] .fam-cat[data-cat="artemis"],
.familiar[data-cat="apollo"] .fam-cat[data-cat="apollo"],
.familiar[data-pose="sit"] .fam-frame[data-frame="sit"],
.familiar[data-pose="groom"] .fam-frame[data-frame="groom"],
.familiar[data-pose="alert"] .fam-frame[data-frame="alert"],
.familiar[data-pose="sleep"] .fam-frame[data-frame="sleep"],
.familiar[data-pose="walk"] .fam-frame[data-frame="walk-a"],
.familiar[data-pose="walk"] .fam-frame[data-frame="walk-b"],
.familiar[data-pose="run"] .fam-frame[data-frame="walk-a"],
.familiar[data-pose="run"] .fam-frame[data-frame="walk-b"] {
  display: block;
}

/* Walking renders both frames stacked and alternates them on a hard cutoff —
   no blending, a sprite either is or is not the next frame. walk-b rests
   transparent so the still state is a whole frame rather than a double
   exposure. */
.fam-frame[data-frame="walk-b"] {
  opacity: 0;
}

.familiar[data-pose="walk"] .fam-frame[data-frame="walk-a"],
.familiar[data-pose="walk"] .fam-frame[data-frame="walk-b"],
.familiar[data-pose="run"] .fam-frame[data-frame="walk-a"],
.familiar[data-pose="run"] .fam-frame[data-frame="walk-b"] {
  animation: fam-step 0.36s steps(1, end) infinite;
}

.familiar[data-pose="walk"] .fam-frame[data-frame="walk-b"] {
  animation-delay: 0.18s;
}

.familiar[data-pose="run"] .fam-frame[data-frame="walk-a"],
.familiar[data-pose="run"] .fam-frame[data-frame="walk-b"] {
  animation-duration: 0.2s;
}

.familiar[data-pose="run"] .fam-frame[data-frame="walk-b"] {
  animation-delay: 0.1s;
}

@keyframes fam-step {
  0%, 50% { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

/* A sitting cat that is perfectly still reads as a decal. One pixel of breath
   is enough to stop that. */
.familiar[data-pose="sit"] .familiar-sprite,
.familiar[data-pose="groom"] .familiar-sprite {
  animation: fam-breathe 3.4s var(--ease-step) infinite;
}

@keyframes fam-breathe {
  0%, 100% { translate: 0 0; }
  50% { translate: 0 1px; }
}

/* Units here are viewBox units, not device pixels: 1 is a whole fat sprite
   pixel, which is as far as a Zzz needs to drift. */
.familiar[data-pose="sleep"] .fam-zzz {
  animation: fam-zzz 2.6s var(--ease-step) infinite;
}

@keyframes fam-zzz {
  0% { opacity: 0; translate: 0 1px; }
  30% { opacity: 1; translate: 0 0; }
  70% { opacity: 1; translate: 0 -1px; }
  100% { opacity: 0; translate: 0 -2px; }
}

/* ------------------------------------------------------------ being handled */

/* Ordered after fam-breathe on purpose: the two rules match at the same
   specificity, and a petted cat should hop rather than keep breathing. */
.familiar.is-petted .familiar-sprite {
  animation: fam-hop 0.42s var(--ease-step);
}

@keyframes fam-hop {
  0% { translate: 0 0; }
  35% { translate: 0 -7px; }
  60% { translate: 0 0; }
  78% { translate: 0 -3px; }
  100% { translate: 0 0; }
}

.fam-heart {
  position: absolute;
  left: 50%;
  top: 2px;
  width: 14px;
  height: 12px;
  margin-left: -7px;
  pointer-events: none;
  background: var(--magic);
  -webkit-mask-image: var(--fam-heart-mask);
  mask-image: var(--fam-heart-mask);
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  animation: fam-float 0.9s var(--ease-step) forwards;
}

.familiar {
  /* A pixel heart on a 7x6 grid, used as a mask so the fill stays a token. */
  --fam-heart-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 7 6' shape-rendering='crispEdges'%3E%3Cpath d='M1 0h2v1h-2zM4 0h2v1h-2zM0 1h7v2h-7zM1 3h5v1h-5zM2 4h3v1h-3zM3 5h1v1h-1z'/%3E%3C/svg%3E");
}

@keyframes fam-float {
  0% { opacity: 0; translate: 0 0; }
  25% { opacity: 1; }
  100% { opacity: 0; translate: 0 -22px; }
}

/* The hold-to-dismiss gauge. It only appears once you are already holding, so
   it reads as confirmation rather than as a control bolted to the cat. */
.fam-gauge {
  position: absolute;
  left: 50%;
  bottom: -7px;
  width: 26px;
  height: 4px;
  margin-left: -13px;
  pointer-events: none;
  opacity: 0;
  background: rgb(var(--rgb-well) / .82);
  box-shadow: 0 0 0 2px var(--frame-dark);
}

.fam-gauge::after {
  content: "";
  display: block;
  width: 0;
  height: 100%;
  background: var(--gold);
}

.familiar.is-holding .fam-gauge {
  opacity: 1;
}

.familiar.is-holding .fam-gauge::after {
  animation: fam-fill 700ms linear forwards;
}

@keyframes fam-fill {
  to { width: 100%; }
}

/* Shown once, ever. After that the gesture is known and the tag is noise. */
.fam-hint {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 4px);
  transform: translateX(-50%);
  padding: 3px 6px;
  white-space: nowrap;
  pointer-events: none;
  font-family: var(--ui);
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gold);
  text-shadow: var(--text-shadow);
  background: var(--plate-fill);
  border: 2px solid var(--frame-mid);
}

.fam-hint[hidden] {
  display: none;
}

/* ------------------------------------------------------------ reduced motion

   Core already clamps every animation, but a clamped animation lands on its
   *last* keyframe — which for the Zzz is opacity 0 and for the walk cycle is a
   blank frame. Land each one on a resting state instead. site.js does the rest:
   with reduced motion it repositions the cat in steps rather than gliding it.
   ------------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .familiar[data-pose="walk"] .fam-frame[data-frame="walk-a"],
  .familiar[data-pose="walk"] .fam-frame[data-frame="walk-b"],
  .familiar[data-pose="run"] .fam-frame[data-frame="walk-a"],
  .familiar[data-pose="run"] .fam-frame[data-frame="walk-b"],
  .familiar[data-pose="sit"] .familiar-sprite,
  .familiar[data-pose="groom"] .familiar-sprite,
  .familiar[data-pose="sleep"] .fam-zzz,
  .familiar.is-petted .familiar-sprite,
  .familiar.is-holding .fam-gauge::after,
  .fam-heart {
    animation: none;
  }

  /* walk-a is the whole frame; keep b out of the way */
  .familiar[data-pose="walk"] .fam-frame[data-frame="walk-b"],
  .familiar[data-pose="run"] .fam-frame[data-frame="walk-b"] {
    opacity: 0;
  }

  .familiar[data-pose="sleep"] .fam-zzz {
    opacity: 1;
    translate: 0 0;
  }

  /* holding still has to show progress, so the gauge simply reads full */
  .familiar.is-holding .fam-gauge::after {
    width: 100%;
  }

  .fam-heart {
    opacity: 1;
  }
}
