/* === Reset === */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
  margin: 0;
  padding: 0;
  border: 0;
  font: inherit;
  font-size: 100%;
  vertical-align: baseline;
}

html {
  line-height: 1;
}

ol, ul {
  list-style: none;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
}

caption, th, td {
  text-align: left;
  font-weight: normal;
  vertical-align: middle;
}

q, blockquote {
  quotes: none;
}
q:before, q:after, blockquote:before, blockquote:after {
  content: "";
  content: none;
}

a img {
  border: none;
}

article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary {
  display: block;
}

[hidden] {
  display: none !important;
}

/* === Brand Tokens === */
:root {
  color-scheme: dark;

  /* Core Greens */
  --g: #4CAF50;
  --gl: #81C784;
  --gp: #C8E6C9;

  /* Dark Foundation */
  --bk: #0D0D0D;
  --dk: #1A1A1A;
  --ch: #2D2D2D;

  /* Warm Accents */
  --em: #E8531E;
  --am: #F5A623;
  --ru: #C0392B;

  /* Neutrals */
  --n1: #F5F5F5;
  --n2: #E0E0E0;
  --n3: #BDBDBD;
  --n4: #9E9E9E;
  --n5: #757575;
  --n6: #424242;

  /* Font Awesome */
  --fa-font-solid: normal 900 1em/1 "Font Awesome 6 Solid";
  --fa-font-brands: normal 400 1em/1 "Font Awesome 6 Brands";
}

/* === Base === */
html, body {
  background: var(--bk);
  color: var(--n1);
}

html {
  height: 100%;
}

body {
  /* Sticky footer. A page with less content than the viewport — the blog
     index with one post, the 404 — used to leave the footer floating
     mid-screen with dead space beneath it.

     dvh after vh, so browsers that understand it use the dynamic viewport and
     the rest fall back. On mobile, 100vh counts the area behind the retracting
     browser chrome, which overflows by exactly that much. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;

  font-family: 'Outfit', sans-serif;
  font-weight: 400;
  line-height: 1.5;
  position: relative;
  overflow-x: hidden;
}

/* The only flex children are <main> and the footer: .skip-link is absolutely
   positioned, .topbar is fixed, and body::before is fixed, so none of them
   join the flex layout. main takes the slack and pushes the footer down. */
main {
  flex: 1 0 auto;
}

body::before {
  content: "";
  position: fixed;
  top: -50%;
  left: -50%;
  width: 200%;
  height: 200%;
  background:
    radial-gradient(ellipse at 60% 50%, rgba(76, 175, 80, 0.06) 0%, transparent 60%),
    radial-gradient(ellipse at 30% 70%, rgba(232, 83, 30, 0.04) 0%, transparent 50%);
  pointer-events: none;
  z-index: 0;
  animation: drift 20s ease-in-out infinite alternate;
}

/* === Accessibility === */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--g);
  outline-offset: 4px;
  border-radius: 2px;
}

.skip-link {
  position: absolute;
  top: -100%;
  left: 0;
  padding: 0.5em 1em;
  background: var(--n1);
  color: var(--bk);
  font-family: 'Outfit', sans-serif;
  font-size: 1rem;
  z-index: 1000;
  text-decoration: none;
}

.skip-link:focus {
  top: 0;
}

/* === Links === */
a:link,
a:visited {
  color: var(--g);
  transition: color 0.2s;
}

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

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

/* === Layout === */
.site {
  position: relative;
  z-index: 1;
  width: 90%;
  max-width: 720px;
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 3rem 0;
  box-sizing: border-box;
}

/* === Divider === */
.divider {
  height: 1px;
  border: none;
  background: linear-gradient(90deg, transparent, rgba(76, 175, 80, 0.2), transparent);
  margin: 2rem auto;
  max-width: 400px;
}

/* === Hero === */
.hero {
  margin-bottom: 1.5rem;
}

.hero-logo-wrap {
  position: relative;
  display: inline-block;
}

.hero-logo {
  border-radius: 24px;
  border: 2px solid rgba(76, 175, 80, 0.2);
  width: 250px;
  height: 250px;
  transition: border-color 0.3s;
  /* A transparent shadow rather than none: box-shadow can't interpolate
     between a shadow and `none`, so without this the glow would cut out at
     the end of the settle instead of fading. */
  box-shadow: 0 0 0 rgba(145, 70, 255, 0);
}

/* Live state — the logo breathes. Four seconds in and out is roughly a
   resting breath, which is what keeps it from reading as an alert: slow
   enough to notice out of the corner of your eye, never fast enough to
   nag. Scale, glow and border move together so it's one gesture rather
   than three effects sharing a timer. */
.hero-logo-wrap.is-live .hero-logo {
  border-color: #9146FF;
  animation: live-breathe 4s ease-in-out infinite;
}

/* Coming on air. The mirror of the problem below, and it has the same cause:
   you cannot transition INTO an animation either. Adding .is-live applies
   live-breathe immediately, so the border simply appears at the breath's
   opening value — a resting green one frame, dim purple the next, with the
   swell only arriving two seconds later.
   So the arrival is its own animation: one swell from rest, up through the
   breath's peak, landing exactly on the breath's opening frame. That last
   keyframe MUST stay identical to live-breathe's 0% or the handover shows as
   a jump. The duration is mirrored by ARRIVE_MS in js/twitch-status.js. */
.hero-logo-wrap.is-live.is-arriving .hero-logo {
  animation: live-arrive 1.1s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}

@keyframes live-arrive {
  0% {
    transform: scale(1);
    border-color: rgba(76, 175, 80, 0.2);
    box-shadow: 0 0 0 rgba(145, 70, 255, 0);
  }
  55% {
    transform: scale(1.045);
    border-color: #B084FF;
    box-shadow:
      0 0 28px rgba(145, 70, 255, 0.75),
      0 0 60px rgba(145, 70, 255, 0.35);
  }
  100% {
    transform: scale(1);
    border-color: rgba(145, 70, 255, 0.35);
    box-shadow: 0 0 0 rgba(145, 70, 255, 0);
  }
}

/* Coming off air. A transition cannot ease out of an animation on its own:
   the spec builds the style a transition compares against WITHOUT declarative
   animations in it, so at the moment .is-live goes away there is no purple
   border or glow on record to start from and the live look is simply gone
   between one frame and the next. js/twitch-status.js copies the animation's
   current values onto the element as inline styles first, which gives this
   something real to move away from, then drops them. */
.hero-logo-wrap.is-settling .hero-logo {
  transition:
    border-color 0.7s ease-out,
    box-shadow 0.7s ease-out,
    transform 0.7s ease-out;
}

/* Logo is only a link while live — the anchor has no href otherwise, so
   these only ever apply on air. */
.hero-logo-link {
  display: inline-block;
  line-height: 0;
}

.hero-logo-link[href] {
  cursor: pointer;
}

.hero-logo-link[href]:hover .hero-logo {
  border-color: #A970FF;
  transform: scale(1.02);
}

/* Hover has to stop the breathing to be felt at all: an animation outranks a
   plain declaration, so while live the rule above would be ignored for both
   transform and border-color. Switching the animation off hands them back.
   The jump is imperceptible — the breath only travels 2.5%. */
.hero-logo-wrap.is-live .hero-logo-link[href]:hover .hero-logo {
  animation: none;
  transform: scale(1.03);
  border-color: #A970FF;
  box-shadow: 0 0 30px rgba(145, 70, 255, 0.5);
}

.hero-logo-link[href] .hero-logo {
  transition: border-color 0.3s, transform 0.3s, box-shadow 0.3s;
}

/* "I've just gone live" — a slot sitting immediately above the logo with the
   line clipped inside it. The text starts translated down, so it's hidden
   behind the logo's top edge, and rises into view: the logo appears to push
   it out. Clipping is what sells it — the logo PNG has transparency, so
   simply stacking the text behind it would show through. */
.live-shout-slot {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 2px);
  transform: translateX(-50%);
  width: max-content;
  max-width: 92vw;
  overflow: hidden;
  padding: 0.35rem 0.25rem;
  pointer-events: none;
}

.live-shout {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: clamp(0.8rem, 2.6vw, 1.05rem);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: #C8A6FF;
  text-shadow: 0 0 18px rgba(145, 70, 255, 0.5);
  white-space: nowrap;
  /* Parked below the slot until the rise animation runs, so nothing flashes
     into place before JS gets to it. */
  transform: translateY(120%);
}

