/* This file is executed as a Go template by layouts/_default/baseof.html so the
   cloud backdrop below can reference a build-time WebP variant, which means a
   literal double curly brace anywhere in here would be parsed as a template
   action and break the build. Only .cloudBackdrop is interpolated. */
:root {
  color-scheme: light;
  --wave-gutter: clamp(8.75rem, 16vw, 13.5rem);
  --sky: #5aa8dc;
  --ice: #d7fbff;
  --cyan: #73f4ff;
  --blue: #365fc6;
  --violet: #6d2c95;
  --lavender: #b9a8ff;
  --pink: #ffd6f5;
  --magenta: #ff00e6;
  --ink: #271452;
  --banana: #fff9b1;
  /* Text-bearing panels are fully opaque on purpose: the striped
     index-window behind them otherwise bleeds through and makes prose hard
     to read. Keep any translucency to decorative surfaces only. */
  --panel: #d8f7ff;
  --panel-strong: #e7fcff;
  --line: rgba(231, 252, 255, 0.9);
  --shadow: rgba(65, 24, 127, 0.28);
  /* Link states are tokens rather than literals because a legible accent
     depends on what the link sits on, and links here inherit their colour
     from whatever panel holds them. These two values clear 4.5:1 on every
     panel surface (--panel, --panel-strong, --ice, --pink, white); the two
     surfaces they don't clear — the mid-blue page background under the
     footer, and the violet link-rail — re-point them further down. */
  --link-hover: #a3006d;
  --link-active: #6d2c95;
  /* One near-black focus ring for the whole site. Every ring here is drawn
     outside its element (outline-offset), so what it needs contrast against
     is the page behind the control, not the control's own fill — and that is
     light or mid-tone everywhere, including behind the dark link-rail.
     Measured 5.2:1 (rail) to 16.8:1 (panels). */
  --focus-ring: #1a0d33;
  /* How far past the viewport edges the fixed backdrop layers paint.

     A mobile browser with a dynamic toolbar (Firefox for Android especially,
     and more so with the toolbar moved to the bottom) doesn't just resize the
     viewport as the bar hides and shows — it translates `position: fixed`
     boxes so bottom-anchored UI clears the bar. A layer sized exactly to the
     viewport gets shifted off one edge, and the strip it vacates falls through
     to the raw page gradient for the length of the animation. On /blog/ that
     strip lands squarely on the footer, which sits at the document's bottom
     edge with nothing under it but sky, so the footer appears to snap back out
     of the archive's paper wash and into the wirenook blue every time the bar
     comes back.

     Overscanning the sky costs a little extra paint on layers that are already
     composited, and means no shift of a toolbar's size can expose anything.
     The 5rem floor clears a Firefox for Android toolbar (~56px) on a short
     viewport, where 12vh alone wouldn't. */
  --sky-overscan: max(5rem, 12vh);
  font-family: Unique, "Trebuchet MS", "Century Gothic", Arial, sans-serif;
}

@font-face {
  font-family: Unique;
  src: url("fonts/Unique.ttf") format("truetype");
  font-display: swap;
}

