/* ==========================================================================
   teveelcats.com
   Night-vision / eyeshine theme, carried over from the original prototype.

   The organising idea: each cat owns an accent colour, exposed to CSS as
   --eye-c on a container. Components read that variable rather than any
   hard-coded colour, so a cat's identity propagates through tabs, borders,
   glows and focus rings without a single per-cat rule.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Tokens
   -------------------------------------------------------------------------- */
:root {
  --bg:            #06090A;
  --panel:         #0D1315;
  --panel-raised:  #111A1C;
  --panel-inset:   #080D0E;
  /* Greener than they were (#1C2A2C / #2A3E40). Every rule and divider on
     the site is one of these two, and the old pair read as plain grey next to
     a page whose whole idea is a single phosphor colour. */
  --line:          #1B3329;
  --line-bright:   #2B4C3D;
  --text:          #DCE6E4;
  --text-strong:   #F2F7F6;
  /*
   * Both of these were measured against EVERY surface they can sit on, not
   * just --bg. --text-faint used to be #48605E (2.96:1) while carrying the
   * smallest text on the site, then #6E8482 — which cleared 4.5:1 on --bg and
   * --panel but came to 4.45:1 on --panel-raised, a hair under the floor small
   * text needs. It is now nudged up so it clears AA on the raised panel too,
   * imperceptibly lighter and still clearly the faintest tier.
   *
   * Four tiers cannot all clear 4.5:1 on a near-black page without collapsing
   * into each other, so the floor was raised rather than the ceiling lowered.
   *
   *   --text-dim    6.99:1 on --bg, 6.19:1 on --panel-raised (worst)
   *   --text-faint  5.45:1 on --bg, 4.82:1 on --panel-raised (worst)
   *
   * prefers-contrast: more bumps both further (see the media block below).
   */
  --text-dim:      #849E9C;
  --text-faint:    #748A88;
  --danger:        #FF6B6B;
  --danger-dim:    #7A3A3A;
  --ok:            #6EE7B7;

  /* Fallback accent, overridden per cat by an inline --eye-c. */
  --eye-c:         #6EE7B7;

  /*
   * The brand's own green, deliberately NOT --eye-c.
   *
   * The wordmark sits inside whatever accent context the page is in, so
   * reading --eye-c there would make the logo change colour with the cat you
   * happen to be looking at — which is the one thing a logo must not do.
   * Same value as the default accent; the point is that it never moves.
   */
  --brand:         #6EE7B7;

  /* The shaded side of a brand-coloured plate, and how tall that side is.
     Containers need the height too, to sit a plate optically straight — see
     .plate. */
  --brand-edge:    #3A8A6A;
  --plate-edge:    4px;

  --radius:        3px;
  --radius-lg:     2px;
  --shell:         1040px;

  --font-display:  'Space Grotesk', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono:     'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --ease:          cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* --------------------------------------------------------------------------
   Accent classes — one per palette entry, mirroring Cats::PALETTE.
   Adding a colour means adding it in both places.

   A cat's colour reaches the page as one of these class names rather than an
   inline style, which keeps the Content-Security-Policy free of any
   unsafe-inline concession. (The narrower `style-src-attr` directive would
   have allowed just style attributes, but Firefox does not implement it and
   falls back to `style-src` — which would have blocked every accent on the
   site.)
   -------------------------------------------------------------------------- */
.accent-green  { --eye-c: #6EE7B7; }
.accent-amber  { --eye-c: #F5B942; }
.accent-pink   { --eye-c: #FF6FA5; }
.accent-blue   { --eye-c: #6FD8FF; }
.accent-violet { --eye-c: #B98EFF; }
.accent-coral  { --eye-c: #FF8A5C; }

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

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

/*
 * Monochrome, not multicoloured.
 *
 * Night vision is a single phosphor colour — that is what makes it read as
 * night vision rather than as a dark website. So the page carries exactly one
 * hue, the selected cat's, and everything else is degrees of near-black. The
 * earlier version washed purple and blue into the corners regardless of which
 * cat was showing, which is the generic dark-dashboard look and pulled against
 * the whole per-cat idea.
 *
 * Under the accent wash is the sky the stars sit in: brightest low down, the
 * way the last of the light sits on the horizon, falling to the site's own
 * black everywhere else. The vignette that used to be here is gone — with a
 * horizon glow doing the same job from the other direction, both at once only
 * muddied the middle.
 */
body {
  margin: 0;
  min-height: 100vh;
  background:
    radial-gradient(120% 85% at 50% 32%,
      color-mix(in srgb, var(--eye-c) 9%, transparent) 0%,
      transparent 55%),
    radial-gradient(ellipse 140% 100% at 50% 100%,
      var(--sky-glow) 0%,
      var(--sky-deep) 70%),
    var(--bg);
  background-attachment: fixed;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/*
 * The sky.
 *
 * Three layers of stars drifting upward at three speeds, which is what makes
 * it read as depth rather than as a pattern: the near ones cross the screen
 * in fifty seconds, the far ones take two and a half minutes.
 *
 * Built from repeating tiles rather than from a list of star positions. The
 * original does it with about a thousand box-shadows on one element, which is
 * roughly 37 KB of coordinates on every page and a famously slow thing for a
 * browser to paint. A tile repeats for nothing, and three tiles of different
 * sizes overlaid do not visibly line up again for thousands of pixels — so
 * the sky looks random without anyone having written down where it is.
 *
 * The layers move with transform rather than background-position, so the
 * animation runs on the compositor and does not repaint a full-screen surface
 * sixty times a second on somebody's phone.
 *
 * They live on html::before, body::before and body::after — three slots that
 * already exist on every page — so the sky needs no markup and cannot be
 * forgotten on a page somebody adds later.
 */
:root {
  --star-a:
    radial-gradient(circle at 401px 38px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 89px 318px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 42px 259px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 87px 66px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 443px 401px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 42px 197px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 218px 464px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 425px 378px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 494px 443px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 362px 241px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 65px 113px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 160px 84px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 8px 401px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 200px 317px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 297px 94px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 178px 45px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 219px 127px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 476px 394px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 257px 150px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 137px 305px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 446px 99px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 426px 225px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 381px 266px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 342px 145px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 321px 113px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 451px 70px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 340px 233px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 93px 109px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 74px 302px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 110px 319px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 207px 232px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 331px 309px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 462px 120px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 126px 396px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 109px 303px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 442px 304px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 307px 413px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 347px 373px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 154px 131px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 342px 111px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 277px 449px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 172px 29px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 288px 383px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 408px 262px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 177px 120px, rgba(255,255,255,0.9) 0 1px, transparent 1.5px);

  --star-b:
    radial-gradient(circle at 596px 71px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 501px 494px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 220px 520px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 167px 180px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 358px 23px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 454px 241px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 564px 160px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 265px 413px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 230px 120px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 608px 479px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 376px 190px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 327px 145px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 262px 61px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 642px 471px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 405px 309px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 645px 210px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 401px 151px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 462px 143px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 411px 512px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 362px 130px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 40px 395px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px),
    radial-gradient(circle at 66px 59px, rgba(255,255,255,0.8) 0 1.5px, transparent 2px);

  --star-c:
    radial-gradient(circle at 56px 313px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 563px 547px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 368px 192px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 362px 691px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 553px 670px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 6px 516px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 219px 387px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 686px 398px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 373px 565px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 222px 275px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 530px 307px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 419px 695px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 501px 75px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 215px 209px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 416px 498px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px),
    radial-gradient(circle at 92px 760px, rgba(255,255,255,0.7) 0 2px, transparent 2.5px);

  /*
   * Swap these two for #1b2735 and #090a0f to get the blue night sky this was
   * taken from. Green-black keeps the night-vision idea the rest of the site
   * is built on; blue is prettier on its own and less this site.
   */
  --sky-glow: #16332B;
  --sky-deep: #06090A;
}

html::before,
body::before,
body::after {
  content: '';
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  z-index: 1;
  pointer-events: none;
  will-change: transform;
}

/*
 * Each layer hangs one tile below the viewport, and travels exactly one tile
 * before repeating. Because the pattern's period is that same tile, the jump
 * back to the start lands on an identical sky and cannot be seen.
 */
/*
 * Longhands, not the background shorthand.
 *
 * Each --star-* is a comma-separated list of forty-odd gradients, and in the
 * shorthand a trailing "/ 500px 500px repeat" attaches to the LAST layer only.
 * Every other gradient then fell back to auto size and painted once at its
 * own coordinates, which put the whole sky in a single clump at the top left
 * of the screen and left the rest of the page empty.
 *
 * As longhands, a single size and repeat value is applied to every layer in
 * the list, which is what makes it a tile at all.
 */
html::before {
  bottom: -500px;
  background-image: var(--star-a);
  background-size: 500px 500px;
  background-repeat: repeat;
  animation: drift-a 50s linear infinite;
}
body::before {
  bottom: -650px;
  background-image: var(--star-b);
  background-size: 650px 650px;
  background-repeat: repeat;
  animation: drift-b 100s linear infinite;
}
body::after {
  bottom: -800px;
  background-image: var(--star-c);
  background-size: 800px 800px;
  background-repeat: repeat;
  animation: drift-c 150s linear infinite;
}

@keyframes drift-a { to { transform: translateY(-500px); } }
@keyframes drift-b { to { transform: translateY(-650px); } }
@keyframes drift-c { to { transform: translateY(-800px); } }

/*
 * Stars that never stop are exactly the kind of thing this setting exists for.
 * The global reduced-motion block only shortens durations, which for an
 * infinite animation means it runs faster rather than not at all — so it is
 * switched off by name here.
 */
@media (prefers-reduced-motion: reduce) {
  html::before, body::before, body::after { animation: none; }
}


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

a { color: var(--eye-c); text-decoration-color: color-mix(in srgb, var(--eye-c) 40%, transparent); }
a:hover { text-decoration-color: currentColor; }

/* One visible focus style for the whole site, keyed to the active accent. */
:where(a, button, input, textarea, select, [tabindex]):focus-visible {
  outline: 2px solid var(--eye-c);
  outline-offset: 2px;
  border-radius: 2px;
}

.shell { max-width: var(--shell); margin-inline: auto; padding-inline: 32px; }

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

/* Skip link — visible only once focused. */
.skip {
  position: absolute; top: 0; left: 0; z-index: 100;
  transform: translateY(-120%);
  background: var(--panel-raised); color: var(--text-strong);
  border: 1px solid var(--eye-c);
  padding: 10px 16px; font-size: 12px;
  text-decoration: none;
}
.skip:focus { transform: translateY(0); }

/* --------------------------------------------------------------------------
   Site navigation — sticky, public side
   -------------------------------------------------------------------------- */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid color-mix(in srgb, var(--eye-c) 14%, var(--line));
}
.site-nav__inner {
  display: flex;
  align-items: center;
  gap: 12px 28px;
  padding-block: 11px;
}

.site-nav__brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: var(--text-strong);
  flex: none;
}

/* The brand is the wordmark itself — see .wordmark further down. */

/* --------------------------------------------------------------------------
   The plate

   A solid button standing on a darker edge, which collapses when you push it
   down. One definition, used by the public bar and by the admin bar: the two
   headers are the same header with different contents, and the surest way to
   keep them that way is to give them nothing of their own to drift with.

   --brand, not --eye-c, for the same reason as the language switcher: a bar
   is site chrome and must look the same on every page. As a 7% tint that
   hardly mattered; as a solid fill it would turn the navigation pink on one
   cat and blue on the next.

   The font is named rather than inherited because <button> does not inherit
   one, and half the plates on the site are buttons.

   A plate is drawn --plate-edge taller than it measures, so whatever holds a
   row of them owes that much bottom margin back — otherwise the plate sits
   centred and its edge hangs past the bar.
   -------------------------------------------------------------------------- */
.plate {
  display: inline-block;
  padding: 9px 18px;
  border: none;
  border-radius: 5px;
  background: var(--brand);
  color: var(--bg);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-decoration: none;
  cursor: pointer;
  box-shadow: 0 var(--plate-edge) 0 0 var(--brand-edge);
  transition: transform 0.1s var(--ease),
              box-shadow 0.1s var(--ease),
              background 0.15s var(--ease);
}
.plate:hover {
  background: color-mix(in srgb, var(--brand) 86%, #fff);
}
/*
 * The page you are on is already pressed in. Nothing to click means nothing
 * standing up, which is a clearer "you are here" than the tinted border it
 * replaces — and the inset makes the state readable without relying on the
 * 4px of travel alone.
 */
.plate[aria-current='page'] {
  transform: translateY(var(--plate-edge));
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.32);
}
.plate:active {
  transform: translateY(var(--plate-edge));
  box-shadow: 0 0 0 0 var(--brand-edge);
}

.site-nav__links {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: var(--plate-edge);
}

/* --------------------------------------------------------------------------
   Page hero
   -------------------------------------------------------------------------- */
.page-hero {
  position: relative;
  z-index: 2;
  padding-block: 44px 26px;
}
.site-header {
  position: relative;
  z-index: 2;
  padding-block: 56px 28px;
}

.eyebrow {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0 0 18px;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/*
 * The blinking eyeshine dot lived here. It was the admin bar's mark, and the
 * admin bar now carries the wordmark like every other page — which has the
 * pupil built into it. Nothing rendered the dot any more, so it is gone
 * rather than left to be found later and wondered about.
 */

/*
 * The wordmark.
 *
 * "tabs" sits in a chip with rounded top corners, flush on a hairline rule —
 * the shape of a browser tab docked to its strip. It is the site's own name
 * drawn as the thing the name is about, which is a better logo than a picture
 * of a cat: it survives at 16px in the bar and at 60px on the landing page
 * without redrawing anything.
 *
 * Deliberately NOT keyed to --eye-c. The background and the cat chrome shift
 * per cat; the brand does not, or it stops being a brand.
 */
.wordmark {
  display: inline-flex;
  align-items: flex-end;
  gap: 0.26em;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(38px, 7vw, 66px);
  letter-spacing: -0.03em;
  line-height: 1;
  margin: 0 0 16px;
  color: var(--text-strong);
  border-bottom: 1px solid var(--line-bright);
  text-decoration: none;
}
.wordmark a { color: inherit; text-decoration: none; }

.wordmark__lead {
  font-weight: 500;
  color: var(--text-dim);
  padding-bottom: 0.16em;
  transition: color 0.18s var(--ease);
}

/*
 * The chip, and the eye in it.
 *
 * It used to be drawn as a browser tab — flat-bottomed, docked to the rule —
 * because the site was called "te veel tabs" and the shape was the pun. The
 * site is called teveelcats now, and a tab shape around the word "cats" is a
 * joke with its setup removed: a form that refers to nothing.
 *
 * So the chip closes into a plain rounded tag and carries a vertical slit
 * pupil instead, the same mark as the favicon. That is the whole idea the
 * site is built on — eyeshine in the dark — stated in the logo rather than
 * only in the theme, and it survives at 16px in the bar where a finer shape
 * would turn to mush.
 */
.wordmark__tab {
  position: relative;
  bottom: -1px;                     /* sits on the rule, not above it */
  padding: 0.12em 0.32em 0.16em 0.78em;
  background: var(--panel-raised);
  border: 1px solid var(--line-bright);
  border-radius: var(--radius);
  text-shadow: 0 0 30px rgba(242, 247, 246, 0.22);
  transition: background 0.18s var(--ease), border-color 0.18s var(--ease);
}

/* Sized in em so it scales with the wordmark at every size it is used at. */
.wordmark__tab::before {
  content: '';
  position: absolute;
  left: 0.26em;
  top: 50%;
  width: 0.20em;
  height: 0.44em;
  margin-top: -0.22em;
  border-radius: 50%;
  background: var(--brand);
  box-shadow: 0 0 0.5em color-mix(in srgb, var(--brand) 45%, transparent);
}

/* A half-size .wordmark--sm lived here, for the landing page's own copy of
   the logo under the bar. That copy is gone, and so is this. */

/* In the bar: same construction, small, and it lights up as a link. */
.wordmark--nav {
  font-size: 16px;
  margin: 0;
  border-bottom-color: var(--line);
  flex: none;
  gap: 0.22em;
}
.wordmark--nav:hover .wordmark__lead,
.wordmark--nav:focus-visible .wordmark__lead { color: var(--text); }
.wordmark--nav:hover .wordmark__tab,
.wordmark--nav:focus-visible .wordmark__tab {
  background: var(--panel-inset);
  border-color: var(--text-faint);
}

.tagline {
  font-family: var(--font-display);
  font-size: 15px;
  color: var(--text-dim);
  max-width: 56ch;
  margin: 0;
}

/* --------------------------------------------------------------------------
   Back to the list

   A strip of every cat name used to run across the top of each profile, so a
   page about one cat opened by listing all of them. The list is its own page
   now, and this is the single link back to it.
   -------------------------------------------------------------------------- */
.backlink {
  position: relative;
  z-index: 2;
  margin: 0;
  padding-block: 26px 0;
  font-family: var(--font-mono);
  font-size: 12px;
}
.backlink a {
  color: var(--text-dim);
  text-decoration: none;
  transition: color 0.15s var(--ease);
}
.backlink a:hover { color: var(--text-strong); }

/* --------------------------------------------------------------------------
   Cat profile
   -------------------------------------------------------------------------- */
.stage {
  position: relative;
  z-index: 2;
  padding-block: 32px 96px;
}

/*
 * The front page has nothing on it, so the main has to carry the height
 * itself or the footer rides up and sits halfway across an empty sky, which
 * reads as a page that failed to finish loading.
 *
 * Done here rather than by making the body a flex column with the footer
 * pushed down: as a flex item, .shell's margin-inline:auto stops stretching
 * and starts centring at content width, which collapsed every page on the
 * site to about half its width. The subtraction is the bar plus the footer.
 */
.stage--empty { min-height: calc(100vh - 160px); }

/*
 * Same gradient-border technique as the sign-in card: an inner fill clipped
 * to the padding box, a gradient clipped to the border box, showing through
 * a transparent 1px border.
 *
 * Here the gradient starts from the cat's own accent and travels through
 * blue, violet and pink — so every cat gets a different edge, and it is
 * recognisably theirs at the top-left where the eye lands first.
 */
/*
 * A frame, not a card.
 *
 * Hard 2px corners and a thin single-colour edge — the geometry of a
 * viewfinder. The soft 10px radius and rainbow edge it replaces were the
 * giveaway that this was a generic dark card with decoration applied; a
 * camera frame is square and one colour.
 */
/*
 * The same corner and the same fall of shadow as the cards on the landing
 * page, so arriving here reads as the card you clicked opening up rather than
 * as a different site.
 *
 * Deliberately not the full .glass treatment: this panel does not grow when
 * you point at it and does not tip when you press it, because it is the page
 * rather than a thing on the page — and it keeps its own viewfinder brackets
 * and its scan-in, which are what make it a profile and not a tile.
 */
.profile {
  display: grid;
  grid-template-columns: minmax(240px, 320px) 1fr;
  border: 1px solid color-mix(in srgb, var(--eye-c) 26%, transparent);
  border-radius: 17px;
  overflow: hidden;
  background: linear-gradient(168deg, #0D1517 0%, #0A1012 100%);
  box-shadow:
    10px 18px 44px rgba(0, 0, 0, 0.55),
    0 24px 60px -34px rgba(0, 0, 0, 0.95),
    inset 0 0 60px -30px color-mix(in srgb, var(--eye-c) 40%, transparent);
  /* The panel wipes in top-to-bottom, like a scan resolving. */
  animation: scan 0.45s var(--ease);
}

/*
 * Viewfinder brackets on the photo. Eight background layers, two per corner:
 * a short horizontal rule and a short vertical one, positioned to the corners
 * of an inset box. Cheaper and sharper than borders, and it survives whatever
 * the photo underneath happens to be.
 */
.profile__photo::after {
  content: '';
  position: absolute;
  inset: 12px;
  pointer-events: none;
  --bk: linear-gradient(var(--eye-c), var(--eye-c));
  background:
    var(--bk) left top / 15px 1.5px no-repeat,
    var(--bk) left top / 1.5px 15px no-repeat,
    var(--bk) right top / 15px 1.5px no-repeat,
    var(--bk) right top / 1.5px 15px no-repeat,
    var(--bk) left bottom / 15px 1.5px no-repeat,
    var(--bk) left bottom / 1.5px 15px no-repeat,
    var(--bk) right bottom / 15px 1.5px no-repeat,
    var(--bk) right bottom / 1.5px 15px no-repeat;
  opacity: 0.85;
}

@keyframes scan {
  from { clip-path: inset(0 0 100% 0); opacity: 0.55; }
  to   { clip-path: inset(0 0 0 0);    opacity: 1; }
}

.profile__photo {
  position: relative;
  min-height: 300px;
  background: var(--panel-inset);
  border-right: 1px solid var(--line);
  display: grid;
  place-items: center;
}
/*
 * The whole photo, never a crop of it.
 *
 * Filling the frame meant a portrait photo lost its top and bottom — which on
 * a cat is the head. The column keeps its full height so the divider line runs
 * the length of the card; the photo just sits centred inside it at its own
 * shape, and the inset background shows around it.
 */
.profile__photo img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* Stand-in when a cat has no photo yet. It is plainly a graphic, not a
   fake photo, and the caption says so. */
.eyes {
  width: 96px; height: 96px;
  border-radius: 50%;
  position: relative;
  background: radial-gradient(circle, color-mix(in srgb, var(--eye-c) 80%, transparent) 0%, transparent 72%);
  filter: blur(2px);
  opacity: 0.85;
}
.eyes::before, .eyes::after {
  content: '';
  position: absolute;
  top: 38%;
  width: 14px; height: 22px;
  border-radius: 50%;
  background: var(--eye-c);
  box-shadow: 0 0 16px var(--eye-c);
}
.eyes::before { left: 24px; }
.eyes::after  { right: 24px; }

.photo-note {
  position: absolute;
  bottom: 12px;
  left: 0; right: 0;
  text-align: center;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.profile__body { padding: 28px 30px; min-width: 0; }

.profile__name {
  font-family: var(--font-display);
  font-size: clamp(26px, 3.4vw, 34px);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text-strong);
  margin: 0 0 20px;
}

/* Fixed 2x2. auto-fit looked tidier in the abstract but produced a 3+1
   layout for the four facts we actually have, stranding the last one across
   a half-empty row. */
.facts {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  border: 1px solid var(--line);
  border-radius: 2px;
  overflow: hidden;
  margin-bottom: 22px;
}
.fact {
  padding: 11px 15px;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  min-width: 0;
}
/* No rules on the outer edges — the container already draws those. */
.fact:nth-child(2n) { border-right: none; }
.fact:nth-last-child(-n + 2) { border-bottom: none; }
.fact__label {
  display: block;
  font-size: 9.5px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 4px;
}
.fact__value {
  font-family: var(--font-display);
  font-size: 14px;
  color: var(--text);
  overflow-wrap: anywhere;
}
/* An unknown value is an em dash, never an invented one. */
.fact__value--empty { color: var(--text-faint); }

.bio {
  font-family: var(--font-display);
  font-size: 14.5px;
  line-height: 1.7;
  color: var(--text-dim);
  border-top: 1px dashed var(--line);
  padding-top: 18px;
  margin: 0;
  white-space: pre-line; /* keeps the paragraph breaks the admin typed */
}

/* --------------------------------------------------------------------------
   Empty state
   -------------------------------------------------------------------------- */
/* Gradient edge here too, dimmer than a profile — an empty state should read
   as quiet, not as something demanding attention. */
.empty {
  border: 1px solid transparent;
  border-radius: 2px;
  padding: 72px 28px;
  text-align: center;
  color: var(--text-faint);
  font-size: 13.5px;
  background-image:
    linear-gradient(160deg, #0F1719 0%, #0B1113 100%),
    linear-gradient(135deg,
      color-mix(in srgb, var(--eye-c) 45%, transparent) 0%,
      rgba(111, 216, 255, 0.20) 45%,
      rgba(185, 142, 255, 0.26) 100%);
  background-origin: border-box;
  background-clip: padding-box, border-box;
}
.empty strong {
  display: block;
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 500;
  color: var(--text-dim);
  margin-bottom: 8px;
}
.empty__action { margin-top: 18px; }

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */
.site-footer {
  position: relative;
  z-index: 2;
  border-top: 1px solid var(--line);
  padding-block: 22px 40px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  justify-content: space-between;
  font-size: 11.5px;
  color: var(--text-faint);
}
.site-footer a { color: var(--text-dim); text-decoration: none; }
.site-footer a:hover { color: var(--text); }

/* --------------------------------------------------------------------------
   Responsive
   -------------------------------------------------------------------------- */
@media (max-width: 720px) {
  .shell { padding-inline: 20px; }
  .site-header { padding-block: 36px 22px; }
  .profile { grid-template-columns: 1fr; }
  .profile__photo {
    min-height: 0;
    aspect-ratio: 4 / 3;
    border-right: none;
    border-bottom: 1px solid var(--line);
  }
  .profile__body { padding: 22px; }

  /* One fact per row on a phone; the 2x2 border rules no longer apply. */
  .facts { grid-template-columns: 1fr; }
  .fact { border-right: none; }
  .fact:nth-last-child(-n + 2) { border-bottom: 1px solid var(--line); }
  .fact:last-child { border-bottom: none; }
}

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

@media (prefers-contrast: more) {
  :root {
    --text-dim:   #A8BCBA;
    --text-faint: #8CA3A1;
    --line:       #335446;
  }
}

@media print {
  /*
   * Paper, not the screen in negative.
   *
   * Everything on the page reads its colour from the --text/--line tokens,
   * so redefining them here is what makes a page built for a dark ground
   * legible on white — no page-specific overrides needed anywhere else.
   */
  :root {
    --text:        #14201D;
    --text-strong: #060B09;
    --text-dim:    #3A4E4A;
    --text-faint:  #52625F;
    --line:        #C7D2CE;
  }

  body::before, body::after { display: none; }
  body { background: #fff; color: var(--text); }

  /* On paper the plates are just slabs of ink around a word. */
  .plate {
    background: none; color: #000; box-shadow: none; transform: none;
  }

  /* Chrome that only makes sense on a screen someone can click. */
  .site-nav__lang, .skip, .site-footer { display: none; }

  /*
   * The whole album, not just whichever photo happened to be open.
   *
   * The viewer's radios-and-panes is an on-screen interaction a sheet of
   * paper cannot host, so every pane is pulled out of it: static in the
   * document instead of stacked on top of each other, all visible, in
   * normal flow so the page simply grows to fit them. The picker itself —
   * the invisible radios, the strip of thumbnails, the prev/next labels —
   * has nothing left to control, so it disappears rather than printing as
   * inert clutter.
   */
  .album__pick, .album__strip, .album__nav { display: none; }
  .album__viewer { height: auto; }
  .album__pane {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    break-inside: avoid;
    margin-bottom: 12pt;
  }
  .album__pane img { height: auto; max-height: 70vh; }
  .album__caption { position: static; background: none; color: var(--text-dim); text-shadow: none; }
}

/* --------------------------------------------------------------------------
   Language switcher
   -------------------------------------------------------------------------- */
/*
 * A switch: a recessed track with the two languages written on it and a lit
 * knob standing on the side you are reading.
 *
 * It is still two links and not a checkbox, because changing language here is
 * a page navigation and this site runs no JavaScript — a checkbox would flip
 * and take you nowhere. The switch is the appearance; the mechanism underneath
 * is the same pair of ordinary links, each pointing at the page you are on.
 */
.site-nav__lang {
  position: relative;
  margin-left: auto;
  display: flex;
  align-items: stretch;
  border: 1px solid var(--line-bright);
  border-radius: 5px;
  padding: 3px;
  background: var(--panel-inset);
  box-shadow: inset 2px 4px 9px rgba(0, 0, 0, 0.6);
  flex: none;
}
.site-nav__lang a {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 2.7em;
  padding: 5px 8px;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--text-faint);
  text-decoration: none;
  transition: color 0.2s var(--ease);
}
.site-nav__lang a:hover { color: var(--text-dim); }
/* Neutral, not --eye-c: the switcher is site chrome and must look identical on
   every page — having it turn pink on one cat and blue on the next made it
   read as part of that cat rather than as a control. */
.site-nav__lang a[aria-current='true'] {
  color: var(--text-strong);
  font-weight: 600;
}

/*
 * The knob is a real element rather than a pseudo, so it can carry a
 * view-transition-name of its own and genuinely slide from one side to the
 * other as the page changes — the only way to animate a switch that is
 * thrown by a navigation.
 */
.lang-knob {
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: 4px;
  width: 2px;
  border-radius: 1px;
  background: var(--brand);
  box-shadow: 0 0 7px color-mix(in srgb, var(--brand) 65%, transparent);
  pointer-events: none;
  view-transition-name: lang-knob;
}
.site-nav__lang:has(a:last-of-type[aria-current='true']) .lang-knob {
  left: auto;
  right: 4px;
}

@media (max-width: 560px) {
  .site-nav__inner { gap: 8px 14px; }
  .wordmark--nav   { font-size: 14px; }
  .site-nav__links { gap: 6px; }
  .plate { padding: 8px 13px; }
}


/* --------------------------------------------------------------------------
   Album

   A separate block under the profile, with its own frame. The cat's portrait
   above is fixed and is not part of this — the two never share a space, so
   browsing the album never changes the picture that identifies the cat.

   Radio buttons and CSS, no JavaScript: each radio sits immediately before
   its own pane, which makes "show this photo" one adjacent-sibling rule that
   works for any number of photos.
   -------------------------------------------------------------------------- */
.album {
  margin-top: 28px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: linear-gradient(168deg, #0B1214 0%, #090F11 100%);
  overflow: hidden;
}

.album__title {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0;
  padding: 14px 20px 12px;
  border-bottom: 1px solid var(--line);
}

/*
 * A fixed frame height, so switching photos never moves the page and a
 * portrait shot cannot stretch the block into a column.
 */
.album__viewer {
  position: relative;
  height: clamp(260px, 50vh, 520px);
  background: var(--panel-inset);
}

/* Out of the layout but still in the tab order. */
.album__pick {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.album__pane {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  visibility: hidden;
  /*
   * Off-canvas, not just invisible. inset:0 alone leaves every pane's box
   * exactly where the visible one sits — opacity and visibility do not move
   * it — so a browser deciding whether a lazy <img> is worth fetching sees
   * every photo in the album as already on screen and fetches all of them at
   * once. Shifted a full box-width to the side, the box genuinely is off
   * screen, which is what native lazy loading actually looks at. No
   * transition on the transform: it only matters for :checked below, where
   * it must snap into place instantly rather than slide — the crossfade
   * remains purely an opacity transition, exactly as it already was.
   */
  transform: translateX(100%);
  transition: opacity 0.22s var(--ease);
}
/* contain, not cover: these are the photos themselves, so none of them gets
   cropped to fit the frame. */
.album__pane img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.album__pick:checked + .album__pane {
  opacity: 1;
  visibility: visible;
  transform: translateX(0);
}

/* The radio is invisible, so its focus ring belongs on the photo it controls. */
.album__pick:focus-visible + .album__pane {
  outline: 2px solid var(--eye-c);
  outline-offset: -4px;
}

/*
 * Previous and next, as a strip down each side of the photo.
 *
 * Visible without being pointed at, at half strength. Hover-only arrows are
 * invisible on a phone, which is the one place where reaching for a thumbnail
 * the size of a fingernail is hardest — so they are always there, and only
 * brighten under the pointer.
 *
 * The scrim is what makes the chevron readable: these sit over photographs,
 * and a white arrow on a photo of a white cat is not an arrow.
 */
.album__nav {
  position: absolute;
  top: 0;
  bottom: 0;
  width: clamp(52px, 13%, 104px);
  display: grid;
  place-items: center;
  cursor: pointer;
  opacity: 0.5;
  transition: opacity 0.18s var(--ease);
}
.album__nav:hover { opacity: 1; }

.album__nav--prev {
  left: 0;
  background: linear-gradient(to right, rgba(0, 0, 0, 0.4), transparent);
}
.album__nav--next {
  right: 0;
  background: linear-gradient(to left, rgba(0, 0, 0, 0.4), transparent);
}

/* Two sides of a square, turned. One element, no image, sharp at any size. */
.album__nav::before {
  content: '';
  width: 13px;
  height: 13px;
  border: 2px solid #fff;
  border-left: 0;
  border-bottom: 0;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.7));
}
.album__nav--next::before { transform: rotate(45deg)   translate(-2px, 2px); }
.album__nav--prev::before { transform: rotate(-135deg) translate(-2px, 2px); }

/*
 * The caption and "photo N of M", visibly — both used to live only in alt=
 * and aria-label, read to a screen reader and invisible to anyone actually
 * looking at the picture. One scrim at the foot of the frame, clear of the
 * nav arrows at each side.
 */
.album__caption {
  position: absolute;
  left: clamp(52px, 13%, 104px);
  right: clamp(52px, 13%, 104px);
  bottom: 0;
  margin: 0;
  padding: 8px 14px;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  font-size: 0.85rem;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
  background: linear-gradient(to top, rgba(0, 0, 0, 0.55), transparent);
}
.album__caption-text { overflow-wrap: anywhere; }
.album__caption-tally {
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.75);
}

.album__strip {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 14px 20px 16px;
  border-top: 1px solid var(--line);
}

.album__thumb {
  display: block;
  width: 76px;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  cursor: pointer;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  filter: grayscale(0.5) brightness(0.7);
  transition: filter 0.18s var(--ease), border-color 0.18s var(--ease);
}
/* Filled, like the mosaic: these are 76px navigation marks, and the pane
   above them is where the photo is actually looked at. */
.album__thumb img { width: 100%; height: 100%; object-fit: cover; }
.album__thumb:hover { filter: none; border-color: var(--line-bright); }

/*
 * The selected thumbnail is the one in colour.
 *
 * This needs one selector per position, because the checked radio and the
 * thumbnail it belongs to sit in different parts of the tree and CSS has no
 * way to say "the nth of these matches the nth of those". Switching photos
 * does NOT depend on any of it, so an album longer than this list still works
 * — it just stops marking which thumbnail is current.
 */
.album__viewer:has(#shot-0:checked)  ~ .album__strip .album__thumb:nth-of-type(1),
.album__viewer:has(#shot-1:checked)  ~ .album__strip .album__thumb:nth-of-type(2),
.album__viewer:has(#shot-2:checked)  ~ .album__strip .album__thumb:nth-of-type(3),
.album__viewer:has(#shot-3:checked)  ~ .album__strip .album__thumb:nth-of-type(4),
.album__viewer:has(#shot-4:checked)  ~ .album__strip .album__thumb:nth-of-type(5),
.album__viewer:has(#shot-5:checked)  ~ .album__strip .album__thumb:nth-of-type(6),
.album__viewer:has(#shot-6:checked)  ~ .album__strip .album__thumb:nth-of-type(7),
.album__viewer:has(#shot-7:checked)  ~ .album__strip .album__thumb:nth-of-type(8),
.album__viewer:has(#shot-8:checked)  ~ .album__strip .album__thumb:nth-of-type(9),
.album__viewer:has(#shot-9:checked)  ~ .album__strip .album__thumb:nth-of-type(10),
.album__viewer:has(#shot-10:checked) ~ .album__strip .album__thumb:nth-of-type(11),
.album__viewer:has(#shot-11:checked) ~ .album__strip .album__thumb:nth-of-type(12),
.album__viewer:has(#shot-12:checked) ~ .album__strip .album__thumb:nth-of-type(13),
.album__viewer:has(#shot-13:checked) ~ .album__strip .album__thumb:nth-of-type(14),
.album__viewer:has(#shot-14:checked) ~ .album__strip .album__thumb:nth-of-type(15),
.album__viewer:has(#shot-15:checked) ~ .album__strip .album__thumb:nth-of-type(16),
.album__viewer:has(#shot-16:checked) ~ .album__strip .album__thumb:nth-of-type(17),
.album__viewer:has(#shot-17:checked) ~ .album__strip .album__thumb:nth-of-type(18),
.album__viewer:has(#shot-18:checked) ~ .album__strip .album__thumb:nth-of-type(19),
.album__viewer:has(#shot-19:checked) ~ .album__strip .album__thumb:nth-of-type(20) {
  filter: none;
  border-color: var(--eye-c);
  box-shadow: 0 0 0 1px var(--eye-c);
}

@media (prefers-reduced-motion: reduce) {
  .album__pane { transition: none; }
}

@media (max-width: 560px) {
  .album__title  { padding: 12px 16px 10px; }
  .album__strip  { padding: 12px 16px 14px; gap: 6px; }
  .album__thumb  { width: 62px; }
}

/* --------------------------------------------------------------------------
   Traits — chips on a cat's profile
   -------------------------------------------------------------------------- */
.chips {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 20px;
  padding: 0;
}

.chip {
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.02em;
  color: var(--text);
  background: color-mix(in srgb, var(--eye-c) 8%, var(--panel-inset));
  border: 1px solid color-mix(in srgb, var(--eye-c) 26%, transparent);
  border-radius: 2px;
  padding: 4px 9px;
}

/* --------------------------------------------------------------------------
   The home page

   Four blocks, all drawn from data that already exists. Each one is a plain
   list underneath; the layout is the only thing CSS adds.
   -------------------------------------------------------------------------- */
.home-block { margin: 0 0 48px; }

.home-block__title {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 18px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}

/* --------------------------------------------------------------------------
   The glass card

   A frosted pane with a hairline edge, which grows a little when you point at
   it and tips as you press. The original is a light card on a light page; on
   a page this dark a 58% grey fill would be a glowing white box, so the frost
   is a few percent of white instead and the tint comes from the cat.

   That tint is the reason this suits the site rather than merely decorating
   it: every cat already owns an accent colour, so each card is frosted in its
   own cat's colour and the row stops being six identical tiles.

   The blur has something to work on because the card is padded — the page's
   dot field shows through the frame around the photo. Without the padding
   the photo would cover the glass and there would be nothing to frost.
   -------------------------------------------------------------------------- */
.glass {
  position: relative;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 17px;
  background: color-mix(in srgb, var(--eye-c) 7%, rgba(255, 255, 255, 0.05));
  backdrop-filter: blur(6px);
  box-shadow: 10px 14px 34px rgba(0, 0, 0, 0.5);
  user-select: none;
  transition: transform 0.3s var(--ease),
              border-color 0.3s var(--ease),
              box-shadow 0.3s var(--ease);
}
/*
 * Growing and tipping belong to something you can press, so they are scoped
 * to links and buttons. A glass panel that is only a panel — the family
 * groups, say — keeps the surface and stays where it is: a box that flinches
 * when the pointer crosses it promises a click it does not have.
 *
 * The original brightens its edge on hover by going to black on a white page.
 * The equivalent here is the cat's own colour, which is both more contrast
 * and more information than plain white would be.
 */
a.glass:hover,
button.glass:hover {
  border-color: var(--eye-c);
  transform: scale(1.05);
  box-shadow: 12px 18px 44px rgba(0, 0, 0, 0.62);
  z-index: 2;   /* grown, it must sit above its neighbours, not under them */
}
a.glass:active,
button.glass:active { transform: scale(0.95) rotateZ(1.7deg); }

/*
 * Growing and tipping is motion, and the global reduced-motion block only
 * shortens transitions — which would leave the card snapping to a new size
 * instead of easing to it. Here it simply does not move.
 */
@media (prefers-reduced-motion: reduce) {
  a.glass:hover, button.glass:hover,
  a.glass:active, button.glass:active { transform: none; }
}

/* ---- Everyone ---------------------------------------------------------- */
.roster {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 16px;
}

/* The card itself is .glass; this is only what makes it a link. The full
   height is so a card in a row with a two-line name is still the same card,
   not a shorter one with a gap under it. */
.roster__link {
  display: block;
  height: 100%;
  padding: 8px;
  text-decoration: none;
}

/*
 * A square frame, identical on every card, with the photo sitting inside it
 * at its own shape and its own size.
 *
 * This has been round the houses. A fixed 4:3 box with the photo letterboxed
 * against a dark inset put black bars down the side of every portrait photo.
 * Giving each card the photo's own shape removed the bars but made the cards
 * different heights, which is worse: a roster is a row of equals.
 *
 * So the frame is fixed and the photo varies within it — and crucially the
 * frame has no background of its own. What shows around a photo that does not
 * fill it is the card's own frosted glass, the same glass already showing on
 * all four sides thanks to the padding. That reads as a photo centred in a
 * card, which it is, rather than as a box with something missing.
 *
 * Square rather than 4:3 because it is the shape that gives away least in
 * both directions: a landscape photo loses a little height, a portrait a
 * little width, and neither is left small.
 */
/*
 * The photo is taken out of the flow to sit in the frame.
 *
 * Left in the flow it defeats the frame: the image's own height feeds back
 * into the box that is supposed to be constraining it, and the square gives
 * way to whatever shape the photo happens to be — which is the bug this was
 * meant to fix. Absolutely positioned, the photo cannot push on anything.
 *
 * inset:0 with margin:auto and no stated width or height is what centres a
 * thing of its own natural size in both directions at once.
 */
.roster__photo {
  display: block;   /* it is a <span>, and an inline box has no shape to set */
  position: relative;
  aspect-ratio: 1 / 1;
}
.roster__photo img,
.roster__none {
  position: absolute;
  inset: 0;
  margin: auto;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  border-radius: 11px;   /* the card's radius less its padding */
}

/* Stand-in for a cat with no photo: plainly a graphic, not a fake picture.
   It is also what the frost has to work with on a cat with no picture, so
   the glass reads as glass rather than as an empty pane. */
.roster__none {
  /* Position, size and radius come from the rule it shares with the photo
     above; unlike a photo it has no shape of its own, so it fills the frame. */
  width: 100%;
  height: 100%;
  background: radial-gradient(circle at 50% 45%,
    color-mix(in srgb, var(--eye-c) 22%, transparent) 0%, transparent 62%);
}

.roster__name {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 15px;
  color: var(--text-strong);
  padding: 9px 5px 0;
}

.roster__meta {
  display: block;
  font-size: 11.5px;
  color: var(--text-dim);
  padding: 2px 5px 5px;
}

/* ---- Family tree -------------------------------------------------------- */
/*
 * Flex rather than equal grid columns. The panels have to fill the row — as a
 * plain wrapping row they hugged the left and the block stopped halfway across
 * the page, which read as something failing to load. But equal columns are
 * wrong too: a parent with three kittens needs more width than one with a
 * single kitten, and equal tracks squeeze the wide tree while the narrow one
 * sits half empty. Growing from a content-sized base gives each family the
 * room its own tree asks for.
 */
.tree {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: start;
}

/* The panel is .glass, same as a cat card. Only the padding is its own. */
.tree__group {
  /*
   * Basis auto, not a fixed 260px. With a fixed basis every panel got an
   * equal share of the row, so a parent with three kittens was squeezed to
   * the same width as one with a single kitten — the kittens fell back to
   * their minimum widths, and unequal widths are exactly what knocks the
   * stem out of line with the middle kitten. Starting from what each tree
   * actually needs and sharing the leftover equally keeps both true.
   */
  flex: 1 1 auto;
  min-width: 200px;
  padding: 18px 18px 22px;

  /*
   * The connectors belong to the family, so they are drawn in the family's
   * colour rather than in the neutral rule colour. At --line-bright they were
   * a near-black hairline on a translucent panel over a starfield, which is
   * about the least visible thing a line can be; mixed most of the way to the
   * accent, with a little of its glow, they read as part of the diagram.
   */
  --tree-line: color-mix(in srgb, var(--eye-c) 65%, var(--line-bright));
  --tree-glow: 0 0 6px color-mix(in srgb, var(--eye-c) 28%, transparent);
}

/* --------------------------------------------------------------------------
   The tree itself

   Parent on top, a stem down to a horizontal bar, and a drop from that bar
   into each kitten — the shape everyone already knows a family tree by. It
   used to be an indented list with a rule down its left, which is a file
   listing rather than a tree: it says these names are under that one without
   ever saying they are siblings of each other.

   Drawn with borders on pseudo-elements, so it needs no images and nothing to
   keep in step when a cat is added — the bar is assembled from one segment per
   child, and the two on the ends stop at their own centres.
   -------------------------------------------------------------------------- */
.tree__top {
  position: relative;
  text-align: center;
  padding-bottom: 22px;   /* the stem hangs in here */
}
/* The stem, from the parent down to the bar. */
.tree__top::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 1px;
  height: 22px;
  background: var(--tree-line);
  box-shadow: var(--tree-glow);
}

/* A cat related to nobody: a family of one, so no stem — a line down to
   nothing would be the diagram claiming something the data does not say. */
.tree__top--alone { padding-bottom: 0; }
.tree__top--alone::after { display: none; }

.tree__node {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--text);
  text-decoration: none;
  /*
   * The edge carries the cat's colour standing still, not a grey that turns
   * green when you point at it. At 30% it mixed away to something you could
   * not tell from the neutral rule colour, so the whole palette only showed
   * up under the pointer — which reads as a hover effect rather than as whose
   * colour it is. Mixed with --line-bright rather than with transparent, so
   * it stays opaque on the panel instead of washing into it.
   */
  border: 1px solid color-mix(in srgb, var(--eye-c) 72%, var(--line-bright));
  border-radius: 9px;
  padding: 5px 11px;
  transition: border-color 0.18s var(--ease),
              background 0.18s var(--ease),
              box-shadow 0.18s var(--ease),
              color 0.18s var(--ease);
}

/*
 * Every cat wears its own colour standing still, not only under the pointer.
 * The kittens were grey until you happened to hover one, which made the
 * colours look like a hover effect rather than like whose colour it is.
 *
 * The dot carries it. Full strength, small enough that four in a row do not
 * fight each other, and it leaves the chip's edge free to say something else
 * — which is what keeps a parent telling apart from a kitten now that both
 * are coloured.
 */
.tree__node .pupil {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--eye-c);
  box-shadow: 0 0 7px color-mix(in srgb, var(--eye-c) 70%, transparent);
  flex: none;
}
/* Hover still has somewhere to go, now that resting is already coloured:
   full strength, a tinted fill, and a glow the resting chip does not have. */
.tree__node:hover {
  border-color: var(--eye-c);
  background: color-mix(in srgb, var(--eye-c) 12%, transparent);
  box-shadow: 0 0 12px color-mix(in srgb, var(--eye-c) 28%, transparent);
  color: var(--text-strong);
}
/* The parent is the one the group is about, so it carries its colour rather
   than only a brighter edge — the hierarchy should survive a squint. */
.tree__node--parent {
  font-size: 13.5px;
  border-color: var(--eye-c);
  background: color-mix(in srgb, var(--eye-c) 12%, transparent);
  color: var(--text-strong);
}

/* A branch is a cat with everything under her. It nests, so this is also what
   sits inside each <li> of the row below. */
.tree__branch { display: block; }

.tree__children {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: center;
  flex-wrap: nowrap;
}

/*
 * The horizontal gap is padding on each child rather than a flex gap, because
 * the bar is built out of the children's own segments: with a real gap the bar
 * would come out dashed, one break per kitten.
 *
 * Equal tracks, because every alignment in the diagram depends on it. The
 * stem hangs from the middle of the panel, and the drops hang from the middle
 * of each kitten — so with content-width tracks a short name next to a long
 * one puts the two lines a few pixels out of true, and the stem into the
 * middle kitten visibly jogs. Sharing the row equally makes the row
 * symmetrical, which puts the bar's midpoint, the panel's centre and the
 * middle kitten's centre all on the same vertical for any number of kittens.
 * The chip stays its own size, centred in its track.
 */
.tree__children > li {
  flex: 1 1 0;
  /*
   * min-width: 0, and this is the whole reason the lines meet.
   *
   * A flex item will not shrink below its content by default, so when the row
   * is exactly as wide as its names need — which it is as soon as the panel is
   * at all tight — every track sits at its own minimum and a long name gets a
   * wider track than a short one. The middle kitten's centre then drifts off
   * the parent's stem by a few pixels, which is small enough to look like a
   * mistake rather than a layout.
   *
   * With no minimum the tracks are exactly equal whatever the names are, so
   * the middle of the row, the middle of the bar and the middle kitten are the
   * same vertical by construction. A name wider than its share overhangs into
   * the padding either side rather than pushing its neighbours over.
   */
  min-width: 0;
  position: relative;
  padding: 16px 9px 0;
  text-align: center;
}

/* One segment of the bar. */
.tree__children > li::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--tree-line);
  box-shadow: var(--tree-glow);
}
/* The ends stop at their own centres, so the bar spans first to last and no
   further — otherwise it overshoots into thin air on both sides. */
.tree__children > li:first-child::before { left: 50%; }
.tree__children > li:last-child::before  { right: 50%; }
/* An only child needs no bar at all: the stem drops straight into it. */
.tree__children > li:only-child::before  { display: none; }

/* The drop from the bar into the kitten. */
.tree__children > li::after {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  width: 1px;
  height: 16px;
  background: var(--tree-line);
  box-shadow: var(--tree-glow);
}

/*
 * Narrow screens get the old list back.
 *
 * A tree is a row, and a row of kittens does not fit on a phone — it would
 * either overflow or squeeze the names to a column of letters. Stacked with a
 * rule down the side it stays readable, and it is the same information.
 */
@media (max-width: 560px) {
  .tree__top { text-align: left; padding-bottom: 14px; }
  .tree__top::after { display: none; }

  .tree__children {
    flex-direction: column;
    align-items: flex-start;
    padding-left: 20px;
    border-left: 1px solid var(--tree-line);
  }
  .tree__children > li { padding: 4px 0; text-align: left; }
  .tree__children > li::before,
  .tree__children > li::after { display: none; }
}

/* A .tree__loose row listed the unrelated cats under the tree behind an "on
   their own" label. They stand in the row now, each as a family of one, so
   both it and its label are gone. */

/* ---- Timeline ----------------------------------------------------------- */
/*
 * One grid for the whole list, not one grid per row.
 *
 * With a grid per row, the 1fr track was sized by whatever was left after
 * that row's name — so "Wizzyman" left a narrower track than "Zorro", and
 * 30% of a narrower track is a different pixel position. Three cats born on
 * the same day got three different dots. Sharing one grid makes every column
 * the same width down the whole list, which is what lets the dots be
 * compared at all.
 */
.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* Year, name, axis. The name column is auto so it takes the width of the
     longest name once, for every row — which is what keeps the axis starting
     at the same place down the whole list. */
  grid-template-columns: 4ch auto 1fr;
  align-items: center;
  gap: 10px 14px;
}

/* The row is a grouping in the markup, not a box: its three cells belong to
   the list's grid so they line up with every other row's. */
.timeline__row { display: contents; }

.timeline__year {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.timeline__track {
  position: relative;
  height: 1px;
  /* Room for a dot sitting at either extreme, which is otherwise half off
     the end of its own track. */
  margin-inline: 6px;
  /* Fading rather than flat: the axis reads as running out from the name it
     belongs to, and a rule that simply stops dead against a starfield looks
     like a border somebody forgot to remove. */
  background: linear-gradient(to right,
    var(--line-bright),
    color-mix(in srgb, var(--line-bright) 25%, transparent));
}

.timeline__dot {
  position: absolute;
  top: 50%;
  width: 9px;
  height: 9px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: var(--eye-c);
  box-shadow: 0 0 10px color-mix(in srgb, var(--eye-c) 60%, transparent);
}
/* A faint ring, so a dot on a long empty rule reads as a marked position
   rather than as a speck of dust on the screen. */
.timeline__dot::after {
  content: '';
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--eye-c) 30%, transparent);
}

/*
 * The dot's position is a class, not an inline style: the Content-Security-
 * Policy allows no inline styles at all, so the view rounds each year to the
 * nearest 5% and picks one of these.
 */
.offset-0   { left: 0%; }
.offset-5   { left: 5%; }
.offset-10  { left: 10%; }
.offset-15  { left: 15%; }
.offset-20  { left: 20%; }
.offset-25  { left: 25%; }
.offset-30  { left: 30%; }
.offset-35  { left: 35%; }
.offset-40  { left: 40%; }
.offset-45  { left: 45%; }
.offset-50  { left: 50%; }
.offset-55  { left: 55%; }
.offset-60  { left: 60%; }
.offset-65  { left: 65%; }
.offset-70  { left: 70%; }
.offset-75  { left: 75%; }
.offset-80  { left: 80%; }
.offset-85  { left: 85%; }
.offset-90  { left: 90%; }
.offset-95  { left: 95%; }
.offset-100 { left: 100%; }

.timeline__name {
  font-family: var(--font-display);
  font-size: 14px;
  color: var(--text);
  text-decoration: none;
}
.timeline__name:hover { color: var(--text-strong); }

/* ---- Every photo -------------------------------------------------------- */
.mosaic {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 6px;
}

/* Square on purpose, and filled rather than fitted. A mosaic is a wall of
   equal tiles — that is the whole form — so here the photo yields to the tile
   instead of the other way round, and a square crop of a centred cat loses
   nothing that matters. The full photo is one click away on the cat's page. */
.mosaic__tile {
  display: block;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: var(--panel-inset);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  filter: grayscale(0.4) brightness(0.82);
  transition: filter 0.18s var(--ease), border-color 0.18s var(--ease);
}
.mosaic__tile:hover { filter: none; border-color: var(--eye-c); }
.mosaic__tile img { width: 100%; height: 100%; object-fit: cover; }

@media (max-width: 560px) {
  .home-block { margin-bottom: 34px; }
  .tree { gap: 18px; }
  /* The three columns stay; the track just gets shorter. Dropping to two
     would break the alignment the shared grid exists to provide. */
  .timeline { gap: 6px 10px; }
}

/* --------------------------------------------------------------------------
   Family tree: someone who is not here

   A cat with no page of her own — a mother who was never on this site,
   recorded so her kittens do not appear to have come from nowhere.

   Dashed and unlinked rather than marked with a question mark: the name is
   not the uncertain part. Who she was is known; what became of her is not,
   and that is what the line underneath says.
   -------------------------------------------------------------------------- */
.tree__node--gone {
  border-style: dashed;
  border-color: var(--text-faint);
  color: var(--text-dim);
  cursor: default;
}
.tree__node--gone .pupil { opacity: 0.4; }

.tree__note {
  display: block;
  max-width: 34ch;
  /* Centred under the parent it belongs to, above the stem. */
  margin: 10px auto 0;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-faint);
}

/* A .tree__legend explained the dashed chips in a sentence under the trees.
   The chip and its own note say it; the caption is gone and so is this. */

/* --------------------------------------------------------------------------
   View transitions

   The site is genuinely multi-page — every cat is a real URL and every tab is
   a real link, which is what makes it work without JavaScript and what makes
   the back button behave. The one cost of that is a hard cut on every click.

   This buys the smoothness back without giving any of it up: it is CSS, so it
   passes a Content-Security-Policy of script-src 'none' untouched, and a
   browser that does not implement it simply navigates the way it does today.

   Each cat's photo carries a name, so the browser recognises the thumbnail on
   the landing page and the portrait on the cat's own page as the same thing
   and moves one into the other rather than fading between them. The names are
   a fixed set of classes rather than per-element styles, because the CSP
   forbids the inline style that would otherwise carry them — the same
   constraint the accent colours work around. See Cats::morphClass().
   -------------------------------------------------------------------------- */
@view-transition { navigation: auto; }

/* Chrome only pairs elements whose names match, so these are shared between
   pages by position in the cat ordering, not by anything page-local. */
.vt-1  { view-transition-name: cat-1; }
.vt-2  { view-transition-name: cat-2; }
.vt-3  { view-transition-name: cat-3; }
.vt-4  { view-transition-name: cat-4; }
.vt-5  { view-transition-name: cat-5; }
.vt-6  { view-transition-name: cat-6; }
.vt-7  { view-transition-name: cat-7; }
.vt-8  { view-transition-name: cat-8; }
.vt-9  { view-transition-name: cat-9; }
.vt-10 { view-transition-name: cat-10; }
.vt-11 { view-transition-name: cat-11; }
.vt-12 { view-transition-name: cat-12; }
.vt-13 { view-transition-name: cat-13; }
.vt-14 { view-transition-name: cat-14; }
.vt-15 { view-transition-name: cat-15; }
.vt-16 { view-transition-name: cat-16; }
.vt-17 { view-transition-name: cat-17; }
.vt-18 { view-transition-name: cat-18; }
.vt-19 { view-transition-name: cat-19; }
.vt-20 { view-transition-name: cat-20; }

/* The bar is the same bar on both pages, so it holds still instead of being
   cross-faded along with the content underneath it. */
.site-nav { view-transition-name: site-nav; }

/*
 * The photo changes shape as it moves — a 4:3 card becoming a tall portrait —
 * so the two snapshots are told to fill their box rather than stretch, which
 * is what stops the cat distorting mid-flight.
 */
::view-transition-old(*),
::view-transition-new(*) {
  object-fit: contain;
}

::view-transition-group(*) {
  animation-duration: 260ms;
  animation-timing-function: var(--ease);
}

/*
 * Reduced motion switches it off outright rather than merely shortening it.
 * The global block above zeroes animation durations through a universal
 * selector, which does not reach these pseudo-elements — they are not in the
 * document tree.
 */
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* The link out of the sampled mosaic. Only rendered when photos were left
   out, so it never appears as a dead-end. */
.mosaic__more {
  margin: 12px 0 0;
  font-size: 13px;
  text-align: right;
}

/* ==========================================================================
   The home-page orbit — a slowly turning ring of the cats.

   Pure CSS, because the site ships no JavaScript: the ring is one 3D
   transform and one linear keyframe. Every value the ring needs that would
   normally be an inline style is a class instead, because the CSP is
   `style-src 'self'` with no inline concession — the same wall the accent
   colours hit, up at the top of this file:

     --i   a card's place in the ring     .orbit__card--0 … --11
     --n   how many cards there are       .orbit--n2 … --n12
     --r   the radius that spaces them    set alongside --n

   The card transform multiplies them with calc(), so the browser does the
   arithmetic, not the template. Colour is the cat's own --eye-c, carried by
   the same accent-* class every other cat card wears — so each photo glows
   in its cat's colour without a single per-card rule.
   ========================================================================== */

.orbit {
  --u: 1.35;                     /* one knob sizes the whole turntable */
  position: relative;
  /* Above the star layers (fixed, z-index: 1), like every other content
     block on the site — otherwise the sky drifts across the photos. The
     gaps between cards still show it; the cards themselves no longer do. */
  z-index: 2;
  width: 100%;
  /*
   * Fill the page the way the empty stage did, so the footer's line stays at
   * the bottom of the screen rather than riding up under a short ring. The
   * 160px is the bar plus the footer, the same subtraction .stage--empty
   * makes; whichever is taller wins, so a tall ring gets its room and a tall
   * screen still pins the footer to the bottom.
   */
  min-height: max(calc(400px * var(--u)), calc(100vh - 160px));
  display: flex;
  align-items: center;
  justify-content: center;
  /* The ring is wider than this box; the sides are clipped, so cards appear
     to enter and leave at the edges instead of piling into a strip. */
  overflow: hidden;
  perspective: 1300px;
}

.orbit__ring {
  --card-w: calc(154px * var(--u));
  --card-h: calc(204px * var(--u));
  position: relative;
  width: var(--card-w);
  height: var(--card-h);
  transform-style: preserve-3d;
  transform: rotateX(-11deg);                 /* the resting tilt */
  animation: orbit-spin 46s linear infinite;
}

@keyframes orbit-spin {
  from { transform: rotateX(-11deg) rotateY(0); }
  to   { transform: rotateX(-11deg) rotateY(360deg); }
}

/*
 * Pause when someone wants to look at — or reach — a card. Only a front-facing
 * link counts (:has(.orbit__hit:hover)), so hovering the gaps or a far card
 * does not stop the ring; a link taking keyboard focus pauses it too, the stop
 * mechanism for anyone not using a pointer. Reduced-motion stops it outright,
 * through the universal sweep earlier in this file.
 */
.orbit__ring:has(.orbit__hit:hover),
.orbit__ring:focus-within { animation-play-state: paused; }

.orbit__card {
  position: absolute;
  inset: 0;
  border-radius: 12px;
  overflow: hidden;
  border: 2px solid color-mix(in srgb, var(--eye-c) 60%, transparent);
  box-shadow: 0 0 24px color-mix(in srgb, var(--eye-c) 32%, transparent);
  background: var(--panel-inset);
  /* the i-th card of n, turned around the ring and pushed out to the radius */
  transform: rotateY(calc(var(--i) * 360deg / var(--n))) translateZ(var(--r));
  /*
   * This is the picture layer — every cat, the whole turning ring, front and
   * back. It is deliberately click-through: interaction lives on .orbit__hit
   * above it. A far card's mirror-image is here to look at, not to touch.
   */
  pointer-events: none;
}

.orbit__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Centred unless a per-cat focus class below sets one or both custom
     properties — composable per axis, which a single object-position value
     could not be (see Cats::focusClass() and the "Per-cat framing" comment
     further down for why this is a class rather than an inline style). */
  object-position: var(--focus-x, 50%) var(--focus-y, 50%);
}

/* The eyeshine edge: each photo darkens into its own cat's colour. */
.orbit__card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 42%,
      transparent 52%,
      color-mix(in srgb, var(--eye-c) 42%, transparent) 100%);
  pointer-events: none;
}

/*
 * The interaction layer: one transparent link per cat, turned to the same
 * place on the ring but sitting a hair in front of its picture (translateZ +
 * a few px) so a front card's link is the thing under the pointer.
 *
 * backface-visibility: hidden is what makes the far half of the ring inert —
 * a back-facing link is neither drawn nor hit-tested. It works here, on a
 * direct child of the 3D ring, where the same property on a nested child does
 * not: the exclusion from hit-testing only applies to elements the ring
 * itself turns. So the three front links are live and the three at the back
 * cannot be hovered or clicked, while every picture stays on screen.
 */
.orbit__hit {
  position: absolute;
  inset: 0;
  border: 2px solid transparent;   /* lights up on hover, aligned to the card */
  border-radius: 12px;
  overflow: hidden;                /* clips the name band to the rounded corner */
  transform: rotateY(calc(var(--i) * 360deg / var(--n))) translateZ(calc(var(--r) + 3px));
  backface-visibility: hidden;
  -webkit-tap-highlight-color: transparent;
}

.orbit__hit:hover,
.orbit__hit:focus-visible {
  border-color: var(--eye-c);
  box-shadow: 0 0 30px color-mix(in srgb, var(--eye-c) 55%, transparent);
  outline: none;
}

/*
 * The name, revealed on the link.
 *
 * There is no way to know in CSS which card is at the front of a turning ring
 * — that would take JavaScript, which this site does not ship — so the name
 * appears when a link is pointed at or focused instead, sliding up on a band
 * of the cat's own colour. It rides the front-only hit layer, so it can only
 * show on a front card; the ring pauses on that same hover, holding the card
 * still long enough to read. aria-hidden because the link already carries the
 * cat's name as its aria-label; this is the sighted echo of it.
 */
.orbit__name {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  padding: 26px 10px 12px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: calc(16px * var(--u));
  letter-spacing: 0.01em;
  text-align: center;
  color: var(--text-strong);
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.85);
  background: linear-gradient(to top,
      color-mix(in srgb, var(--eye-c) 55%, #000) 0%,
      color-mix(in srgb, var(--eye-c) 30%, #000) 42%,
      transparent 100%);
  opacity: 0;
  transform: translateY(55%);
  transition: opacity .28s var(--ease), transform .28s var(--ease);
  pointer-events: none;
}

.orbit__hit:hover .orbit__name,
.orbit__hit:focus-visible .orbit__name {
  opacity: 1;
  transform: translateY(0);
}

/*
 * Per-cat framing, admin-editable (Cats::focusClass()). object-fit: cover
 * centres the crop, which is right for a cat sitting in the middle of their
 * photo — most of them. A cat off to one side needs the crop aimed at them;
 * these compose independently per axis via the custom properties .orbit__img
 * reads above, so a card can carry one, both, or neither.
 */
.orbit__card--focus-x-left  .orbit__img { --focus-x: 20%; }
.orbit__card--focus-x-right .orbit__img { --focus-x: 80%; }
.orbit__card--focus-y-top    .orbit__img { --focus-y: 25%; }
.orbit__card--focus-y-bottom .orbit__img { --focus-y: 75%; }

/* Index — where each card sits around the ring. */
.orbit__card--0  { --i: 0; }
.orbit__card--1  { --i: 1; }
.orbit__card--2  { --i: 2; }
.orbit__card--3  { --i: 3; }
.orbit__card--4  { --i: 4; }
.orbit__card--5  { --i: 5; }
.orbit__card--6  { --i: 6; }
.orbit__card--7  { --i: 7; }
.orbit__card--8  { --i: 8; }
.orbit__card--9  { --i: 9; }
.orbit__card--10 { --i: 10; }
.orbit__card--11 { --i: 11; }

/* Count and radius — how many cards, and how far out to space them so they
   do not overlap. The radius grows with the count; --u shrinks both on a
   narrow screen. */
.orbit--n2  { --n: 2;  --r: calc(155px * var(--u)); }
.orbit--n3  { --n: 3;  --r: calc(80px  * var(--u)); }
.orbit--n4  { --n: 4;  --r: calc(115px * var(--u)); }
.orbit--n5  { --n: 5;  --r: calc(150px * var(--u)); }
.orbit--n6  { --n: 6;  --r: calc(190px * var(--u)); }
.orbit--n7  { --n: 7;  --r: calc(225px * var(--u)); }
.orbit--n8  { --n: 8;  --r: calc(260px * var(--u)); }
.orbit--n9  { --n: 9;  --r: calc(295px * var(--u)); }
.orbit--n10 { --n: 10; --r: calc(330px * var(--u)); }
.orbit--n11 { --n: 11; --r: calc(365px * var(--u)); }
.orbit--n12 { --n: 12; --r: calc(400px * var(--u)); }

@media (max-width: 900px) { .orbit { --u: 1.05; } }
@media (max-width: 640px) { .orbit { --u: 0.82; } }
@media (max-width: 430px) { .orbit { --u: 0.64; } }

/* One cat with one photo is not a ring — just the portrait, centred. */
.orbit-solo {
  display: block;
  width: min(280px, 80vw);
  margin: clamp(1.5rem, 6vw, 4rem) auto;
  border-radius: 12px;
  overflow: hidden;
  border: 2px solid color-mix(in srgb, var(--eye-c) 60%, transparent);
  box-shadow: 0 0 24px color-mix(in srgb, var(--eye-c) 30%, transparent);
}
.orbit-solo img { display: block; width: 100%; height: auto; }