/* Two animations on one element, with the properties split cleanly between
   them: the rise owns transform, the breath owns opacity and text-shadow. A
   property in both would go to whichever is declared later in the shorthand,
   silently, so the split is the thing that keeps this predictable. Both start
   the moment we go live — the same moment the logo starts breathing — which
   is what keeps the two in step. */
.live-shout.live-shout-rise {
  animation:
    shout-rise 0.7s cubic-bezier(0.2, 0.8, 0.2, 1) forwards,
    live-shout-breathe 4s ease-in-out infinite;
}

/* Declared after the rise so its animation replaces that shorthand outright.
   It has to be an animation rather than a transition: JS adds is-leaving
   while live-shout-rise is still on the element, and a forwards-filled
   animation outranks plain declarations — so the transform and opacity this
   used to carry were silently ignored and the old line never dropped away. */
.live-shout.is-leaving {
  animation: shout-fall 0.22s ease-in forwards;
}

/* Transform only — the breath owns opacity now. The slot clips anything below
   its own top edge, so the line is hidden until it emerges regardless; the
   fade this used to carry was never what sold the effect. */
@keyframes shout-rise {
  from { transform: translateY(120%); }
  to   { transform: translateY(0); }
}

@keyframes shout-fall {
  from { transform: translateY(0);    opacity: 1; }
  to   { transform: translateY(120%); opacity: 0; }
}

/* Glow plus a shallow dip in opacity — glow alone was invisible against a
   dark page. 0.82 is as far down as this can go: #C8A6FF over #0D0D0D at that
   alpha still clears 5:1, so the line stays readable at the bottom of every
   breath, which is the whole constraint. */
@keyframes live-shout-breathe {
  0%, 100% {
    opacity: 1;
    text-shadow: 0 0 10px rgba(145, 70, 255, 0.3);
  }
  50% {
    opacity: 0.82;
    text-shadow: 0 0 30px rgba(145, 70, 255, 0.95);
  }
}

/* The reduced-motion counterpart: glow only, nothing moves, nothing fades. */
@keyframes live-shout-glow-still {
  0%, 100% { text-shadow: 0 0 10px rgba(145, 70, 255, 0.3); }
  50%      { text-shadow: 0 0 30px rgba(145, 70, 255, 0.95); }
}

/* Tint the Twitch icon while live so the topbar agrees with the hero. */
.topbar-links a.twitch-status.is-live,
.topbar-links a.twitch-status.is-live:link,
.topbar-links a.twitch-status.is-live:visited {
  color: #9146FF;
}

@media (prefers-reduced-motion: reduce) {
  /* No travel: the line is simply there, in place, from the start. It keeps
     the glow pulse though — same reasoning as the logo's, and the two still
     share a cadence. Opacity is pinned at 1 here, so nothing fades either. */
  .live-shout,
  .live-shout.live-shout-rise {
    animation: live-shout-glow-still 6s ease-in-out infinite;
    transform: none;
    opacity: 1;
  }

  .live-shout.is-leaving {
    animation: none;
    transform: none;
    opacity: 1;
  }

  .hero-logo-link[href]:hover .hero-logo {
    transform: none;
  }
}

.live-badge {
  display: none;
  position: absolute;
  top: 12px;
  left: 12px;
  background: rgba(13, 13, 13, 0.85);
  color: var(--n1);
  font-family: 'JetBrains Mono', monospace;
  font-weight: 600;
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  align-items: center;
  gap: 0.35rem;
  pointer-events: none;
}

/* Fades in with the arrival rather than appearing fully formed — the same
   entrance the now-playing card already uses, so the two arrive together. */
.hero-logo-wrap.is-live .live-badge {
  display: inline-flex;
  animation: fadeUp 0.4s ease-out both;
}

/* Now playing — box art + category, top-right of the hero logo. Only
   rendered while live; JS toggles [hidden] and the reset above makes that
   display: none !important. */
.now-playing {
  position: absolute;
  top: -10px;
  right: -10px;
  width: 76px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  padding: 0.4rem;
  box-sizing: border-box;
  background: rgba(13, 13, 13, 0.9);
  border: 1px solid rgba(145, 70, 255, 0.45);
  border-radius: 8px;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  text-align: center;
  animation: fadeUp 0.4s ease-out both;
}

.now-playing-art {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: 4px;
}

.now-playing-game {
  font-family: 'JetBrains Mono', monospace;
  font-weight: 500;
  font-size: 0.6rem;
  line-height: 1.25;
  letter-spacing: 0.04em;
  color: var(--n2);
  text-transform: uppercase;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

@media (max-width: 400px) {
  .now-playing {
    width: 64px;
    top: -8px;
    right: -8px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .now-playing {
    animation: none;
  }
}

.live-dot {
  width: 6px;
  height: 6px;
  background: var(--ru);
  border-radius: 50%;
  animation: live-dot-pulse 2s ease-in-out infinite;
}

@keyframes live-dot-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.2; }
}

/* Amounts matter more than intent here. The first pass moved 2.5% over two
   seconds — about 3px per side on a 250px logo — and measured a 3.7%
   brightness swing in the glow beside it, which is under the threshold where
   the eye picks movement up at all. It read as static. 4.5% and a halo twice
   the strength is still unmistakably a breath rather than a throb, but you
   can actually see it happen. */
@keyframes live-breathe {
  0%, 100% {
    transform: scale(1);
    border-color: rgba(145, 70, 255, 0.35);
    box-shadow: 0 0 0 rgba(145, 70, 255, 0);
  }
  50% {
    transform: scale(1.045);
    border-color: #B084FF;
    box-shadow:
      0 0 28px rgba(145, 70, 255, 0.75),
      0 0 60px rgba(145, 70, 255, 0.35);
  }
}

/* Reduced motion, but not reduced signal.
 *
 * Switching the breath off entirely is what made the live logo look dead on
 * a machine with Windows' animation effects turned off — which is a common
 * setting, not an edge case. The request that setting expresses is about
 * MOVEMENT: travel, parallax, zoom, the things that cause vestibular trouble.
 * A colour and glow fade moves nothing.
 *
 * So the scale is dropped and the pulse stays, slowed to six seconds and
 * softened. That's 0.17Hz — three orders of magnitude below the three-per-
 * second flash threshold — and the border never drops below a visible purple,
 * so the live state is carried by colour on its own even at the trough. */
@media (prefers-reduced-motion: reduce) {
  .live-dot {
    animation: none;
  }

  .hero-logo-wrap.is-live .hero-logo {
    animation: live-breathe-soft 6s ease-in-out infinite;
  }

  /* Its own arrival, landing on live-breathe-soft's opening frame rather than
     live-breathe's — the two rest at different border alphas, and handing over
     to the wrong one shows. */
  .hero-logo-wrap.is-live.is-arriving .hero-logo {
    animation: live-arrive-soft 1.1s ease-out forwards;
  }
}

@keyframes live-arrive-soft {
  0% {
    transform: scale(1);
    border-color: rgba(76, 175, 80, 0.2);
    box-shadow: 0 0 0 rgba(145, 70, 255, 0);
  }
  55% {
    transform: scale(1.03);
    border-color: #B084FF;
    box-shadow:
      0 0 26px rgba(145, 70, 255, 0.7),
      0 0 56px rgba(145, 70, 255, 0.3);
  }
  100% {
    transform: scale(1);
    border-color: rgba(145, 70, 255, 0.5);
    box-shadow: 0 0 0 rgba(145, 70, 255, 0);
  }
}

/* Asked for: the logo grows with the pulse here too, not only colour and
   glow. It's 3% against the full breath's 4.5%, over six seconds rather than
   four — slow enough that it reads as the border swelling rather than the
   image jumping, which is what keeps it tolerable for someone who asked for
   less movement. Worth knowing it applies to every visitor with that setting,
   not just this browser. */
@keyframes live-breathe-soft {
  0%, 100% {
    transform: scale(1);
    border-color: rgba(145, 70, 255, 0.5);
    box-shadow: 0 0 0 rgba(145, 70, 255, 0);
  }
  50% {
    transform: scale(1.03);
    border-color: #B084FF;
    box-shadow:
      0 0 26px rgba(145, 70, 255, 0.7),
      0 0 56px rgba(145, 70, 255, 0.3);
  }
}