@font-face {
  font-family: Atkinson-Regular;
  src: url('fonts/AtkinsonHyperlegible-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: Atkinson-Italic;
  src: url('fonts/AtkinsonHyperlegible-Italic.ttf') format('truetype');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: Atkinson-Bold;
  src: url('fonts/AtkinsonHyperlegible-Bold.ttf') format('truetype');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: Atkinson-BoldItalic;
  src: url('fonts/AtkinsonHyperlegible-BoldItalic.ttf') format('truetype');
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

* {
  box-sizing: border-box;
}

html {
  background: linear-gradient(155deg, #65b8e6 0%, #519ed5 48%, #8abbe9 100%);
}

body {
  min-height: 100vh;
  margin: 0;
  color: var(--ink);
  background: linear-gradient(155deg, #65b8e6 0%, #519ed5 48%, #8abbe9 100%);
  overflow-x: hidden;
}

/* Firefox's elastic overscroll bounce (trackpad scroll past the top/bottom)
   reveals a strip of raw browser canvas beyond the rendered document that CSS
   backgrounds can't paint into, showing white. Disabling the bounce keeps
   scrolling clamped at the document edge instead.

   Fine pointers only, because on the root this property does more than suppress
   a paint artefact: it cancels the browser's own overscroll gesture, and the
   gesture at the top of a touchscreen document is pull-to-refresh. Declared
   unconditionally it took that away on every page of the site to answer a
   trackpad bounce no phone performs — a touch browser meets a downward drag at
   the top with its refresh affordance rather than by sliding the document off
   its own painted area, so there is no bare canvas there to cover. A laptop
   with a touchscreen still reports `fine` for its primary pointer, which is the
   answer this wants: it has the trackpad the rule exists for. */
@media (pointer: fine) {
  html,
  body {
    overscroll-behavior-y: none;
  }
}

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

a:hover {
  color: var(--link-hover);
}

a:active {
  color: var(--link-active);
}

/* :focus-visible, not :focus — :focus also fires on mouse-down, which made
   every click flash the hover colour. The rest of the file already uses
   :focus-visible for its components. */
a:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.page-shell {
  position: relative;
  min-height: 100vh;
  padding: clamp(1rem, 2.6vw, 2.4rem);
  padding-left: var(--wave-gutter);
  isolation: isolate;
  /* The wave-field, clouds, and the feature bunny are all meant to bleed to
     (or off) the edges. Clip horizontally so those bleeds never become a
     horizontal scrollbar. overflow-x on <body> alone doesn't hold because
     <html> stays visible; clip (not hidden) keeps the sticky link-rail working
     and doesn't force a vertical scroll container. */
  overflow-x: clip;
}

.page-shell::before {
  content: "";
  position: fixed;
  /* Bottom edge only: both background-position values below are measured from
     the top of this box, so growing that edge would slide the whole wallpaper.
     Growing downward just lets repeat-y tile further, which is what the
     overscan is for. */
  inset: 0 0 calc(var(--sky-overscan) * -1);
  z-index: -4;
  pointer-events: none;
  /* Forces its own compositor layer so Firefox doesn't clip/drop it during
     the elastic overscroll (trackpad rubber-band) bounce animation. */
  transform: translateZ(0);
  will-change: transform;
  background-image: url("/images/wirenook/volumetric-clouds_hu1071626173177197105.webp"),
  url("/images/wirenook/volumetric-clouds_hu1071626173177197105.webp");
  background-position:
    center 8vh,
    58% 78vh;
  background-repeat:
    repeat-y,
    repeat-y;
  background-size:
    min(118rem, 115vw) auto,
    min(92rem, 96vw) auto;
  opacity: .5;
}

.wave-field {
  position: fixed;
  inset: 0 auto 0 0;
  width: clamp(7rem, 12vw, 27.5rem);
  height: calc(100vh + var(--sky-overscan));
  transform: translateZ(0);
  will-change: transform;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14'%3E%3Cpolygon points='7,3 8.1,5.9 11,7 8.1,8.1 7,11 5.9,8.1 3,7 5.9,5.9' fill='rgba(139,19,214,0.75)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14'%3E%3Cpolygon points='7,3 8.1,5.9 11,7 8.1,8.1 7,11 5.9,8.1 3,7 5.9,5.9' fill='rgba(221,200,235,0.34)'/%3E%3C/svg%3E");
  background-size: 14px 14px, 14px 14px;
  background-position: 0 0, 7px 7px;
  background-repeat: repeat;
  z-index: -1;
  filter: drop-shadow(0 0 1px var(--cyan));
  opacity: 0.98;
  pointer-events: none;
  mask-image: linear-gradient(to right,
      black 40%,
      black 2%,
      transparent 100%);
}

/* Each cloud stacks three independent motions without fighting over one
   property: scroll parallax rides the `translate` property (driven by the
   --scrolly counter script.js keeps on <html>), the ambient drift keyframes
   own `transform`, and the static per-layer flips/turns live on the
   individual `scale`/`rotate` properties. Negative animation-delays and
   co-prime durations keep the five layers from ever breathing in sync. */
.cloud-field {
  position: fixed;
  z-index: -2;
  width: min(34rem, 55vw);
  height: auto;
  pointer-events: none;
  --cloud-o: 0.2;
  --drift-x: 2.6rem;
  --drift-y: 0.9rem;
  --parallax: 0.05;
  opacity: var(--cloud-o);
  translate: 0 calc(var(--scrolly, 0) * var(--parallax) * -1px);
  transform: translateZ(0);
  will-change: transform, translate, opacity;
  animation: cloudDrift 90s ease-in-out infinite;
}

.cloud-field-one {
  left: 6vw;
  bottom: -2rem;
  width: min(46rem, 58vw);
  --cloud-o: 0.18;
  --parallax: 0.11;
  --drift-x: 3.4rem;
  animation-duration: 110s;
}

.cloud-field-two {
  right: -12rem;
  bottom: 1rem;
  width: min(46rem, 58vw);
  scale: -1 1;
  --cloud-o: 0.22;
  --parallax: 0.085;
  --drift-x: 3.1rem;
  animation-duration: 133s;
  animation-delay: -47s;
}

.cloud-field-three {
  left: 28vw;
  top: 12vh;
  width: min(30rem, 42vw);
  rotate: -8deg;
  --cloud-o: 0.18;
  --parallax: 0.06;
  animation-duration: 97s;
  animation-delay: -71s;
}

.cloud-field-four {
  left: 18vw;
  bottom: 18vh;
  width: min(42rem, 50vw);
  --cloud-o: 0.13;
  --parallax: 0.075;
  animation-duration: 149s;
  animation-delay: -12s;
}

.cloud-field-five {
  right: 4vw;
  top: 18vh;
  width: min(42rem, 50vw);
  scale: -1 1;
  --cloud-o: 0.13;
  --parallax: 0.045;
  animation-duration: 121s;
  animation-delay: -88s;
}

/* A slow wandering loop rather than a metronome bob: the waypoints trace a
   lopsided ellipse while opacity swells and thins, so clouds read as forming
   and dissipating as they float through. Opacity bottoms out well above zero
   to avoid a popping in/out feel. */
@keyframes cloudDrift {

  0%,
  100% {
    transform: translateZ(0) translate3d(calc(var(--drift-x) * -1), calc(var(--drift-y) * 0.4), 0);
    opacity: calc(var(--cloud-o) * 0.45);
  }

  30% {
    transform: translateZ(0) translate3d(calc(var(--drift-x) * 0.2), calc(var(--drift-y) * -1), 0);
    opacity: var(--cloud-o);
  }

  55% {
    transform: translateZ(0) translate3d(var(--drift-x), calc(var(--drift-y) * 0.3), 0);
    opacity: calc(var(--cloud-o) * 0.85);
  }

  80% {
    transform: translateZ(0) translate3d(calc(var(--drift-x) * -0.25), var(--drift-y), 0);
    opacity: calc(var(--cloud-o) * 0.95);
  }
}

.volume-cloud {
  mix-blend-mode: soft-light;
}

.sigil-lines {
  position: fixed;
  left: 6vw;
  bottom: -4rem;
  width: 28rem;
  max-width: 62vw;
  aspect-ratio: 1;
  z-index: -1;
  border: 3px solid rgba(232, 246, 255, 0.42);
  clip-path: polygon(50% 0, 82% 86%, 20% 72%);
  transform: translateZ(0) rotate(12deg);
  will-change: transform;
}

/* Technical-drawing rulers along the top and right viewport edges, drawn by
   script.js. Purely decorative: fixed so it never takes layout space, and it
   sits with the other background layers behind all content. Same z-index as
   the wave-field but later in the DOM, so it paints above the clouds and
   waves without ever reaching the foreground. */
.tick-rulers {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
  transform: translateZ(0);
  will-change: transform;
}

.site-header {
  text-align: center;
  margin-bottom: clamp(0.85rem, 1.5vw, 1.45rem);
}

.banner-image {
  display: block;
  width: clamp(24rem, 45vw, 60rem);
  max-width: 100%;
  height: auto;
  /* Pinned to the master's ratio rather than left to whichever srcset
     candidate loaded. Rounding a 3821x1280 master to 960x322 shifts the
     derived height by 0.38px, and the 1920px candidate rounds differently
     again — enough to re-rasterize every glyph on the page below and to move
     the header when the browser swaps candidates. */
  aspect-ratio: 3821 / 1280;
  margin: 0 auto;
  object-fit: contain;
}

.site-header {
  font-size: clamp(2rem, 5vw, 3.5rem);
}

.layout-grid {
  display: grid;
  grid-template-columns: minmax(8.5rem, 13rem) minmax(20rem, 1fr) minmax(15rem, 27rem);
  gap: clamp(1rem, 2vw, 2rem);
  align-items: stretch;
  max-width: 92rem;
  margin: 0 auto;
}

.link-rail {
  /* How far each charm hangs past its button's outer edge. Shared with the
     rail's padding below the stacking breakpoint, where the rail spans the
     page and the overhang would otherwise land in the shell's gutter (or off
     the screen entirely, getting clipped by .page-shell). */
  --charm-overhang: 1.45rem;
  position: sticky;
  top: clamp(0.75rem, 2vw, 1.5rem);
  align-self: start;
  display: grid;
  align-content: start;
  gap: clamp(0.75rem, 1.5vw, 1.1rem);
  padding-top: clamp(0.2rem, 1vw, 0.65rem);
}

.rail-link {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 4.2rem;
  padding: 0.8rem 0.75rem 0.8rem 3.1rem;
  border: 2px double var(--line);
  /* The gradient used to fade out to near-transparent (alpha 0.08) on the
     right, which left the label sitting on raw page background at 1.8:1 —
     worst for the even-numbered rails, which right-align their text straight
     onto that end. It still travels violet -> blue, just dark enough the
     whole way that --ice stays above 4.5:1. */
  background: linear-gradient(100deg, rgba(62, 69, 196, 0.86), rgba(34, 88, 180, 0.85));
  color: var(--ice);
  /* Light-on-dark surface: the page-wide accents are dark and would vanish
     here, so the rail is where the yellow hover still belongs — it reads at
     6.7:1 against the violet. */
  --link-hover: var(--banana);
  --link-active: var(--cyan);
  text-decoration: none;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.36), 0 0.9rem 2rem rgba(74, 37, 132, 0.15);
  transition: transform 180ms ease, background 180ms ease;
}

.rail-link:nth-child(even) {
  justify-content: flex-end;
  padding: 0.8rem 3.1rem 0.8rem 0.75rem;
  text-align: right;
}

.rail-link:hover,
.rail-link:focus-visible {
  transform: translateX(0.35rem);
  background: linear-gradient(100deg, rgba(89, 49, 184, 0.92), rgba(40, 86, 192, 0.9));
}

/* Focus-only. The old rule handed the same --pink outline to :hover, where
   it read as a mouse affordance while only managing 1.7-2.3:1 against the
   surfaces behind it. */
.rail-link:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

.link-charm {
  position: absolute;
  left: calc(var(--charm-overhang) * -1);
  width: 3rem;
  height: 3.6rem;
  object-fit: contain;
  pointer-events: none;
}

.rail-link:nth-child(even) .link-charm {
  right: calc(var(--charm-overhang) * -1);
  left: auto;
  transform: scaleX(-1);
}

.index-window {
  min-height: 39rem;
  border: 5px double var(--line);
  background:
    repeating-linear-gradient(172deg, rgba(84, 80, 188, 0.2) 0 7px, rgba(111, 244, 255, 0.24) 8px 13px),
    linear-gradient(160deg, rgba(78, 171, 216, 0.72), rgba(191, 171, 255, 0.74));
  box-shadow: inset 0 0 4rem rgba(215, 251, 255, 0.18), 0 1.2rem 3.4rem var(--shadow);
  padding: clamp(1rem, 2vw, 1.5rem);
}

/* Wrap, because a flex line cannot shrink below its items' min-content: on a
   long post title the heading's longest word plus the "<-- Blog" chip stop
   fitting once the viewport is narrow or the reader's default font is large,
   and without wrap the chip is pushed out of the panel and clipped at the
   screen edge (.page-shell clips all horizontal spill). */
.window-topline {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 1rem;
  align-items: center;
  margin-bottom: 1rem;
}

h2,
h3,
p {
  margin-top: 0;
}

h2 {
  font-size: clamp(1.4rem, 3vw, 2.2rem);
}

/* Window headings sit directly on the index-window's stripes. At 4.9:1 the
   ratio passes for text this size, but that number is measured against an
   average the eye never sees: the stripes put ~1400 distinct colours behind
   the glyphs, in a condensed display face. The same flat chip the topline
   links and the post date use takes it to a single background colour at
   15.1:1. Scoped to .window-topline so ## headings in prose stay plain. */
.window-topline h2 {
  margin-bottom: 0;
  padding: 0.15em 0.45em;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  /* The title is the only item in the topline that gives, and a flex item can't
     shrink below its min-content width — one long word in a post title would
     push the chip beside it off the window's edge. This is what makes the row
     shrinkable all the way down, so the row never needs to wrap. */
  overflow-wrap: anywhere;
}

.status-light {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  /* These chips are two or three words at most, and they are flex items in a
     row that runs tight on a phone. Left shrinkable, "<-- Blog" in a post's
     topline broke across two lines with the arrow stranded on its own. */
  flex-shrink: 0;
  white-space: nowrap;
}

.topline-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
}

.blink-dot {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--cyan);
  box-shadow: 0 0 0.75rem var(--cyan);
  animation: blinkDot 1.1s steps(2, end) infinite;
}

.content-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.8rem;
}