.wordmark {
  font-family: 'Syne', sans-serif;
  font-weight: 800;
  font-size: clamp(2rem, 6vw, 3.2rem);
  color: var(--n1);
  margin-top: 1.25rem;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.wordmark span {
  color: var(--g);
}

.tagline {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 1rem;
  color: var(--n4);
  text-transform: uppercase;
  letter-spacing: 0.15em;
  margin-top: 0.5rem;
}

/* === Hero Glow (on body::before — see body rule) === */
@keyframes drift {
  0%   { transform: translate(0, 0) rotate(0deg); }
  100% { transform: translate(-2%, 1%) rotate(1deg); }
}

@media (prefers-reduced-motion: reduce) {
  body::before {
    animation: none;
  }
}

/* === Top Bar === */
.topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: rgba(13, 13, 13, 0.92);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-bottom: 1px solid rgba(76, 175, 80, 0.1);
}

.topbar-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0.8rem 2rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.topbar-brand {
  font-family: 'Syne', sans-serif;
  font-weight: 800;
  font-size: 1.1rem;
  letter-spacing: 0.05em;
  color: var(--n1);
}

.topbar-brand span {
  color: var(--g);
}

.topbar-links {
  display: flex;
  gap: 1.25rem;
  align-items: center;
  font-size: 1.1rem;
}

.topbar-links a,
.topbar-links a:link,
.topbar-links a:visited {
  color: var(--n4);
  text-decoration: none;
  transition: color 0.3s;
}

.topbar-links a:hover {
  color: var(--g);
}

/* === Fade-Up Animation === */
@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.fade-up {
  opacity: 0;
  animation: fadeUp 0.6s ease-out forwards;
}

.fade-up-1 { animation-delay: 0s; }
.fade-up-2 { animation-delay: 0.15s; }
.fade-up-3 { animation-delay: 0.3s; }
.fade-up-4 { animation-delay: 0.45s; }
.fade-up-5 { animation-delay: 0.6s; }

@media (prefers-reduced-motion: reduce) {
  .fade-up {
    opacity: 1;
    animation: none;
  }
}

/* === Utilities === */

/* Visible to screen readers, not to eyes. Used for context that sighted
   users get from layout — e.g. "Load position 01:" before a mod name. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* === Content pages (everything except the hero homepage) === */
.page {
  position: relative;
  z-index: 1;
  width: 90%;
  max-width: 860px;
  margin: 0 auto;
  padding: 6rem 0 4rem;
  box-sizing: border-box;
}

.page-header {
  margin-bottom: 2rem;
}

.page-eyebrow {
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-weight: 500;
  font-size: 0.7rem;
  color: var(--em);
  text-transform: uppercase;
  letter-spacing: 0.15em;
  margin-bottom: 0.4rem;
}

.page-title {
  font-family: 'Syne', sans-serif;
  font-weight: 800;
  font-size: clamp(1.8rem, 5vw, 2.6rem);
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--n1);
}

.page-intro {
  margin-top: 0.75rem;
  max-width: 60ch;
  color: var(--n3);
  font-size: 0.95rem;
}

/* Server facts — a definition list, so the label/value pairing is real
   structure rather than two spans that happen to sit next to each other. */
.server-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1.5rem;
}

.server-fact {
  background: var(--dk);
  border: 1px solid rgba(255, 255, 255, 0.05);
  border-radius: 6px;
  padding: 0.5rem 0.75rem;
}

.server-fact dt {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--n5);
}

.server-fact dd {
  font-size: 0.9rem;
  color: var(--n2);
  margin-top: 0.15rem;
}

/* === Mods: controls === */
.mods-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 0.75rem;
  margin: 2rem 0 1rem;
  padding-top: 1.5rem;
  border-top: 1px solid rgba(76, 175, 80, 0.15);
}

.mods-filter {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  flex: 1 1 260px;
}

.mods-filter label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--n4);
}

.mods-filter input {
  font-family: 'Outfit', sans-serif;
  font-size: 0.95rem;
  color: var(--n1);
  background: var(--dk);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 6px;
  padding: 0.55rem 0.75rem;
  width: 100%;
  box-sizing: border-box;
}

.mods-filter input:focus-visible {
  outline: 2px solid var(--g);
  outline-offset: 2px;
  border-color: transparent;
}

.mods-filter input::placeholder {
  color: var(--n5);
}

.mods-count {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.75rem;
  color: var(--n4);
  padding-bottom: 0.6rem;
}

/* === Jump nav (shared: mods tiers, command groups) === */
.page-jump {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 0.75rem;
  margin-bottom: 2.5rem;
}

.page-jump-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--n5);
}

.page-jump-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.page-jump-links a {
  font-size: 0.8rem;
  text-decoration: none;
  color: var(--gl);
  background: rgba(76, 175, 80, 0.08);
  border: 1px solid rgba(76, 175, 80, 0.2);
  border-radius: 999px;
  padding: 0.2rem 0.65rem;
}

.page-jump-links a:hover {
  background: rgba(76, 175, 80, 0.16);
}

/* === Section headings (shared: mod tiers, command groups) === */
.tier {
  margin-bottom: 2.5rem;
  scroll-margin-top: 5rem; /* clear the fixed topbar when jumped to */
}

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  border-bottom: 1px solid rgba(76, 175, 80, 0.2);
  padding-bottom: 0.5rem;
}

.section-heading {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: 1.1rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--n1);
}

.tier-number {
  color: var(--g);
  margin-right: 0.5rem;
}

.section-count {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.7rem;
  color: var(--n5);
}

.section-note {
  margin-top: 0.6rem;
  max-width: 65ch;
  font-size: 0.85rem;
  color: var(--n4);
}

/* === Mods: entries === */
.mod-list {
  margin-top: 1rem;
}

.mod {
  display: flex;
  gap: 0.9rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

.mod:last-child {
  border-bottom: none;
}

/* Load-order rail. aria-hidden in the markup — the same information is in
   the mod name as visually-hidden text, so it isn't read out twice. */
.mod-order {
  flex: 0 0 3.2rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.8rem;
  color: var(--n5);
  padding-top: 0.15rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.mod-body {
  flex: 1 1 auto;
  min-width: 0;
}

.mod-name {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: 1rem;
  color: var(--n1);
}

.mod-flags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.35rem;
}

/* Flags carry their meaning as text, never as colour alone. */
.mod-flag {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--am);
  background: rgba(245, 166, 35, 0.1);
  border: 1px solid rgba(245, 166, 35, 0.3);
  border-radius: 4px;
  padding: 0.1rem 0.4rem;
}

.mod-desc {
  margin-top: 0.4rem;
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--n3);
  max-width: 68ch;
}

.mod-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.5rem;
}

.mod-id {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.7rem;
  color: var(--n5);
  background: var(--dk);
  border-radius: 3px;
  padding: 0.1rem 0.35rem;
  word-break: break-all;
}

.mod-link {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  text-decoration: none;
  color: var(--gl);
  border-bottom: 1px solid rgba(129, 199, 132, 0.4);
}

.mod-link:hover {
  color: var(--gp);
}

.mods-footnote {
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid rgba(76, 175, 80, 0.15);
  font-size: 0.8rem;
  color: var(--n5);
  max-width: 60ch;
}

@media (max-width: 520px) {
  /* Stack the order rail above the entry rather than squeezing the text
     into a narrow column. */
  .mod {
    flex-direction: column;
    gap: 0.25rem;
  }

  .mod-order {
    text-align: left;
  }
}

/* === Stream commands === */
.cmd-group {
  margin-bottom: 2.5rem;
  scroll-margin-top: 5rem; /* clear the fixed topbar when jumped to */
}

.cmd-list {
  margin-top: 1rem;
}

/* A <dl> where each command is a <div> of dt + dd — valid HTML, and the only
   way to give one command its own grid row. */
.cmd {
  display: grid;
  grid-template-columns: minmax(8rem, 11rem) 1fr;
  gap: 0.4rem 1rem;
  align-items: start;
  padding: 0.9rem 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

.cmd:last-child {
  border-bottom: none;
}

.cmd-term {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
}

.cmd-name,
.cmd-alias {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.85rem;
  font-weight: 500;
  border-radius: 5px;
  padding: 0.2rem 0.5rem;
  background: var(--dk);
  white-space: nowrap;
}

.cmd-name {
  color: var(--gl);
  border: 1px solid rgba(76, 175, 80, 0.25);
}

/* !so next to !shoutout — same command, quieter treatment. */
.cmd-alias {
  color: var(--n4);
  border: 1px solid rgba(255, 255, 255, 0.1);
  font-size: 0.75rem;
}

.cmd-flags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

/* Amber, not green: these two commands cost you your chat privileges for a
   bit, and that should be visible before you type it — not a surprise. */
.cmd-flag {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--am);
  border: 1px solid rgba(245, 166, 35, 0.35);
  border-radius: 3px;
  padding: 0.12rem 0.35rem;
}

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