.intro-panel,
.link-card,
.socials-panel {
  border: 1px solid rgba(255, 255, 255, 0.72);
  background: var(--panel);
}

.socials-panel {
  padding: clamp(1rem, 2vw, 1.4rem);
}

.socials-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.social-button {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.45rem;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  color: var(--ink);
  font: inherit;
  text-decoration: none;
  cursor: pointer;
  box-shadow: inset 0 0 0.6rem rgba(115, 244, 255, 0.18), 0 0.35rem 0.85rem rgba(65, 24, 127, 0.12);
  transition: transform 160ms ease, background 160ms ease, box-shadow 160ms ease;
}

.social-button:hover,
.social-button:focus-visible {
  transform: translateY(-1px);
  background: var(--ice);
}

.social-button:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.social-button::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 0.45rem);
  left: 50%;
  transform: translateX(-50%);
  background: var(--ink);
  color: #e8f9ff;
  padding: 0.25rem 0.55rem;
  border-radius: 4px;
  font-size: 0.8rem;
  /* max-content keeps a short handle on one line the way nowrap did, while
     the cap stops "@cassidyco.bsky.social" from growing wider than a phone
     screen and being clipped away at the shell's edge. */
  width: max-content;
  max-width: min(16rem, 70vw);
  pointer-events: none;
  opacity: 0;
  transition: opacity 160ms ease;
  z-index: 10;
}

.social-button:hover::after,
.social-button:focus-visible::after {
  opacity: 1;
}

.social-icon {
  flex: 0 0 auto;
  width: 1.6rem;
  height: 1.6rem;
  color: var(--violet);
}

.social-button[data-copy].is-copied {
  background: var(--pink);
}

.intro-panel {
  grid-column: 1 / -1;
  /* Hug the poem's longest line rather than spanning the whole window;
     fit-content still allows wrapping when the column gets narrow. */
  width: fit-content;
  justify-self: center;
  padding: clamp(1rem, 2vw, 1.4rem);
}

.intro-panel p {
  max-width: 54ch;
  margin-bottom: 0;
  line-height: 1.55;
  font-family: Atkinson-Regular, arial, sans-serif;
}

.link-card {
  min-height: 7.5rem;
  padding: 1rem;
  display: grid;
  align-content: space-between;
}

.link-card span {
  color: rgba(39, 20, 82, 0.72);
}

.link-card a {
  font-weight: 700;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.22em;
  /* Long bare URLs have no natural break points; wrap them rather than
     letting them clip out of the card when the column runs narrow. */
  overflow-wrap: anywhere;
}

.badge-card {
  align-content: start;
  gap: 0.5rem;
  padding: 0.6rem;
  min-height: 0;
  /* inline-size containment stops the badge block from setting this card's
     own width, so an oversized badge grid cannot inflate the card and push
     its siblings around. */
  container-type: inline-size;
}

.badge-grid {
  /* Badges are pixel art at their native 88x31 and look mushy at any other
     size, so the column count changes rather than the badges. auto-fit reads
     the space the card actually has: the card sits four nested layout
     decisions below the viewport — wave gutter, the layout-grid's middle
     track, the index window's padding, the content grid's two columns — and
     it is between 255px and 575px wide across the whole sweep, which is room
     for three to six badges nearly everywhere. A hard-coded two columns left
     that space empty and wrapped the badges into twice the rows they needed. */
  --badge-width: 88px;
  display: grid;
  grid-template-columns: repeat(auto-fit, var(--badge-width));
  gap: 2px;
  justify-content: center;
  justify-items: center;
  align-items: center;
}

.badge-grid a {
  display: block;
  line-height: 0;
}

.badge-grid img {
  display: block;
  /* The AGPL badge is wider than one column; let it scale in the single
     column layout instead of hanging off the card. */
  max-width: 100%;
  height: auto;
  image-rendering: pixelated;
}

.badge-grid .badge-wide {
  grid-column: 1 / -1;
}

.badge-note {
  margin: 0;
  font-size: 0.78rem;
  text-align: center;
  color: rgba(39, 20, 82, 0.72);
}

.right-stack {
  display: grid;
  align-content: center;
  gap: 1rem;
}

.webring,
.neolink-panel,
.death-clock,
.feature-object {
  position: relative;
  border: 2px solid rgba(232, 246, 255, 0.75);
  background: rgba(216, 247, 255, 0.24);
  padding: 1rem;
}

.neolink-panel {
  display: grid;
  justify-items: center;
}

.neolink-panel iframe {
  max-width: 100%;
}

.death-clock {
  display: grid;
  gap: 0.65rem;
  align-content: start;
  /* Contained for the same reason as .webring: the five readout cells must
     not set the stacked column's minimum width. */
  container-type: inline-size;
}

.death-clock-topline {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
}