.cmd-desc {
  font-size: 0.9rem;
  color: var(--n2);
}

.cmd-line {
  margin-top: 0.45rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.45rem;
}

.cmd-label {
  flex: 0 0 auto;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--n4);
}

.cmd-snippet {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.8rem;
  color: var(--gp);
  background: var(--dk);
  border-radius: 4px;
  padding: 0.15rem 0.4rem;
  overflow-wrap: anywhere;
}

/* What the bot says back. The reset kills q's quote marks site-wide, so put
   them back here — they're what distinguishes a reply from an instruction. */
.cmd-reply {
  font-size: 0.85rem;
  font-style: italic;
  color: var(--n4);
  quotes: '\201C' '\201D';
}

.cmd-reply::before {
  content: open-quote;
}

.cmd-reply::after {
  content: close-quote;
}

.cmd-linkout {
  font-size: 0.8rem;
  overflow-wrap: anywhere;
}

@media (max-width: 640px) {
  /* The command and its explanation stack; a 8rem term column would wrap
     "!russianroulette" to three lines. */
  .cmd {
    grid-template-columns: 1fr;
  }
}

/* === Topbar page links === */
.topbar-nav {
  display: flex;
  gap: 1rem;
  margin-left: auto;
  margin-right: 1.25rem;
}

.topbar-page-link,
.topbar-page-link:link,
.topbar-page-link:visited {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--n4);
  text-decoration: none;
  transition: color 0.3s;
}

.topbar-page-link:hover {
  color: var(--g);
}

/* Current page marked structurally with aria-current; this is the visual
   half of the same information. */
.topbar-page-link[aria-current="page"] {
  color: var(--n1);
  border-bottom: 1px solid var(--g);
}

a.topbar-brand,
a.topbar-brand:link,
a.topbar-brand:visited {
  color: var(--n1);
  text-decoration: none;
}

/* Brand + three page links + five social icons is a lot for a phone. Let the
   bar wrap rather than overflow, and give content pages room for the taller
   bar that results. */
@media (max-width: 640px) {
  .topbar-inner {
    padding: 0.6rem 1rem;
    flex-wrap: wrap;
    gap: 0.4rem 0.75rem;
  }

  .topbar-nav {
    margin-right: 0.5rem;
    gap: 0.7rem;
  }

  .topbar-page-link {
    font-size: 0.7rem;
  }

  .topbar-links {
    gap: 0.9rem;
    font-size: 1rem;
  }

  .page {
    padding-top: 7.5rem;
  }
}

/* === Broadcasts Section === */
#broadcasts {
  margin-top: 0;
  text-align: center;
}

.broadcasts-label {
  font-family: 'JetBrains Mono', monospace;
  font-weight: 500;
  font-size: 0.7rem;
  color: var(--em);
  text-transform: uppercase;
  letter-spacing: 0.15em;
  margin-bottom: 0.25rem;
}

.broadcasts-heading {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: 1.2rem;
  color: var(--n1);
  margin-bottom: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

#broadcasts-list {
  display: flex;
  flex-direction: row;
  gap: 0.75rem;
  align-items: stretch;
  overflow-x: auto;
  overflow-y: hidden;
  padding-inline: 1rem;
  scrollbar-width: thin;
  scroll-snap-type: x proximity;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 1.5rem, #000 calc(100% - 1.5rem), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 1.5rem, #000 calc(100% - 1.5rem), transparent 100%);
}

.broadcast-card {
  display: flex;
  flex-direction: column;
  flex: 0 0 280px;
  scroll-snap-align: start;
  background: var(--dk);
  border: 1px solid rgba(255, 255, 255, 0.04);
  border-radius: 8px;
  /* Padded all round: the thumbnail is inset on every side and the box art
     overhangs its bottom edge, so nothing bleeds and nothing is clipped —
     hence no overflow: hidden here. */
  padding: 0.5rem;
  text-decoration: none;
  color: var(--n1);
  transition: border-color 0.2s, background 0.2s;
}

.broadcast-card:hover,
.broadcast-card:focus-visible {
  border-color: rgba(76, 175, 80, 0.3);
  background: var(--ch);
}

.broadcast-card:link,
.broadcast-card:visited {
  color: var(--n1);
}

/* Positioning context for the box art, which hangs over the thumbnail's
   bottom edge. line-height: 0 kills the inline-image gap under the img. */
.broadcast-media {
  position: relative;
  line-height: 0;
}

.broadcast-thumb {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 6px;
}

/* Box art straddling the thumbnail's bottom-right corner: mostly over the
   image, overhanging into the row below, where the category name sits to
   its left. The shadow is what separates it from the frame grab behind. */
.broadcast-boxart {
  position: absolute;
  right: 8px;
  bottom: -34px;
  width: 64px;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: 4px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.65);
}

.broadcast-info {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  text-align: left;
  overflow: hidden;
  padding-top: 0.5rem;
}

/* Category name, level with the overhanging art. min-height reserves the
   band the art hangs into so the title always clears it; the padding keeps
   long names from running underneath it. Both only apply when there IS art
   — see .has-boxart, set by the JS once the image loads. */
.broadcast-game {
  font-family: 'JetBrains Mono', monospace;
  font-weight: 500;
  font-size: 0.7rem;
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--n2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.broadcast-card.has-boxart .broadcast-game {
  min-height: 36px;
  padding-right: 78px;
}

/* Without art the category is just the first line of the info block. */
.broadcast-card.has-boxart .broadcast-title {
  margin-top: 0.15rem;
}

.broadcast-title {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: 0.9rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.broadcast-meta {
  font-family: 'JetBrains Mono', monospace;
  font-weight: 400;
  font-size: 0.75rem;
  color: var(--n4);
}

.broadcasts-empty {
  font-family: 'Outfit', sans-serif;
  font-weight: 200;
  font-size: 0.9rem;
  color: var(--n5);
}


/* === Site footer === */
/* New in Phase 0. The site used to end at its last section with nothing
   below it, which meant the topbar had to carry every link. The footer is
   where the secondary pages live so the bar can stay at four or five —
   both render from the same site model, so which one a page appears in is
   a field in tools/theme/site.mjs, not a decision made twice. */
.site-footer {
  position: relative;
  z-index: 1;
  margin-top: 6rem;
  border-top: 1px solid rgba(76, 175, 80, 0.12);
  background: rgba(13, 13, 13, 0.6);
}

.site-footer-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2.5rem 2rem 1.5rem;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 2rem;
}

.site-footer-brand {
  margin-right: auto;
}

.site-footer-wordmark,
.site-footer-wordmark:link,
.site-footer-wordmark:visited {
  font-family: 'Syne', sans-serif;
  font-weight: 800;
  font-size: 1.1rem;
  letter-spacing: 0.05em;
  color: var(--n1);
  text-decoration: none;
}

.site-footer-wordmark span {
  color: var(--g);
}

.site-footer-tagline {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  color: var(--n5);
  margin-top: 0.4rem;
}

.site-footer-nav {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.site-footer-link,
.site-footer-link:link,
.site-footer-link:visited {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--n4);
  text-decoration: none;
  transition: color 0.3s;
}

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

/* Borrows .topbar-links for the icon styling — same icons, same treatment,
   one set of rules. Only the alignment differs. */
.site-footer-social {
  align-self: flex-start;
}

.site-footer-legal {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 2rem 2rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  color: var(--n6);
}

@media (max-width: 640px) {
  .site-footer-inner {
    padding: 2rem 1rem 1rem;
    gap: 1.5rem;
  }

  .site-footer-brand {
    margin-right: 0;
    width: 100%;
  }

  .site-footer-nav {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 0.75rem 1.1rem;
  }

  .site-footer-legal {
    padding: 0 1rem 1.5rem;
  }
}

/* === 404 === */
/* Reachable more often than you'd think: /mods/ is retired with no redirect
   and old chat links keep pointing at it. This turns a dead end into a
   redirect the visitor performs themselves. */
.notfound-nav {
  margin-top: 2rem;
}

.notfound-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.75rem;
}