.death-clock-title {
  margin: 0;
  font-size: 1.05rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.death-clock-status {
  font-size: 0.82rem;
}

/* The index window's dot announces "Online"; this one is a heartbeat. */
.death-clock .blink-dot {
  background: var(--magenta);
  box-shadow: 0 0 0.75rem var(--magenta);
  animation: heartbeatDot 1.1s ease-in-out infinite;
}

@keyframes heartbeatDot {

  0%,
  48%,
  100% {
    transform: scale(1);
    opacity: 0.85;
  }

  12% {
    transform: scale(1.45);
    opacity: 1;
  }

  30% {
    transform: scale(1.25);
    opacity: 1;
  }
}

.death-clock-readout {
  display: flex;
  gap: 0.3rem;
  margin: 0;
}

.death-cell {
  flex: 1 1 0;
  /* A flex item's default minimum is its content's width; zero it so the
     five cells split whatever the panel has evenly instead of refusing to
     shrink and shoving the readout out of the panel. */
  min-width: 0;
  display: grid;
  place-items: center;
  padding: 0.5rem 0.15rem;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  box-shadow: inset 0 0 0.6rem rgba(115, 244, 255, 0.18);
}

.death-num {
  font-family: Atkinson-Bold, arial, sans-serif;
  font-weight: 700;
  /* Tracks the panel on screens too narrow for the full-size digits; the
     cqi term only wins below ~19rem of panel, so desktop is untouched. */
  font-size: min(1.1rem, 6cqi);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

.death-clock-bar {
  height: 0.6rem;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  overflow: hidden;
}

.death-clock-fill {
  display: block;
  width: 0;
  height: 100%;
  background: linear-gradient(90deg, var(--cyan), var(--blue), var(--violet));
}

.death-clock-note {
  margin: 0;
  font-size: 0.82rem;
  font-family: Atkinson-Italic, arial, sans-serif;
  font-style: italic;
  color: rgba(39, 20, 82, 0.8);
}

/* The bonus round: past the estimate the heartbeat settles into a steady
   gold glow and the bar overflows into candy stripes. Mostly for people who
   mess with the clock (?dies=2020-01-01) — or for Cassidy in 2083. */
.death-clock.is-overtime {
  border-color: var(--banana);
  box-shadow: 0 0 1.1rem rgba(255, 0, 230, 0.35);
}

.death-clock.is-overtime .blink-dot {
  animation: none;
  background: var(--banana);
  box-shadow: 0 0 0.75rem var(--banana);
}

.death-clock.is-overtime .death-clock-fill {
  background: repeating-linear-gradient(135deg, var(--magenta) 0 8px, var(--banana) 8px 16px);
}

.portal-image {
  display: block;
  width: min(20rem, 100%);
  aspect-ratio: 900 / 620;
  height: auto;
  margin: 0 auto 1rem;
  object-fit: contain;
  animation: hoverPortal 6s ease-in-out infinite;
}

/* The ring, its orbit radius and its buttons are all sized against this
   panel rather than in bare rem: a fixed 12rem ring is wider than a small
   phone once the reader raises their default font, and as the widest thing
   in the stacked column it dragged every panel past the screen edge.
   inline-size containment keeps the panel from flooring the grid track it
   sits in (same move as the badge card); the cqi caps let the whole
   assembly shrink with the panel while leaving it pixel-identical wherever
   the rem sizes fit. */
.webring {
  container-type: inline-size;
}

.spinner {
  position: relative;
  width: min(12rem, 96cqi);
  aspect-ratio: 1;
  margin: 1rem auto;
  border: 1px dashed rgba(39, 20, 82, 0.4);
  border-radius: 50%;
}

.spinner a {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(5.2rem, 36cqi);
  min-height: 2.05rem;
  padding: 0.35rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel-strong);
  box-shadow: inset 0 0 0.75rem rgba(115, 244, 255, 0.3), 0 0.45rem 1rem rgba(65, 24, 127, 0.12);
  text-align: center;
  text-decoration: none;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 700ms cubic-bezier(0.2, 0.9, 0.25, 1.2);
  transform: translate(-50%, -50%) rotate(calc(var(--i) * (360deg / var(--count, 4)) + var(--spin, 0deg))) translateY(calc(-1 * min(4.35rem, 30cqi))) rotate(calc((var(--i) * (-360deg / var(--count, 4))) - var(--spin, 0deg)));
}

.spinner a img {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: 999px;
}

.spin-controls {
  display: flex;
  /* Three rigid pills; on a sliver of a screen SPIN drops to its own line
     instead of flooring the column width. */
  flex-wrap: wrap;
  justify-content: center;
  align-items: stretch;
  gap: 0.6rem;
}

.spin-button {
  position: relative;
  display: block;
  min-height: 2.7rem;
  padding: 0 1.4rem;
  border: 2px solid var(--violet);
  border-radius: 999px;
  background: var(--ice);
  color: var(--ink);
  font: inherit;
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  box-shadow: inset 0 0 0.85rem rgba(115, 244, 255, 0.35), 0 0.45rem 1.1rem rgba(65, 24, 127, 0.13);
}

.spin-step {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 2.7rem;
  padding: 0;
}

/* Border-drawn chevron so we don't depend on the display font having
   nice arrow glyphs. */
.spin-step-glyph {
  display: block;
  width: 0.5em;
  height: 0.5em;
  border-right: 0.16em solid currentColor;
  border-bottom: 0.16em solid currentColor;
}

.spin-step-glyph-prev {
  transform: translateX(15%) rotate(135deg);
}

.spin-step-glyph-next {
  transform: translateX(-15%) rotate(-45deg);
}

/* The chevrons are <a>, SPIN is a <button>; setting colour here rather than
   letting a:hover reach only the anchors keeps all three in step. */
.spin-button:hover,
.spin-button:focus-visible {
  background: var(--pink);
  color: var(--link-hover);
}

.spin-button:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

.feature-object {
  min-height: clamp(13rem, 23vw, 18rem);
  overflow: visible;
  border: 0;
  background: transparent;
  padding: 0.75rem;
  pointer-events: none;
  display: grid;
  place-items: end center;
}

.feature-image {
  display: block;
  position: relative;
  right: auto;
  bottom: auto;
  width: min(118%, 31rem);
  max-width: none;
  height: auto;
  margin: -3.2rem -1.2rem -1.4rem auto;
  opacity: 0.88;
  z-index: 0;
}

@keyframes hoverPortal {

  0%,
  100% {
    transform: translateY(0) rotate(2deg);
  }

  50% {
    transform: translateY(-0.55rem) rotate(-1deg);
  }
}

@keyframes blinkDot {

  0%,
  58% {
    opacity: 1;
  }

  59%,
  100% {
    opacity: 0.22;
  }
}

.site-footer {
  margin-top: clamp(1.5rem, 3vw, 2.5rem);
  padding: 0.75rem 0;
  text-align: center;
  font-size: 0.82rem;
  border-top: 1px solid rgba(231, 252, 255, 0.45);
  /* The only text on the site that sits straight on the page gradient rather
     than on a panel. At 0.7 it managed 3.5:1 over the gradient's dark stop;
     0.9 clears 4.5:1 across the whole sweep and still reads as muted. */
  color: rgba(39, 20, 82, 0.9);
  /* Same reason the accents are re-pointed: against mid-blue, the panel-tuned
     magenta drops to 2.6:1. These are the darkest members of the same family
     that clear 4.5:1 on every stop of the gradient. */
  --link-hover: #50006b;
  --link-active: #2e0a5e;
}

.site-footer a {
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

/*
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#-..............=%@@@@@@@@@@@
@@@@#+=======+#@@+==+#%*===##+==+%@*========#@#===*%@@@@@@@@@@@@@@@@@%+-+%@@@@@@@@@@@@#=-*@@@@@@@@@@
@@@@#- ..#*: .-@@:. -*#-  .**: .:%@-. :*%...*@*.  =%@@@@@@@@@@@@@@@@#--#%*:..........=#%+:+%@@@@@@@@
@@@@#- ..#=:.:=@@:. -*%+...==...=%@-. :*%%#*#@*.  =%@@@@@@@@@@@@@@@=:+%#=:............-*%#-:#%@@@@@@
@@@@#- ..-+*%@@@@:. -*%#+-.--:-+*%@-. .:=#%@@@*.  =%@@@@@@@@@@@@%#--#%*-...............:=#%+:+%@@@@@
@@@@#- ..#@@@@@@@:. -*#-  .**: ..%@-. :*@%*=*@*.  =%%*=+@@@@@@@%*:+%%=:..................-*@%--#@@@@
@@@@#- ..#@@@@@@@:. -*#-  .**: ..%@-. :=*...*@*.  -=:..=@@@@@@@*-+@#-:....................:+%#-=%@@@
@@@@%*===%@@@@@@@===+#%*==+##+==+%@+========#@#========*@@@@@@@#=-@#-.....................:+%*-+@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*.#%=:....................:%%==#@@@@
@@@@#+======+#@@@%*+=====+#%@%#+=====+*%%+====+#%*=======+%@@@@@#-=%*-...................:=@#:+%@@@@
@@@@*...=*-. :+@@-. .-*-  .*@*: .:*=. .-%##:. :+%-..:++: .:#@@@@%+.%%=:..................-*@+-*@@@@@
@@@@#=+%%-...-%@@-. .=#-  .*@+. .:#+: .-@@@-. :+%+=#%+:...*%@@@@@#.*%*-.................:-%@:+%@@@@@
@@@@@@%#:. .=%@@@-. .=#-  .*@+. .:#+: .-@@@-. :+@@@%+: .:*%@@@@@@%=-#%*=-:............:-+#%*:*%@@@@@
@@@@@%#.. .+##*%@-. .=#-  .*@+. .:#+: .-@@@-. :+@@%-. .-#%##%@@@@%#:-+#%%%+-::....::=*%%%*=:=#@@@@@@
@@@@%*.  .*=..:*@-. .=#-  .*@*. .:#+: .-@@@-. :+@%-. .=*-..-#@@@@@@%#*=--+#%%#+==*#%%*=--+#%%@@@@@@@
@@@@#==========#@%+=======*%@%*=======+#@@@+===*%+=========+%@@@@@@@@@@%#+--=*#%%*+--=*%@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%+:..-*%@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%@@@@@@@@@@@@@@@@@@@

!!post your asci here to troll cassidy!!
*/

.license-main {
  max-width: 56rem;
  margin: 0 auto;
}

.archive-intro {
  border: 1px solid rgba(255, 255, 255, 0.72);
  background: var(--panel);
  padding: clamp(1rem, 2vw, 1.4rem);
  margin-bottom: 1rem;
  line-height: 1.55;
  font-family: Atkinson-Regular, arial, sans-serif;
}

.archive-intro> :last-child {
  margin-bottom: 0;
}

.license-list {
  display: grid;
  /* minmax(0, 1fr) rather than the implicit `auto`: an auto track is floored
     at its content's min-content width, so a single unbreakable path in a
     .license-files <code> would push the whole column wider than the window
     it sits in. */
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
}

.license-entry {
  border: 1px solid rgba(255, 255, 255, 0.72);
  background: var(--panel);
  padding: clamp(1rem, 2vw, 1.4rem);
}

.license-entry h3 {
  margin-bottom: 0.4rem;
  font-size: 1.15rem;
}

.license-entry p {
  margin-bottom: 0.5rem;
  line-height: 1.55;
  font-family: Atkinson-Regular, arial, sans-serif;
}

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

/* Prose rendered from Markdown — license blurbs, blog posts, the press-kit
   notes — routinely carries a bare URL, which is a single token as wide as
   it is long. `anywhere` rather than `break-word` so it also stops setting
   the block's min-content width, which is what pushes a grid track wider
   than the container holding it. */
.license-entry p,
.license-callout p,
.press-kit-note p,
.archive-intro,
.entry-body,
.post-body {
  overflow-wrap: anywhere;
}

/* Fenced code blocks carry pre-formatted lines that must not be re-wrapped --
   the chat export in the "Prove It" post is a box-drawing table, and wrapping
   it would shred the alignment that makes it a table at all. So the long line
   has to go somewhere, and `white-space: pre` with the UA's default
   `overflow: visible` sends it straight out of the panel: at every width it
   spilled ~2500px past the column, then got swallowed by .page-shell's
   `overflow-x: clip`, which made the far side of the table unreachable rather
   than merely ugly. `overflow-x: auto` turns that spill into a scroll region
   the reader can actually pan. Hugo already emits `tabindex="0"` on <pre> for
   exactly this, so the region is keyboard-scrollable once it exists. */
.post-body pre,
.entry-body pre {
  max-width: 100%;
  overflow-x: auto;
  /* Panning that table on a touch screen must not chain into the page once it
     hits the end of its scroll: Chrome on Android reads the handed-off gesture
     as a back-navigation swipe, so a reader trying to reach the right-hand
     columns leaves the post instead. */
  overscroll-behavior-x: contain;
  margin: 1.4rem 0;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  padding: 0.8rem 0.9rem;
  font-size: 0.86rem;
  line-height: 1.45;
}

/* The block already draws the panel; a nested <code> must not draw a second
   one on top of it, and it inherits the smaller font from the <pre>.

   It also has to carry a width. A fence with a language on it goes through
   Chroma, which wraps every line in a `<span style="display:flex">`; those
   wrappers are block-level flex containers, so they size to the <pre>'s content
   box rather than to the lines they hold, and a line longer than the column
   overflows its wrapper instead of widening it. Nothing up the chain sees that
   width, because a default <code> is an inline box with none of its own to hand
   to the <pre> — on a 360px viewport a 611px jj-log line left `pre.scrollWidth`
   sitting at `clientWidth`, so the `overflow-x: auto` above had nothing to
   scroll and the right-hand half of the line was clipped away with no way to
   reach it. Sizing this box to its widest line rebuilds that chain, and
   `min-width: 100%` keeps a short block filling the panel rather than shrinking
   to fit its text. */
.post-body pre code,
.entry-body pre code {
  background: none;
  padding: 0;
  font-size: inherit;
  display: block;
  width: max-content;
  min-width: 100%;
}

.license-files {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  color: rgba(39, 20, 82, 0.65);
  font-size: 0.88rem;
}

.license-files code {
  background: rgba(255, 255, 255, 0.4);
  padding: 0.1em 0.35em;
  border-radius: 3px;
  /* Paths like fonts/AtkinsonHyperlegible-BoldItalic.ttf are one long token
     with no natural break point. */
  overflow-wrap: anywhere;
}

.gallery-section {
  margin-top: 1.4rem;
  border: 1px solid rgba(255, 255, 255, 0.72);
  background: var(--panel);
  padding: clamp(1rem, 2vw, 1.4rem);
}

.gallery-section h3 {
  margin-bottom: 0.4rem;
  font-size: 1.15rem;
}

.gallery-section>p {
  margin-bottom: 0.5rem;
  line-height: 1.55;
}

.gallery-license {
  font-size: 0.72rem;
  vertical-align: middle;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  padding: 0.15em 0.5em;
  border-radius: 3px;
  white-space: nowrap;
}

.gallery-grid {
  display: grid;
  /* The 9.5rem floor outgrows a small screen (or a large reader font);
     min() keeps the single remaining column inside the panel there. */
  grid-template-columns: repeat(auto-fill, minmax(min(9.5rem, 100%), 1fr));
  gap: 0.75rem;
  margin-top: 0.9rem;
}

.gallery-item {
  /* <figure> ships with a 1em 40px UA margin, which was quietly insetting
     every card 40px inside its grid cell — and on a sliver-width screen
     squeezed the card narrower than its own label. The cell's gap and the
     card's padding already provide the spacing. */
  margin: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 0.35rem;
  text-align: center;
  padding: 0.6rem;
  border: 1px solid rgba(255, 255, 255, 0.55);
  background: rgba(255, 255, 255, 0.28);
}

.gallery-item img {
  max-width: 100%;
  height: 7rem;
  object-fit: contain;
  /* Checkerboard so transparent art reads against the panel. */
  background:
    repeating-conic-gradient(rgba(255, 255, 255, 0.55) 0% 25%, rgba(215, 251, 255, 0.35) 0% 50%) 0 0 / 1rem 1rem;
}

.gallery-item figcaption {
  font-size: 0.78rem;
  color: rgba(39, 20, 82, 0.65);
  overflow-wrap: anywhere;
}

.gallery-item code {
  background: rgba(255, 255, 255, 0.4);
  padding: 0.1em 0.35em;
  border-radius: 3px;
}

/* A file name is one unbreakable token, and a centred flex item is sized to
   fit-content — which bottoms out at min-content, not at the cell. Without a
   break opportunity the name sets the card's floor and hangs out of the grid
   cell at every width where the name is longer than the column. */
.gallery-item>* {
  max-width: 100%;
}

.gallery-item a,
.gallery-item code {
  overflow-wrap: anywhere;
}

/* Cards with no preview (an offsite source file) still have to hold a grid
   cell the same size as the ones carrying a 7rem image. */
.gallery-item-source {
  min-height: 9.6rem;
}

.gallery-note {
  display: block;
  margin-top: 0.3rem;
  font-size: 0.72rem;
  line-height: 1.4;
  /* 0.55 gave 3.7:1 on --panel; 0.7 clears 4.5:1 at this size. */
  color: rgba(39, 20, 82, 0.7);
}

.gallery-alt,
.gallery-offsite {
  display: inline-block;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  padding: 0.05em 0.4em;
  border-radius: 3px;
  font-size: 0.7rem;
}

.gallery-alt {
  margin-left: 0.3rem;
}

/* Placeholders keep the grid's rhythm while reading as not-yet-shipped.
   This used to be `opacity: 0.62` on the whole figure, which multiplied into
   every colour inside it — the caption landed at 3.4:1 and the "coming soon"
   label at 2.3:1, and any future change to those colours would have been
   silently scaled too. The dashed empty box already carries the meaning, so
   the recession is a background tint that leaves text contrast alone. */
.gallery-item-coming {
  background: rgba(255, 255, 255, 0.1);
  border-style: dashed;
}

.gallery-coming {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 7rem;
  border: 1px dashed rgba(39, 20, 82, 0.5);
  font-size: 0.78rem;
  color: rgba(39, 20, 82, 0.7);
}

.press-kit-note,
.license-callout {
  margin-top: 1.4rem;
  border: 1px solid rgba(255, 255, 255, 0.72);
  background: var(--panel);
  padding: clamp(1rem, 2vw, 1.4rem);
  line-height: 1.55;
  font-family: Atkinson-Regular, arial, sans-serif;
}

.press-kit-note p,
.license-callout p {
  margin-bottom: 0.5rem;
}

.press-kit-note>p:last-child,
.license-callout>p:last-child {
  margin-bottom: 0;
}

.guestbook-intro {
  max-width: 60ch;
  margin: 0 auto 1.2rem;
  padding: clamp(0.85rem, 1.7vw, 1.2rem);
  border: 1px solid rgba(255, 255, 255, 0.72);
  background: var(--panel);
  line-height: 1.55;
}

.guestbook-frame-wrap {
  position: relative;
  margin: 0 auto;
  max-width: 44rem;
  padding: clamp(0.75rem, 1.7vw, 1.2rem);
  border: 3px double var(--line);
  background: var(--panel-strong);
  box-shadow:
    inset 0 0 1rem rgba(115, 244, 255, 0.25),
    0 0.9rem 2.2rem var(--shadow);
}

.guestbook-frame {
  display: block;
  width: 100%;
  height: min(80vh, 50rem);
  min-height: 32rem;
  border: 1px solid rgba(232, 246, 255, 0.55);
  background: var(--ice);
  color-scheme: light;
}

.blog-main {
  max-width: 72rem;
  margin: 0 auto;
}

.blog-columns {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(13rem, 17rem);
  gap: 1rem;
  align-items: start;
}

.blog-feed {
  display: grid;
  gap: 1rem;
  min-width: 0;
}

.blog-search input[type="search"] {
  width: 100%;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  color: var(--ink);
  font: inherit;
}

.blog-search input[type="search"]:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.search-status {
  margin: 0;
  font-size: 0.88rem;
  color: rgba(39, 20, 82, 0.72);
}

.blog-entry {
  border: 1px solid rgba(255, 255, 255, 0.72);
  background: var(--panel);
  padding: clamp(1rem, 2vw, 1.4rem);
}

.entry-title {
  margin-bottom: 0.4rem;
  font-size: 1.3rem;
}

.entry-body {
  line-height: 1.55;
  font-family: Atkinson-Regular, arial, sans-serif;
}

.entry-body> :last-child {
  margin-bottom: 0;
}

.entry-more {
  margin: 0.5rem 0 0;
  font-size: 0.92rem;
}

.entry-meta,
.post-meta {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-size: 0.82rem;
  color: rgba(39, 20, 82, 0.72);
}

.entry-meta {
  margin: 0.85rem 0 0;
  padding-top: 0.55rem;
  border-top: 1px dashed rgba(39, 20, 82, 0.25);
}

/* A standalone control rather than a link inside a sentence, so it doesn't get
   the inline exemption from a minimum target size — and at 0.82rem the "#"
   glyph alone made a 20x17 chip. 1.6rem is 25.6px at the default root size. */
.entry-permalink {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.6rem;
  min-height: 1.6rem;
  padding: 0 0.35em;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  text-decoration: none;
}

.blog-highlights {
  position: sticky;
  top: clamp(0.75rem, 2vw, 1.5rem);
  border: 2px solid rgba(232, 246, 255, 0.75);
  background: var(--panel);
  padding: 1rem;
}

.blog-highlights h3 {
  margin-bottom: 0.6rem;
  font-size: 1.05rem;
}

.blog-highlights ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.6rem;
}

.highlight-date {
  display: block;
  font-size: 0.78rem;
  color: rgba(39, 20, 82, 0.65);
}

.highlight-empty {
  margin: 0;
  font-size: 0.88rem;
  color: rgba(39, 20, 82, 0.65);
}

.post-meta {
  margin: 0 0 1rem;
  /* Shares the .entry-meta rule above, but not its background: .entry-meta
     sits inside a .blog-entry panel, while this one sits directly on the
     index-window's stripes. Solid ink alone technically clears AA there
     (4.9:1), but the ratio is measured against an average the eye never
     sees — the stripes put 182 different colours behind these glyphs, and
     this is the smallest text on the page set in the display face. So it
     gets a flat panel to sit on, the same chip the topline links use. */
  width: fit-content;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--line);
  background: var(--panel-strong);
  color: var(--ink);
}