.notfound-link,
.notfound-link:link,
.notfound-link:visited {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--n2);
  text-decoration: none;
  border: 1px solid var(--n6);
  border-radius: 999px;
  padding: 0.4rem 0.9rem;
  transition: color 0.3s, border-color 0.3s;
}

.notfound-link:hover {
  color: var(--g);
  border-color: var(--g);
}

/* === Games index === */
/* Designed to read as deliberate with one entry as well as with six — the
   cards are full-width rows rather than a grid that would leave a lone card
   stranded beside empty columns. */
.game-cards {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  margin-top: 2rem;
}

.game-card {
  border: 1px solid var(--n6);
  border-radius: 4px;
  background: rgba(26, 26, 26, 0.5);
  transition: border-color 0.3s;
}

.game-card:hover {
  border-color: rgba(76, 175, 80, 0.5);
}

.game-card-body {
  padding: 1.5rem 1.6rem;
}

.game-card-name {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: 1.45rem;
  letter-spacing: -0.01em;
}

.game-card-name a,
.game-card-name a:link,
.game-card-name a:visited {
  color: var(--n1);
  text-decoration: none;
}

.game-card-name a:hover {
  color: var(--g);
}

.game-card-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.game-card-tag {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--gl);
  border: 1px solid rgba(76, 175, 80, 0.3);
  border-radius: 999px;
  padding: 0.2rem 0.6rem;
}

.game-card-blurb {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.95rem;
  color: var(--n3);
  margin-top: 0.85rem;
  max-width: 62ch;
}

/* === Game hub === */
.game-section {
  margin-top: 3rem;
}

/* The private-server line. Stated plainly on the page rather than left to be
   asked — an unanswered "how do I join?" is worse than a clear no. */
.game-access {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.92rem;
  color: var(--n4);
  border-left: 2px solid var(--n6);
  padding-left: 1rem;
  margin-top: 1.5rem;
  max-width: 62ch;
}

/* What you're looking at — the mods a viewer would actually notice. */
.highlight-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1.25rem 1.75rem;
  margin-top: 1.5rem;
  list-style: none;
}

.highlight {
  border-top: 1px solid var(--n6);
  padding-top: 0.9rem;
}

.highlight-name {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: 1rem;
}

.highlight-name a,
.highlight-name a:link,
.highlight-name a:visited {
  color: var(--n1);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.highlight-name a:hover {
  color: var(--g);
  border-bottom-color: var(--g);
}

.highlight-why {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.92rem;
  color: var(--n3);
  margin-top: 0.4rem;
}

/* House rules — sandbox settings and why they're set where they are. */
.rule-list {
  margin-top: 1.5rem;
}

.rule {
  border-top: 1px solid var(--n6);
  padding: 0.9rem 0;
}

.rule-name {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: 1rem;
  color: var(--n1);
}

.rule-body {
  margin-top: 0.3rem;
}

.rule-setting {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.8rem;
  color: var(--gl);
}

.rule-why {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.92rem;
  color: var(--n3);
  margin-top: 0.35rem;
  max-width: 62ch;
}

/* Modlist summary on the hub — the shape of the list, not the list itself. */
.mod-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 0.5rem 1.5rem;
  margin-top: 1.25rem;
  list-style: none;
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.92rem;
  color: var(--n3);
}

.mod-summary li {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  border-bottom: 1px solid rgba(66, 66, 66, 0.5);
  padding-bottom: 0.4rem;
}

.mod-summary-count {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.78rem;
  color: var(--g);
  min-width: 1.6rem;
  text-align: right;
}

.mod-summary-link {
  margin-top: 1.5rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.mod-summary-link a,
.mod-summary-link a:link,
.mod-summary-link a:visited {
  color: var(--g);
  text-decoration: none;
  border-bottom: 1px solid rgba(76, 175, 80, 0.4);
  padding-bottom: 0.15rem;
}

.mod-summary-link a:hover {
  color: var(--gl);
  border-bottom-color: var(--gl);
}

/* The eyebrow links back to the hub from the full modlist. */
.page-eyebrow a,
.page-eyebrow a:link,
.page-eyebrow a:visited {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid rgba(76, 175, 80, 0.35);
}

.page-eyebrow a:hover {
  color: var(--g);
}

/* === Blog index === */
.post-list {
  list-style: none;
  margin-top: 2rem;
}

.post-card {
  border-top: 1px solid var(--n6);
  padding: 1.6rem 0;
}

.post-card:last-child {
  border-bottom: 1px solid var(--n6);
}

.post-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--n5);
}

.post-dot {
  color: var(--n6);
}

.post-back,
.post-back:link,
.post-back:visited {
  color: var(--g);
  text-decoration: none;
}

.post-back:hover {
  color: var(--gl);
}

.post-card-title {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: 1.4rem;
  letter-spacing: -0.01em;
  margin-top: 0.5rem;
}

.post-card-title a,
.post-card-title a:link,
.post-card-title a:visited {
  color: var(--n1);
  text-decoration: none;
}

.post-card-title a:hover {
  color: var(--g);
}

.post-summary {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.95rem;
  color: var(--n3);
  margin-top: 0.5rem;
  max-width: 64ch;
}

.post-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.75rem;
}

.post-tag {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--n4);
  border: 1px solid var(--n6);
  border-radius: 999px;
  padding: 0.18rem 0.55rem;
}

.blog-empty {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  color: var(--n4);
  margin-top: 2rem;
}

/* === Post body === */
/* Generated from markdown, so these are element selectors rather than
   classes — the renderer emits plain tags on purpose and giving it a class
   vocabulary would mean teaching it the design system. Scoped under
   .post-body so nothing here leaks into the hand-built pages. */
.post-body {
  max-width: 68ch;
  margin-top: 2.5rem;
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 1.02rem;
  line-height: 1.75;
  color: var(--n2);
}

.post-body > * + * {
  margin-top: 1.1rem;
}

.post-body h2,
.post-body h3,
.post-body h4 {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  color: var(--n1);
  letter-spacing: -0.01em;
  line-height: 1.25;
}

.post-body h2 {
  font-size: 1.5rem;
  margin-top: 2.75rem;
}

.post-body h3 {
  font-size: 1.15rem;
  margin-top: 2rem;
}

.post-body h4 {
  font-size: 1rem;
  margin-top: 1.75rem;
}

.post-body a,
.post-body a:link,
.post-body a:visited {
  color: var(--gl);
  text-decoration: none;
  border-bottom: 1px solid rgba(129, 199, 132, 0.4);
}

.post-body a:hover {
  color: var(--g);
  border-bottom-color: var(--g);
}

.post-body strong {
  font-weight: 500;
  color: var(--n1);
}

.post-body ul,
.post-body ol {
  padding-left: 1.3rem;
}

.post-body li + li {
  margin-top: 0.35rem;
}

.post-body li::marker {
  color: var(--n5);
}

.post-body code {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.85em;
  color: var(--gp);
  background: rgba(76, 175, 80, 0.1);
  border-radius: 3px;
  padding: 0.1em 0.35em;
}

.post-body pre {
  background: var(--dk);
  border: 1px solid var(--n6);
  border-radius: 4px;
  padding: 1rem 1.1rem;
  /* The one thing allowed to scroll sideways — the page body never does. */
  overflow-x: auto;
  margin-top: 1.4rem;
}

.post-body pre code {
  background: none;
  padding: 0;
  color: var(--n2);
  font-size: 0.82rem;
  line-height: 1.65;
}

.post-body blockquote {
  border-left: 2px solid var(--g);
  padding-left: 1.1rem;
  color: var(--n3);
  font-style: italic;
}

.post-body blockquote > * + * {
  margin-top: 0.8rem;
}

.post-body hr {
  border: 0;
  border-top: 1px solid var(--n6);
  margin-top: 2.5rem;
}

.post-body img {
  max-width: 100%;
  height: auto;
  border-radius: 4px;
}

.blog-feed {
  margin-top: 2rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

.blog-feed a,
.blog-feed a:link,
.blog-feed a:visited {
  color: var(--n4);
  text-decoration: none;
  border-bottom: 1px solid var(--n6);
  padding-bottom: 0.15rem;
}

.blog-feed a:hover {
  color: var(--g);
  border-bottom-color: var(--g);
}

/* === Schedule strip (homepage, under the tagline) === */
/* The most-asked question, answered under the logo. The value is hidden
   until js/schedule.js has put the reader's own timezone in it — a <noscript>
   carries the weekly pattern for anyone without scripts, and the link works
   either way. */
.schedule-strip {
  margin-top: 1.5rem;
}

.schedule-strip-link,
.schedule-strip-link:link,
.schedule-strip-link:visited {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.5rem;
  text-decoration: none;
  border: 1px solid rgba(76, 175, 80, 0.25);
  border-radius: 999px;
  padding: 0.4rem 1rem;
  transition: border-color 0.3s;
}

.schedule-strip-link:hover {
  border-color: var(--g);
}

.schedule-strip-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--n5);
}

.schedule-strip-value,
.schedule-strip-link noscript {
  font-family: 'Outfit', sans-serif;
  font-weight: 400;
  font-size: 0.85rem;
  color: var(--n2);
}

.schedule-strip-link:hover .schedule-strip-value {
  color: var(--gl);
}

/* === Schedule page === */
.next-stream {
  border: 1px solid rgba(76, 175, 80, 0.25);
  border-radius: 4px;
  background: rgba(26, 26, 26, 0.5);
  padding: 1.5rem 1.6rem;
  margin-top: 2rem;
}

.next-stream-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--n5);
}

.next-stream-when {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: clamp(1.3rem, 4vw, 1.8rem);
  line-height: 1.2;
  color: var(--n1);
  margin-top: 0.5rem;
}

.next-stream-count {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.95rem;
  color: var(--gl);
  margin-top: 0.3rem;
}

.next-stream-count:empty {
  display: none;
}

.schedule-section {
  margin-top: 3rem;
}

.week-list {
  margin-top: 1.25rem;
}

.week-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  border-top: 1px solid var(--n6);
  padding: 0.75rem 0;
}

.week-day {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: 1rem;
  color: var(--n1);
}

.week-time {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.85rem;
  color: var(--gl);
  font-variant-numeric: tabular-nums;
}

/* === Game hub: cover art beside the heading === */
.game-head {
  display: flex;
  align-items: flex-start;
  gap: 2rem;
}

/* The header has to be allowed to shrink or the flex row overflows: its
   intro paragraph is long and a flex item won't go below its content width
   without this. */
.game-head .page-header {
  flex: 1 1 auto;
  min-width: 0;
}

.game-art {
  flex: 0 0 auto;
  width: 190px;
  height: auto;
  border-radius: 4px;
  border: 1px solid var(--n6);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
}

/* Below this the art would leave the title about twelve characters wide, so
   it goes above instead — still the first thing you see, just stacked. */
@media (max-width: 720px) {
  .game-head {
    flex-direction: column-reverse;
    gap: 1.5rem;
  }

  .game-art {
    width: 140px;
    align-self: flex-start;
  }
}

/* === Markdown: tables === */
/* Its own scroll container — a wide table is the one thing allowed to run
   past the measure, and the page body must never scroll sideways. */
.post-body .table-wrap {
  overflow-x: auto;
  margin-top: 1.4rem;
}

.post-body table {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.92rem;
}

.post-body th,
.post-body td {
  text-align: left;
  padding: 0.55rem 1rem 0.55rem 0;
  border-bottom: 1px solid var(--n6);
  vertical-align: top;
}

.post-body th {
  font-family: 'JetBrains Mono', monospace;
  font-weight: 500;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--n4);
  white-space: nowrap;
}

.post-body tbody tr:last-child td {
  border-bottom: none;
}

/* === Markdown: task lists === */
.post-body .task-list {
  list-style: none;
  padding-left: 0;
}

.post-body .task {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}

.post-body .task input[type="checkbox"] {
  accent-color: var(--g);
  /* Nudged onto the text baseline — a checkbox aligns to its own box, which
     sits a little high beside a line of Outfit. */
  transform: translateY(0.1em);
  flex: none;
}

/* Done items recede rather than disappear: still readable, clearly finished. */
.post-body .task.is-done {
  color: var(--n5);
  text-decoration: line-through;
  text-decoration-color: var(--n6);
}

/* === Markdown: strikethrough === */
.post-body del {
  color: var(--n5);
  text-decoration-color: var(--n6);
}

/* === Games index: cover art on the cards === */
.game-card.has-art {
  display: flex;
  align-items: flex-start;
  gap: 1.25rem;
}

.game-card.has-art .game-card-body {
  flex: 1 1 auto;
  min-width: 0;
}

.game-card-art {
  flex: 0 0 auto;
  width: 110px;
  height: auto;
  border-radius: 3px;
  border: 1px solid var(--n6);
  /* The card already has its own padding; this pulls the art up and across so
     it sits flush with the card's inner edges rather than floating inside a
     second margin. */
  margin: 1.5rem 1.6rem 1.5rem 0;
}

@media (max-width: 560px) {
  .game-card.has-art {
    flex-direction: column-reverse;
    gap: 0;
  }

  .game-card-art {
    width: 90px;
    margin: 1.5rem 1.6rem 0;
  }
}

/* === Recent streams on a game page === */
/* A grid, not the homepage's carousel: this is a content page you read down,
   not a hero you glance at. Without a layout of its own the cards ignored
   their flex-basis (flex only applies to flex items) and stretched to the
   full column, upscaling a 320px thumbnail about three times. */
#game-broadcasts-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1rem;
  margin-top: 1.25rem;
}

/* === Schedule: game cover per row === */
/* The cover sits left of the day, so the row aligns on a centre line that
   survives the image being there or not. */
.week-row {
  align-items: center;
}

.schedule-art {
  flex: 0 0 auto;
  width: 34px;
  height: 45px;
  object-fit: cover;
  border-radius: 2px;
  border: 1px solid var(--n6);
}

.week-main {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
}

.week-labels {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.week-game {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--gl);
}

.week-row.is-cancelled .week-game,
.week-row.is-cancelled .week-day {
  color: var(--n5);
}

/* === Timeline === */
/* The next few streams along a spine, cards alternating above and below it.
   Built as a component rather than a schedule feature — the same markup would
   carry a save's history or a run of channel milestones. */

/* It has to escape the 860px content column. Over five cards and four gaps
   that width leaves about 150px each, which is enough for a wrapped game name
   and nothing else. ~1100px is where it starts to breathe.
   body has overflow-x: hidden, so the viewport clamp is belt and braces. */
.timeline-section {
  width: min(1140px, calc(100vw - 3rem));
  margin-left: 50%;
  transform: translateX(-50%);
}

/* Leading the page rather than following two other sections, so it doesn't
   need the 3rem gap that separates sections from each other. */
.page-header + .timeline-section {
  margin-top: 1.5rem;
}

/* The section head is centred with the timeline, but its text should still
   line up with the rest of the page. */
.timeline-section .section-head {
  max-width: 860px;
  margin-inline: auto;
}

/* --- narrow: a vertical rail (the default) --- */
.tl {
  list-style: none;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 1.5rem;
  padding-left: 30px;
}

/* The rail is a pseudo-element, not a child element. As a child it would be
   counted by :nth-child, which decides which cards go above the line and
   which column each one sits in — one extra element and every card lands in
   the wrong place, shifted a column and flipped top for bottom. */
/* Amber, against green nodes.
   Both being green flattened the graphic — the nodes stopped reading as
   punctuation and the whole thing looked like one continuous object. Two
   colours is what the reference was doing too: a gold spine with the nodes
   in the louder accent. */
.tl::before {
  content: "";
  position: absolute;
  left: 7px;
  top: 14px;
  bottom: 14px;
  width: 2px;
  background: linear-gradient(180deg,
    transparent, var(--am) 6%, var(--am) 94%, transparent);
}

.tl-item {
  position: relative;
}

.tl-node {
  position: absolute;
  left: -30px;
  top: 20px;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 2px solid var(--g);
  background: var(--bk);
  display: grid;
  place-items: center;
}

.tl-node::after {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--g);
}

/* --- the card --- */
.tl-card {
  background: var(--dk);
  border-radius: 8px;
  border-top: 3px solid var(--g);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.7rem 0.9rem 0.8rem;
}