.post-body {
  border: 1px solid rgba(255, 255, 255, 0.72);
  background: var(--panel);
  padding: clamp(1rem, 2vw, 1.4rem);
  line-height: 1.55;
  font-family: Atkinson-Regular, arial, sans-serif;
}

.post-body> :last-child {
  margin-bottom: 0;
}

/* Markdown prose — post bodies, feed entries, section intros. The site-wide
   `h2, h3, p { margin-top: 0 }` reset is right for the panel headings it was
   written for, but in prose it leaves a section heading exactly as far from
   the paragraph above it as that paragraph sits from its own predecessor, so
   a section break reads as just another paragraph. The gaps are in `em` so
   each level scales with its own type rather than the root. */
.post-body :is(h2, h3, h4, h5),
.entry-body :is(h2, h3, h4, h5),
.archive-intro :is(h2, h3, h4, h5) {
  margin-top: 1.6em;
  margin-bottom: 0.45em;
  line-height: 1.2;
}

/* Nothing to separate from above the panel's first line. */
.post-body> :first-child,
.entry-body> :first-child,
.archive-intro> :first-child {
  margin-top: 0;
}

/* The UA's 40px list indent is a tenth of a phone screen, spent on white space
   left of the text. 1.6em still clears a two-digit ordinal marker, and it is
   near enough to the 1.625em Tailwind Typography uses that the bun.com
   pull-quote below — whose whole point is reading as that site's skin — is not
   visibly moved by inheriting it. */