/* The cover stretches to the card's content height, so its top and bottom
   edges line up with the text beside it however many lines the title runs to.
   A fixed height can't do that — the card is sized by its text, so the art
   ends up short and floating.
   The wrapper is what makes this safe: object-fit only crops when the element
   has a definite size in both axes, and a flex item with height:auto doesn't.
   Stretching the wrapper and absolutely filling it gives the image real
   dimensions, so it crops the sides of a portrait cover rather than squashing
   it — which is what went wrong the first time this was tried. */
.tl-art-wrap {
  flex: none;
  align-self: stretch;
  position: relative;
  width: 58px;
  /* Keeps the card from collapsing shorter than a recognisable cover when a
     title happens to fit on one line. */
  min-height: 74px;
  border-radius: 3px;
  border: 1px solid var(--n6);
  overflow: hidden;
}

.tl-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

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

.tl-date {
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--am);
  white-space: nowrap;
}

.tl-title {
  font-family: 'Syne', sans-serif;
  font-weight: 800;
  font-size: 0.95rem;
  line-height: 1.12;
  letter-spacing: -0.005em;
  color: var(--n1);
  margin-top: 0.3rem;
}

.tl-meta {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.78rem;
  color: var(--n3);
  margin-top: 0.35rem;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* --- cancelled --- */
/* Ember, struck through, and the node hollows out. Three signals, because
   colour alone is not one.
   Ember rather than amber now the spine is amber — a cancelled node has to
   stand out against the line it sits on, not blend into it. It is also the
   redder of the two warm accents, which suits "not happening" better. */
.tl-item.is-off .tl-card {
  border-top-color: var(--em);
}

.tl-item.is-off .tl-title {
  color: var(--n5);
  text-decoration: line-through;
  text-decoration-color: var(--n6);
}

.tl-item.is-off .tl-art {
  filter: grayscale(1);
  opacity: 0.45;
}

.tl-item.is-off .tl-node {
  border-color: var(--em);
}

.tl-item.is-off .tl-node::after {
  background: transparent;
}

.tl-off {
  display: inline-block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.58rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--em);
  margin-top: 0.4rem;
}

/* --- wide: a horizontal spine --- */
@media (min-width: 900px) {
  .tl {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    grid-template-rows: 1fr 46px 1fr;
    column-gap: 1.1rem;
    padding-left: 0;
    min-height: 300px;
  }

  /* display: contents lifts each item's node and card into the grid, so one
     piece of markup serves both layouts. It removes the <li> from the
     accessibility tree, which is fine for a wrapper carrying no role. */
  .tl-item {
    display: contents;
  }

  .tl::before {
    position: static;
    grid-row: 2;
    grid-column: 1 / -1;
    align-self: center;
    width: auto;
    height: 2px;
    background: linear-gradient(90deg,
      transparent, var(--am) 4%, var(--am) 96%, transparent);
  }

  .tl-node {
    position: static;
    grid-row: 2;
    align-self: center;
    justify-self: center;
  }

  /* First entry above the line, then alternating. Alternating is what stops
     five cards reading as a list, and it halves the width each needs. */
  .tl-item:nth-child(odd) .tl-card {
    grid-row: 1;
    align-self: end;
  }

  .tl-item:nth-child(even) .tl-card {
    grid-row: 3;
    align-self: start;
  }

  .tl-item:nth-child(1) > * { grid-column: 1; }
  .tl-item:nth-child(2) > * { grid-column: 2; }
  .tl-item:nth-child(3) > * { grid-column: 3; }
  .tl-item:nth-child(4) > * { grid-column: 4; }
  .tl-item:nth-child(5) > * { grid-column: 5; }
}

/* === Section rail === */
/* The second level of navigation: the topbar keeps one slot per section and
   this lists the pages inside the one you're in.

   Not a dropdown — see tools/theme/subnav.mjs for why. It renders as the
   first child of .page, so it inherits the content column and needs no
   offset of its own under the fixed topbar. */
.subnav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 2rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--n6);
}

.subnav-link,
.subnav-link:link,
.subnav-link:visited {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--n4);
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: 3px;
  padding: 0.35rem 0.7rem;
  transition: color 0.3s, border-color 0.3s;
}

.subnav-link:hover {
  color: var(--g);
  border-color: rgba(76, 175, 80, 0.3);
}

/* The page you're on. A chip rather than the topbar's underline: two levels
   of "you are here" that looked identical would be one signal repeated. */
.subnav-link[aria-current="page"] {
  color: var(--gl);
  background: rgba(76, 175, 80, 0.12);
  border-color: var(--g);
}

/* The section hub's own link in the topbar while you're on a page inside it.
   aria-current="true", not "page" — the page is one level down, and the rail
   is what marks it. Softer than the current-page underline for the same
   reason. */
.topbar-page-link[aria-current="true"] {
  color: var(--n1);
  border-bottom: 1px solid rgba(76, 175, 80, 0.45);
}

/* === Section hub === */
/* A grid rather than the games index's full-width rows: these cards carry a
   name and a line, so two across reads better than one wide one, and the
   section is never going to hold enough pages to need a third column. */
.hub-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 1.25rem;
  margin-top: 2rem;
}

.hub-card {
  border: 1px solid var(--n6);
  border-radius: 4px;
  background: rgba(26, 26, 26, 0.5);
  padding: 1.4rem 1.5rem;
  transition: border-color 0.3s;
}

.hub-card:hover {
  border-color: rgba(76, 175, 80, 0.5);
}

.hub-card-name {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: 1.2rem;
  letter-spacing: -0.01em;
}

.hub-card-name a,
.hub-card-name a:link,
.hub-card-name a:visited {
  color: var(--n1);
  text-decoration: none;
}

.hub-card-name a:hover {
  color: var(--g);
}

.hub-card-blurb {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.92rem;
  color: var(--n3);
  margin-top: 0.6rem;
}

/* === Gear === */
/* The role is the term and the kit is what defines it, so this is a <dl> like
   the command list. The role column is what you scan — "what's the mic?" —
   which is why it leads rather than the product name. */
.gear-group {
  margin-top: 3rem;
}

.gear-list {
  margin-top: 1rem;
}

.gear {
  display: grid;
  grid-template-columns: 9rem 1fr;
  gap: 0.25rem 1.25rem;
  border-top: 1px solid var(--n6);
  padding: 0.85rem 0;
}

.gear-role {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--n5);
  padding-top: 0.2rem;
}

.gear-name {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: 1rem;
  color: var(--n1);
}

.gear-name a,
.gear-name a:link,
.gear-name a:visited {
  color: var(--n1);
  text-decoration: none;
  border-bottom: 1px solid rgba(76, 175, 80, 0.35);
}

.gear-name a:hover {
  color: var(--g);
}

/* Two key lights, two monitors. The count lives in the data as `quantity`
   rather than in the prose, so the detail line doesn't have to repeat it. */
.gear-qty {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: var(--gl);
  border: 1px solid rgba(76, 175, 80, 0.3);
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  margin-left: 0.4rem;
  vertical-align: 0.1em;
}

.gear-detail {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.9rem;
  color: var(--n3);
  margin-top: 0.3rem;
  max-width: 60ch;
}

/* The role column stops earning its width once it wraps to two lines. */
@media (max-width: 560px) {
  .gear {
    grid-template-columns: 1fr;
  }

  .gear-role {
    padding-top: 0;
  }
}

/* === Rewards: the two currencies === */
/* Stated before either list, not in a footnote under them. One currency is
   earned by watching and refunds itself; the other is bought with bits and
   doesn't. Reading a price list without knowing which is which is the exact
   failure this page exists to prevent.

   Colour carries it through the rest of the page — green for the free one,
   amber for the one that costs money — but never alone: every panel and
   every group says it in words too. */
.currencies {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1rem;
  margin-top: 2rem;
}

.currency {
  border: 1px solid var(--n6);
  border-left-width: 3px;
  border-radius: 4px;
  background: rgba(26, 26, 26, 0.5);
  padding: 1.1rem 1.25rem;
}

.currency.is-points {
  border-left-color: var(--g);
}

.currency.is-bits {
  border-left-color: var(--am);
}

.currency-name {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: 1.05rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--n1);
}

.currency-earned {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.9rem;
  color: var(--n3);
  margin-top: 0.5rem;
}

/* The terms. Small and mono because it reads as a rule rather than as prose,
   which is what it is. */
.currency-policy {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  line-height: 1.6;
  margin-top: 0.7rem;
}

.currency.is-points .currency-policy {
  color: var(--gl);
}

.currency.is-bits .currency-policy {
  color: var(--am);
}

/* The PZ prefix, explained once. Same treatment as the private-server line on
   a game hub — a plain statement set off from the page. */
.reward-game-note {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.92rem;
  color: var(--n4);
  border-left: 2px solid var(--n6);
  padding-left: 1rem;
  margin-top: 1.75rem;
  /* .page-jump carries no top margin of its own — everywhere else on the
     site it follows .page-header, which ends in 2rem. Sitting this note
     between the two put the jump chips right under the note's last line. */
  margin-bottom: 2rem;
  max-width: 68ch;
}

/* === Rewards: the lists === */
.reward-group {
  margin-top: 3rem;
}

.reward-list {
  list-style: none;
  margin-top: 1rem;
}

.reward {
  display: flex;
  gap: 1rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

.reward:last-child {
  border-bottom: none;
}

/* The price rail. Fixed width so the costs line up as a column — that column
   is what people scan — and wide enough for five digits, because the
   helicopter costs 10,000. */
.reward-cost {
  flex: 0 0 5.2rem;
  text-align: right;
  padding-top: 0.1rem;
}

.reward-amount {
  display: block;
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: 1.05rem;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
}

.reward-group.is-points .reward-amount {
  color: var(--gl);
}

.reward-group.is-bits .reward-amount {
  color: var(--am);
}

.reward-unit {
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--n5);
  margin-top: 0.1rem;
}

.reward-body {
  flex: 1 1 auto;
  min-width: 0;
}

.reward-name {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: 1rem;
  color: var(--n1);
}

/* The copy as it appears in Twitch — flavour, and the line you'd recognise
   from the redeem screen. */
.reward-desc {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.88rem;
  color: var(--n4);
  margin-top: 0.3rem;
  max-width: 68ch;
}

/* What actually happens. Twitch never shows this, which is most of the reason
   the page exists — so it outranks the flavour text above it. */
.reward-effect {
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--n2);
  margin-top: 0.35rem;
  max-width: 68ch;
}

.reward-input {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.45rem;
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.85rem;
  color: var(--n3);
  margin-top: 0.45rem;
}

.reward-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--n5);
}

.reward-targets {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.4rem;
}

.reward-target {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.7rem;
  color: var(--n5);
  background: var(--dk);
  border-radius: 3px;
  padding: 0.1rem 0.35rem;
  word-break: break-all;
}

/* The price column stops helping once the row is this narrow — it eats a
   third of the width to hold four characters. */
@media (max-width: 520px) {
  .reward {
    flex-direction: column;
    gap: 0.35rem;
  }

  .reward-cost {
    flex: none;
    text-align: left;
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
  }

  .reward-amount,
  .reward-unit {
    display: inline;
    margin-top: 0;
  }
}

/* === Games index: the current one === */
/* Every card states its status now, so the one being played says so rather
   than being the only card with nothing said about it. Filled instead of
   outlined — the text carries the meaning, the fill just stops it reading as
   one more neutral tag in the row. */
.game-card-tag.is-now {
  color: var(--gp);
  background: rgba(76, 175, 80, 0.16);
  border-color: var(--g);
}

/* === Games index: coming up === */
/* Not cards. A planned game has no page to link to and no art to show, so a
   card would be a box drawn around a name. What it has is a date — or an
   honest lack of one — and the reason it's on the list. */
.planned-list {
  list-style: none;
  margin-top: 1.25rem;
}

.planned {
  display: flex;
  gap: 1rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

.planned:last-child {
  border-bottom: none;
}

.planned-when {
  flex: 0 0 6.5rem;
  padding-top: 0.15rem;
}

/* Amber, like every other date on the site — the schedule timeline sets that
   convention and this is the same kind of fact. */
.planned-date {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--am);
}

.planned-date.is-undated {
  color: var(--n5);
}

.planned-body {
  flex: 1 1 auto;
  min-width: 0;
}

.planned-name {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--n1);
}

.planned-note {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--n3);
  margin-top: 0.3rem;
  max-width: 66ch;
}

@media (max-width: 520px) {
  .planned {
    flex-direction: column;
    gap: 0.3rem;
  }

  .planned-when {
    flex: none;
    padding-top: 0;
  }
}

/* === Games index: art on a planned row === */
/* Optional by design: a planned game often has no Twitch category yet, so
   the row has to look deliberate without it rather than look like a missing
   image. Right-hand side, matching the cards above it. */
.planned-art {
  flex: 0 0 auto;
  align-self: flex-start;
  width: 54px;
  height: auto;
  border-radius: 3px;
  border: 1px solid var(--n6);
}

/* === About === */
/* Prose, so it gets a reading measure and more line height than the rest of
   the site. Everything else here is a reference page you scan; this is the
   one page somebody reads top to bottom. */
.about-section {
  margin-top: 2.75rem;
}

.about-prose {
  margin-top: 1rem;
}

.about-prose p {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 1rem;
  line-height: 1.75;
  color: var(--n3);
  max-width: 64ch;
}

.about-prose p + p {
  margin-top: 1rem;
}

.about-prose a,
.about-prose a:link,
.about-prose a:visited {
  color: var(--gl);
  text-decoration: none;
  border-bottom: 1px solid rgba(76, 175, 80, 0.35);
}

.about-prose a:hover {
  color: var(--g);
  border-bottom-color: var(--g);
}

/* The line aimed at anyone else streaming around a chronic condition. It was
   the tail of a paragraph, where it read as an aside; it's the sentence a
   reader in the same position is looking for. */
.about-callout {
  font-family: 'Outfit', sans-serif;
  font-weight: 400;
  font-size: 1.05rem;
  line-height: 1.6;
  color: var(--n1);
  border-left: 3px solid var(--g);
  background: rgba(76, 175, 80, 0.06);
  padding: 1rem 1.25rem;
  margin-top: 1.5rem;
  max-width: 62ch;
}

/* The two brand lines that close the page. Centred and spaced, because they
   are a sign-off rather than another paragraph. */
.about-signoff {
  margin-top: 3.5rem;
  padding-top: 2rem;
  border-top: 1px solid var(--n6);
  text-align: center;
}

.about-signoff-line {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  font-size: 0.95rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--n4);
  line-height: 1.6;
}

/* The last line is the brand's own sign-off, so it takes the brand's green. */
.about-signoff-line:last-child {
  color: var(--g);
  margin-top: 0.5rem;
}

/* === Timeline: tonight === */
/* The node for a stream happening today, in the reader's own timezone —
   js/schedule.js decides that, because a page built on Monday has no way of
   knowing it's being read on Wednesday.

   box-shadow rather than an extra element: .tl-node already spends its
   ::after on the inner dot, and a shadow costs no layout, so the ring can
   expand past the node without moving the card beside it. */
@keyframes tl-pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.55);
  }
  70% {
    box-shadow: 0 0 0 11px rgba(76, 175, 80, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(76, 175, 80, 0);
  }
}

.tl-item.is-today .tl-node {
  animation: tl-pulse 2.4s ease-out infinite;
}

/* Today's card leads with the word as well as the ring, so the date turns
   from amber to green with it — the row it belongs to reads as one thing. */
.tl-item.is-today .tl-date {
  color: var(--gl);
}

/* Reduced motion: breathe instead of expand.
   The ring's radius stays at 4px and only its opacity changes, so nothing
   moves and nothing grows — the thing `prefers-reduced-motion` is actually
   asking us to stop. A fade isn't motion, and it keeps the node alive rather
   than freezing it, which the first version did: it dropped to a static glow
   and lost the signal along with the movement.

   Worth knowing when testing: Windows maps Settings > Accessibility > Visual
   effects > Animation effects onto this query, so a machine with animations
   turned off sees this branch and never the one above. */
@keyframes tl-breathe {
  0%,
  100% {
    box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.1);
  }
  50% {
    box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.42);
  }
}

@media (prefers-reduced-motion: reduce) {
  .tl-item.is-today .tl-node {
    animation: tl-breathe 4s ease-in-out infinite;
  }
}

/* The line the about page ends on. Deliberately not a second boxed callout —
   it sits directly under one, and two boxes in a row would make neither of
   them the last word. Bigger and brighter than the prose, and nothing else. */
.about-closer {
  font-family: 'Outfit', sans-serif;
  font-weight: 400;
  font-size: 1.12rem;
  line-height: 1.6;
  color: var(--n1);
  margin-top: 1.75rem;
  max-width: 62ch;
}