.post-body :is(ul, ol),
.entry-body :is(ul, ol) {
  padding-inline-start: 1.6em;
}

.post-body li,
.entry-body li {
  margin-bottom: 0.35em;
}

/* A <details> in prose had no block spacing of its own, which "Prove It" was
   working around with a trailing <br> after each one (both now removed). Give
   it a paragraph's rhythm here instead, and a summary with enough height to be
   a thumb target rather than a line of text. */
.post-body details,
.entry-body details {
  margin: 1.4rem 0;
}

.post-body summary,
.entry-body summary {
  padding: 0.3rem 0;
  font-family: Atkinson-Bold, arial, sans-serif;
  font-weight: 700;
  cursor: pointer;
}

/* The aside partway through "Social Agents" is a parenthetical inside a
   sentence, not a section between them, and the block rhythm above would cut
   its paragraph in half around a two-line note.

   It can't be a <details> at all, which is why none of this looks like one.
   The HTML parser closes an open <p> the moment it meets a <details> start
   tag, so a <details> written mid-paragraph in Markdown is torn out of that
   paragraph and lands between two of them before any stylesheet gets a say —
   the marker ends up alone on its own line with the rest of the sentence
   orphaned under it. A checkbox and its label are phrasing content, so the
   paragraph survives the parse, and the toggle still needs no script.

   Above the breakpoint the open note lifts out of flow into a bubble over the
   marker, so reading it doesn't shove the rest of the post down the page.
   Below it that shape stops working: the bubble is centred on a marker that
   can sit anywhere along a line, and once the box is a good fraction of the
   screen it hangs off whichever edge the marker is nearest, with nothing in
   CSS that can pull it back. A phone gets the note unfolding into the sentence
   instead — same markup, same toggle, and the paragraph is the only thing that
   moves.

   The note's parentheses are in the Markdown rather than generated here,
   because this post's summary is also what goes out in the Atom feed, where
   none of this file is loaded. A reader that drops the checkbox and shows the
   note inline still gets a sentence with a parenthetical in it. */
.post-body .prose-aside,
.entry-body .prose-aside {
  position: relative;
}

.post-body .prose-aside-marker,
.entry-body .prose-aside-marker {
  font-size: 0.75em;
  vertical-align: super;
  color: var(--link-active);
  text-decoration: underline dotted;
  cursor: pointer;
}

.post-body .prose-aside-marker::before,
.entry-body .prose-aside-marker::before {
  content: "[";
}

.post-body .prose-aside-marker::after,
.entry-body .prose-aside-marker::after {
  content: "]";
}

.post-body .prose-aside-marker:hover,
.entry-body .prose-aside-marker:hover {
  color: var(--link-hover);
}

/* The label is the whole of the control's visible self, so the box itself is
   hidden the way that keeps it focusable and operable by Space, and the focus
   ring it would have drawn goes on the label instead. */
.post-body .prose-aside-marker input,
.entry-body .prose-aside-marker input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  clip-path: inset(50%);
}

.post-body .prose-aside-marker:has(input:focus-visible),
.entry-body .prose-aside-marker:has(input:focus-visible) {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.post-body .prose-aside-note,
.entry-body .prose-aside-note {
  display: none;
  font-size: 0.9em;
  color: var(--link-active);
}

.post-body .prose-aside:has(input:checked)>.prose-aside-note,
.entry-body .prose-aside:has(input:checked)>.prose-aside-note {
  display: inline;
  /* The gap can't be a space in the Markdown: out of flow above the
     breakpoint, that space would stay behind in the sentence and stand
     between "excellence" and the question mark that follows it. */
  margin-inline-start: 0.25em;
}

@media (min-width: 48rem) {

  .post-body .prose-aside:has(input:checked)>.prose-aside-note,
  .entry-body .prose-aside:has(input:checked)>.prose-aside-note {
    display: block;
    position: absolute;
    margin: 0;
    bottom: calc(100% + 0.6rem);
    left: 50%;
    /* Centred on the marker, then pushed back inside the column if that hangs
       it off an edge. The marker can land anywhere along a line, so how far it
       has to move is only knowable once the paragraph has been laid out —
       assets/script.js measures it and sets --aside-shift. Without that script
       the bubble stays centred, which the cap below keeps on screen. */
    transform: translateX(calc(-50% + var(--aside-shift, 0px)));
    z-index: 5;
    /* max-content sizes the bubble to the note, the cap keeps it inside the
       column — the same pairing the social-button tooltips use. */
    width: max-content;
    max-width: min(24rem, 60vw);
    padding: 0.6rem 0.75rem;
    border: 1px solid var(--lavender);
    background: var(--panel-strong);
    box-shadow: 0 0.4rem 0.9rem var(--shadow);
    color: var(--ink);
  }
}

/* Markdown figures come out of the exporter at their native pixel size (the
   forest plots are 1760px wide), which is far past the column — clamp them
   here rather than sizing each one at the call site. */
.post-body img,
.entry-body img {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  margin-inline: auto;
}

/* The posts under content/blog/cohost/ render in cohost's skin on their own
   pages (assets/cohost.css), but the blog feed above still draws their
   summaries in this one. Their inline eggbug emoji are 128px sprites meant to
   sit in a line of text at 1.25em, and the rule above would blow each one up
   into a centred block. */
.entry-body .co-emoji {
  display: inline-block;
  width: auto;
  height: 1.25em;
  margin: 0;
  vertical-align: middle;
}

/* The "Jarred says" pull-quote is rendered in bun.com's own skin rather than
   this site's: Tailwind Typography `prose` at base size, on their page
   background, in their system-ui stack. Every value below is lifted from
   bun.com/_assets/site-*.css so the block reads as a slab of the source page
   dropped onto this one — that's the joke, so resist tidying it into the
   surrounding Atkinson prose. It tracks the reader's color-scheme preference
   because bun.com's own theme switch ships set to `system`. */
.bun-quote {
  --bun-bg: #f9fafb;
  /* gray-50, their <body> */
  --bun-body: #374151;
  /* --tw-prose-body */
  --bun-headings: #111827;
  /* --tw-prose-headings */
  --bun-line: #e5e7eb;
  /* gray-200 */
  margin: 1.8rem 0;
  padding: clamp(1.1rem, 3vw, 2rem) clamp(1.1rem, 3.5vw, 2.5rem);
  border: 1px solid var(--bun-line);
  border-radius: 0.75rem;
  background: var(--bun-bg);
  color: var(--bun-body);
  font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
    "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  font-size: 1rem;
  line-height: 1.75;
}

@media (prefers-color-scheme: dark) {
  .bun-quote {
    --bun-bg: #111827;
    /* gray-900 */
    --bun-body: #d1d5db;
    /* --tw-prose-invert-body */
    --bun-headings: #fff;
    --bun-line: #374151;
  }
}

/* Their `prose` container is max-w-65ch mx-auto and the children just fill
   it. The measure has to live on the wrapper, not on each child: `ch` is
   font-size-relative, so a per-child max-width gives the 1.25em h3 a wider
   box than the paragraphs and every heading hangs left of the text. */
.bun-prose {
  max-width: 65ch;
  margin-inline: auto;
}

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

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

/* margin-block, not the `margin` shorthand — the shorthand would reset the
   auto inline margins above and knock the column out of center. */
.bun-quote p {
  margin-block: 1.25em;
}

.bun-quote h3,
.bun-quote h4 {
  color: var(--bun-headings);
  font-family: inherit;
  font-weight: 600;
}

.bun-quote h3 {
  font-size: 1.25em;
  line-height: 1.6;
  margin-block: 1.6em 0.6em;
}

.bun-quote h4 {
  font-size: 1em;
  line-height: 1.5;
  margin-block: 1.5em 0.5em;
}

/* Their `h3+*`/`h4+*` rules. Without these the paragraph's 1.25em top margin
   collapses against the heading's smaller bottom margin and wins, so every
   heading sits ~0.5em further from its text than it does on bun.com. */
.bun-quote h3+*,
.bun-quote h4+* {
  margin-top: 0;
}

/* Their heading anchors are undecorated and take the heading color, hover
   included. Ours points back at the source section instead of a local id. */
.bun-quote h3 a,
.bun-quote h3 a:hover,
.bun-quote h3 a:focus,
.bun-quote h3 a:active {
  color: inherit;
  text-decoration: none;
}

/* ```goat fences become inline SVG during Hugo's own markdown pass, via the
   embedded render-codeblock-goat.html hook — no client-side renderer. GoAT
   strokes everything with currentColor, so the diagram picks up --ink from
   body without any help. It emits a viewBox and no width/height, though, so
   the sizing below is what actually makes it lay out. */
.goat {
  margin: 1.4rem 0;
}

.goat svg {
  display: block;
  width: 100%;
  max-width: 38rem;
  height: auto;
  margin: 0 auto;
}

/* The Amoeba entry keeps the dress of the landing page it replaced: libtcod's
   palette and a terminal face, so the card in the Works list looks like the
   thing it links to. */
.work-amoeba {
  --void: #050508;
  --wall: #484d55;
  --slime: #6daa2c;
  --bright-slime: #84be38;
  --city: #8595a1;
  --paper: #deeed6;
  /* Same re-pointing the link-rail does, for the same reason: the page-tuned
     magenta drops to 2.1:1 on the void. The slime greens clear 7:1. */
  --link-hover: var(--bright-slime);
  --link-active: var(--slime);
  /* The ring is drawn 2px outside the link, which is still the card's own
     void rather than the page behind it, so the near-black default vanishes
     here. Paper reads 16.8:1. */
  --focus-ring: var(--paper);
  border-color: var(--wall);
  background: var(--void);
  color: var(--paper);
  font-family: "DejaVu Sans Mono", "Liberation Mono", Consolas, monospace;
}

.work-amoeba h3 {
  color: var(--bright-slime);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* .license-entry p hands prose to Atkinson; this card is terminal all the way
   down. City grey is the muted tier, at 6.6:1. */
.work-amoeba p {
  font-family: inherit;
  color: var(--city);
}

/* Prose stays muted and the links in it don't -- the destinations are the only
   thing this entry is really for. The title keeps the slime it inherits from
   its h3. */
.work-amoeba p a {
  color: var(--paper);
}

/* Space Trucking gets the same treatment against its own palette (the roles
   are src/palette.rs in the game's repo, named the same way here). What it
   borrows is the console rather than a screen: a riveted plate face lit from
   the top left, a stencilled amber lamp label, phosphor green kept for the
   states a lamp shows. The game has no text in it anywhere but the version
   string, so unlike Amoeba there is no face of its own to set this in. */
.work-space-trucking {
  --plate: #242b27;
  --plate-lit: #3a443d;
  --plate-shade: #121614;
  --rivet: #46504a;
  --amber: #e8a33d;
  --phosphor: #7fd962;
  --phosphor-hot: #bdf29b;
  --icon-lit: #a7b3a9;
  --glint: #e7e3d1;
  /* Same re-pointing the Amoeba card does, for the same reason: the
     page-tuned magenta is unreadable on plate. Both phosphors clear 8:1. */
  --link-hover: var(--phosphor-hot);
  --link-active: var(--phosphor);
  /* The ring is drawn 2px outside the link, which is still the card's own
     plate rather than the page behind it, so the near-black default vanishes
     here. Glint reads 11.3:1. */
  --focus-ring: var(--glint);
  border-color: var(--rivet);
  background: var(--plate);
  color: var(--glint);
  /* The bevel every raised thing in the game carries: the lit edge toward the
     light, the shaded one away from it. Inset shadows rather than a two-tone
     border, so the 1px border above stays the card's own edge. */
  box-shadow:
    inset 1px 1px 0 var(--plate-lit),
    inset -1px -1px 0 var(--plate-shade);
}

/* Painted on the plate, not printed: the console labels its hardware in
   stencilled capitals and lights them amber, at 6.7:1 here. */
.work-space-trucking h3 {
  color: var(--amber);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Prose keeps the Atkinson .license-entry p gives it. Etched-icon grey is the
   muted tier, at 6.7:1. */
.work-space-trucking p {
  color: var(--icon-lit);
}

/* Prose stays muted and the links in it don't, as on the Amoeba card. The
   title keeps the amber it inherits from its h3. */
.work-space-trucking p a {
  color: var(--glint);
}

/* The three-column layout needs the wave gutter (16vw) plus all three
   columns near their preferred widths; below ~86rem the middle column gets
   squeezed toward its minimum and its contents clip, so stack well before
   that happens. The wave gutter itself stays until 62rem (further down). */
@media (max-width: 86rem) {
  .layout-grid {
    grid-template-columns: 1fr;
  }

  .link-rail {
    position: static;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-content: stretch;
    /* The rail spans the page from here down, so the charms need their room
       inside the grid; the shell's gutter is narrower than the overhang once
       the wave gutter collapses. */
    padding-inline: var(--charm-overhang);
  }

  .rail-link {
    padding-left: 3rem;
  }

  .rail-link:nth-child(even) {
    padding-right: 3rem;
  }

  .index-window {
    min-height: auto;
  }

  .right-stack {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }

  /* Webring and neolink pair up side by side; the death clock and the bunny
     each get a full row. */
  .right-stack .death-clock,
  .right-stack .feature-object {
    grid-column: 1 / -1;
  }
}

@media (max-width: 62rem) {
  .page-shell {
    --wave-gutter: clamp(1rem, 2.6vw, 2.4rem);
    padding-left: clamp(1rem, 2.6vw, 2.4rem);
  }

  .blog-columns {
    grid-template-columns: 1fr;
  }

  .blog-highlights {
    position: static;
  }
}

@media (max-width: 40rem) {

  /* A blog post sits inside three nested padded boxes — the shell, the window's
     double frame, the prose panel — and each one charged full price for its
     gutter. At 412px (a Pixel's viewport) that spent 105px of the 412 on frame
     and left the text measuring 307px: about 30 characters a line, which is
     what makes the reading feel boxed in. The two outer layers are frame
     rather than reading surface, so they give the width back; only the panel
     the text actually sits on keeps a full gutter. Vertical padding stays
     generous — height is free in a column you scroll. */
  .page-shell {
    padding: 0.9rem 0.55rem;
  }

  .index-window {
    padding: 0.8rem 0.5rem;
  }

  .post-body,
  .blog-entry,
  .archive-intro {
    padding: 1.05rem 0.9rem;
  }

  /* Nested one level deeper again, inside .post-body. */
  .bun-quote {
    padding: 1.1rem 0.85rem;
  }

  /* Long-form prose at a ~33-character measure. A little more leading than the
     desktop 1.55 stops the lines stacking into a slab, and hyphenation takes
     the worst of the rag out of a column this narrow (`lang` is on <html>, so
     the browser has a dictionary to work from). */
  .post-body,
  .entry-body,
  .archive-intro {
    line-height: 1.65;
    hyphens: auto;
  }

  /* Never hyphenate an identifier, a path, or a URL — the inserted hyphen is a
     character the reader cannot tell from one that's really there. */
  .post-body :is(code, kbd, samp, pre),
  .entry-body :is(code, kbd, samp, pre) {
    hyphens: manual;
  }

  /* Paragraph spacing has to grow with the leading or the gap between
     paragraphs stops reading as wider than the gap between lines. The `:not`
     leaves the bun.com pull-quote on its own measurements, which are lifted
     from that site and shouldn't drift with ours. */
  .post-body p:not(.bun-quote p),
  .entry-body p {
    margin-bottom: 1.25em;
  }

  .post-body pre,
  .entry-body pre {
    padding: 0.7rem;
  }

  /* The entry title and its "Read more" both sit tighter to the summary than
     two paragraphs of that summary sit to each other. At desktop leading that
     reads as pairing; once the leading opens up it reads as a collision. */
  .entry-title {
    margin-bottom: 0.6rem;
  }

  .entry-more {
    margin-top: 0.85rem;
  }

  .wave-field {
    opacity: 0.4;
  }

  /* Panels run nearly edge-to-edge here, so the rulers sit behind content
     more often — fade them like the wave-field rather than dropping them. */
  .tick-rulers {
    opacity: 0.55;
  }

  .link-rail,
  .content-grid,
  .right-stack {
    grid-template-columns: 1fr;
  }

  .rail-link {
    min-height: 3.7rem;
  }

  .feature-image {
    position: relative;
    right: auto;
    bottom: auto;
    width: min(100%, 25rem);
    margin-left: auto;
  }
}

@media (prefers-reduced-motion: reduce) {

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }
}