/* === 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;
}

/* Inside a section the heading has already opened the space, so the cards
   sit at the same distance below it as the planned list does below its own. */
.game-section .game-cards {
  margin-top: 1.25rem;
}

.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);
}

/* A card with no page behind it doesn't light up when you point at it.
   The index lists what's played, and not everything played earns a hub —
   Battlefield 6 has no modlist, no server and no settings to explain. The
   card stays; the hover response would be a promise of a page that isn't
   there, and it's the only cue separating a card you can open from one you
   can't now that both exist. */
.game-card.is-pageless:hover {
  border-color: var(--n6);
}

.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 rotation line, on a game that isn't the current one. Amber rather than
   green: it is a qualification on everything below it, not a feature of it.
   The index says the same thing in a tag, but most arrivals here come from a
   search and never see the index. */
.game-status {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.92rem;
  color: var(--n3);
  margin-top: 1rem;
  padding-left: 0.85rem;
  border-left: 2px solid var(--am);
  max-width: 62ch;
}

.game-status a,
.game-status a:link,
.game-status a:visited {
  color: var(--gl);
  white-space: nowrap;
}

/* 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. */
/* Wider than the 1140px it started at. Five cards across is the layout's
   whole point and each needs room for a game title; at 1140 the text column
   came out around 115px, narrower than the word "Battlefield" — which is
   why titles were escaping their cards. */
.timeline-section {
  width: min(1320px, 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(--n1) 6%, var(--n1) 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(--n1);
  background: var(--bk);
  display: grid;
  place-items: center;
}

/* White, like the spine. They no longer differ in colour, which was the
   earlier ask — they differ in shape instead: a filled ring on a hairline.
   The two states that genuinely need telling apart keep colours of their
   own, and both are about a particular night rather than the furniture:
   ember for a cancelled node, a green pulse for tonight. */
.tl-node::after {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--n1);
}

/* --- the card --- */
/* A 16:9 band with a body under it, and nothing else. One shape for every
   card is the point: a scheduled night and a broadcast night used to want
   different pictures — a portrait cover and a landscape still — and the card
   spent its life reconciling them, ending up either with a crop of somebody's
   shoulder or with a past half towering over the future half.

   No min-height any more. Both things that used to vary are fixed: the band
   is a ratio of the card's own width, which is identical for every card in a
   row, and the title reserves two lines whether it needs them or not. Equal
   heights fall out rather than being propped up. */
.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);

  /* A block, not a flex row. The band runs edge to edge, so the padding
     belongs to .tl-body, and the overflow has to be clipped or the picture's
     square corners sit proud of the card's rounded ones. */
  display: block;
  overflow: hidden;

  /* The rail layout gives the card the full column, and 16:9 of an 800px
     column is a half-metre-tall photograph of a video game. Capping the card
     caps the band with it. Lifted again in both grid layouts below, where the
     column is doing the capping. */
  max-width: 420px;
}

/* --- the band --- */
/* aspect-ratio on the container rather than on the image, so the box is the
   right shape before anything has loaded and stays that shape if nothing ever
   does. Four different things can end up inside it — a VOD frame, our key
   art, Twitch's box art, or just the game's name — and the card must not
   change height depending on which. See bandFor() in js/schedule.js. */
.tl-band {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-bottom: 1px solid var(--n6);

  /* Showing through wherever the picture doesn't reach: behind a letterboxed
     portrait cover, and behind the name panel. Not flat black — a card with
     no art should still look like part of this site. */
  background:
    radial-gradient(120% 140% at 50% 0%, rgba(76, 175, 80, 0.10), transparent 70%),
    var(--bk);
}

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

/* Twitch's own box art, which is 3:4 against this box's 16:9. Contained and
   centred rather than cropped: a portrait cover cropped to landscape is a
   close-up of the middle of a logo. This is what a game looks like until
   somebody draws it key art — see data/game-art.json. */
.tl-band-img.is-cover {
  object-fit: contain;
  padding: 0.35rem;
}

/* Last resort: no picture of any kind, so the name is the picture. */
.tl-band-panel {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 0.5rem 0.75rem;
  font-family: 'Syne', sans-serif;
  font-weight: 800;
  font-size: 0.85rem;
  line-height: 1.15;
  letter-spacing: -0.01em;
  text-align: center;
  text-wrap: balance;
  color: var(--n4);
  overflow-wrap: break-word;
}

.tl-body {
  min-width: 0;
  padding: 0.6rem 0.8rem 0.75rem;
}

.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;

  /* Two lines, always — clamped so a long one can't grow the card, and
     reserved so a short one can't shrink it. Broadcast titles run to seventy
     characters where a game name runs to thirteen, and without both halves of
     this the cards in a row are wildly different heights.

     The full title stays on the element's title attribute, so clamping hides
     it rather than losing it. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: calc(1.12em * 2);

  /* A game called "Battlefield" is one word longer than the text column in a
     five-across timeline, and an unbreakable word in a flex item does not
     wrap — it runs straight out of the card, over the shadow and onto the
     page. break-word rather than anywhere: a word is broken only when it
     cannot fit on a line of its own, so ordinary titles are untouched. */
  overflow-wrap: break-word;
}

.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-band-img {
  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;
}

/* --- past: it has already happened --- */
/* Dimmed, not dropped. The point of a window is seeing where you are in the
   run, and that needs something behind you.

   Grey rather than ember, because "over" and "called off" are different
   things and a night can be both. These rules sit AFTER the cancelled ones
   so a past cancelled night reads as history first while keeping its
   strikethrough and its "Off" label — the facts stack, the colour doesn't. */
.tl-item.is-past .tl-card {
  border-top-color: var(--n6);
  box-shadow: none;
  /* One dial rather than recolouring every child: opacity takes the title,
     the date, the meta and the label with it. */
  opacity: 0.55;
}

.tl-item.is-past .tl-band-img {
  filter: grayscale(1);
  /* Restored on hover for a card that links to its VOD, a few rules down. */
  transition: filter 0.2s;
}

.tl-item.is-past .tl-node {
  border-color: var(--n6);
}

.tl-item.is-past .tl-node::after {
  background: var(--n6);
}

/* --- a past night that was actually broadcast --- */
/* The band is already the right shape for a VOD frame, so this adds no
   layout of its own — only the fact that the whole card is a link.
   The band stays even when the frame doesn't: a VOD expires after a couple
   of months and its thumbnail 404s with it, and bandFor() falls through to
   the key art rather than removing the picture, which would leave one card a
   different height from the four beside it. */
.tl-card.is-vod {
  text-decoration: none;
  color: inherit;
}

/* Dimmed until you go near it. Past is the resting state, but a link nobody
   can see is not a link — hovering restores it, which also says it is
   clickable without a second affordance. */
.tl-item.is-past .tl-card.is-vod {
  transition: opacity 0.2s, border-color 0.2s;
}

.tl-item.is-past .tl-card.is-vod:hover,
.tl-item.is-past .tl-card.is-vod:focus-visible {
  opacity: 1;
  border-top-color: var(--g);
}

.tl-item.is-past .tl-card.is-vod:hover .tl-band-img,
.tl-item.is-past .tl-card.is-vod:focus-visible .tl-band-img {
  filter: none;
}

/* A chip over the artwork, not a line under the title. Under the title it
   added a line to the body, which made a cancelled card taller than the four
   beside it — and every card being the same size is what the band is for.
   Over the picture it is also harder to miss, which suits the one state on
   this page that changes somebody's evening.

   Tinted ground, ember hairline, ember text: the same three-part treatment
   the current page gets in the section rail, rather than a new device. */
.tl-off {
  position: absolute;
  top: 0.4rem;
  left: 0.4rem;
  max-width: calc(100% - 0.8rem);

  /* The image is positioned too and comes after this in the DOM, so without
     a stacking order it paints straight over the label. */
  z-index: 1;

  font-family: 'JetBrains Mono', monospace;
  font-size: 0.58rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--em);
  background: rgba(13, 13, 13, 0.82);
  border: 1px solid var(--em);
  border-radius: 3px;
  padding: 0.2rem 0.4rem;

  /* A note from the built-in schedule can run long. One line here, and the
     full text stays on the title attribute. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- mid: two across, and the spine goes ---
   The gap between a phone and the five-across row. A rail here wastes most
   of the width — one 420px card against 800px of page — but five columns
   still don't fit, so it is two columns of cards and no spine at all.

   Dropping the spine rather than keeping a vestigial one is the honest call:
   a line that connects two columns reading left-to-right-then-down is not a
   timeline, it is decoration. The dates are on every card and the order is
   the reading order. */
@media (min-width: 720px) and (max-width: 1199px) {
  .tl {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1rem;
    padding-left: 0;
  }

  .tl::before,
  .tl-node {
    display: none;
  }

  /* The column is the cap now. */
  .tl-card {
    max-width: none;
  }
}

/* --- wide: a horizontal spine ---
   1200, not 900. Below this the five columns are narrower than the titles
   that go in them, and two roomy columns beat five cramped ones. */
@media (min-width: 1200px) {
  .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;
  }

  /* The column is the cap here too — at 1320px it is about 250px, well
     inside the 420px the rail layout needs. */
  .tl-card {
    max-width: none;
  }

  /* 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(--n1) 4%, var(--n1) 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;
}

/* === Clips === */
/* The same two layouts as broadcasts — a sideways carousel on the homepage,
   a grid on a game hub — because they sit one above the other and a second
   visual language between them would just look like a mistake.

   The card differs where the content does. A VOD card sells three hours on
   its title; a clip card sells twenty seconds on its picture, so the
   thumbnail is the card and the words underneath are small. */
#clips-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%);
}

#game-clips-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 1rem;
  margin-top: 1.25rem;
}

.clip-card {
  display: flex;
  flex-direction: column;
  background: var(--dk);
  border: 1px solid rgba(255, 255, 255, 0.04);
  border-radius: 8px;
  padding: 0.5rem;
  text-decoration: none;
  color: var(--n1);
  transition: border-color 0.2s, background 0.2s;
}

/* Only the carousel needs a fixed width; in the grid the track sets it. */
#clips-list .clip-card {
  flex: 0 0 260px;
  scroll-snap-align: start;
}

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

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

/* Positioning context for the duration and the featured flag, both of which
   sit over the picture. line-height: 0 kills the inline-image gap. */
.clip-media {
  position: relative;
  line-height: 0;
}

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

/* A clip whose thumbnail didn't load. The card keeps its shape rather than
   collapsing to a line of text among neighbours that are mostly picture. */
.clip-media.is-artless {
  aspect-ratio: 16 / 9;
  border-radius: 6px;
  background: linear-gradient(150deg, rgba(76, 175, 80, 0.14), rgba(13, 13, 13, 0.9) 70%), var(--ch);
}

.clip-duration {
  position: absolute;
  right: 6px;
  bottom: 6px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  line-height: 1;
  color: var(--n1);
  background: rgba(13, 13, 13, 0.82);
  border-radius: 3px;
  padding: 0.2rem 0.35rem;
}

/* Marks the ones Mark chose, not the ones that merely did well. Text rather
   than a colour on its own, same rule as every other flag on the site. */
.clip-featured {
  position: absolute;
  left: 6px;
  top: 6px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--bk);
  background: var(--am);
  border-radius: 3px;
  padding: 0.22rem 0.4rem;
  font-weight: 500;
}

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

.clip-game {
  font-family: 'JetBrains Mono', monospace;
  font-weight: 500;
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--n2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Two lines, then clipped. Clip titles are written in a hurry and some of
   them are very long. */
.clip-title {
  font-family: 'Outfit', sans-serif;
  font-weight: 400;
  font-size: 0.88rem;
  line-height: 1.35;
  color: var(--n1);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.clip-meta {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.65rem;
  color: var(--n5);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* === Press kit === */
.press-section {
  margin-top: 3rem;
}

/* The paragraph someone pastes into their own copy. Set as a quotation
   because that is what it is — words to be reproduced, not read. */
.press-boilerplate {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 1rem;
  line-height: 1.7;
  color: var(--n2);
  background: rgba(26, 26, 26, 0.6);
  border-left: 3px solid var(--g);
  border-radius: 0 4px 4px 0;
  padding: 1.25rem 1.4rem;
  margin-top: 1.25rem;
  max-width: 68ch;
}

.press-assets {
  list-style: none;
  margin-top: 1.25rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 0.9rem;
}

.press-asset {
  border: 1px solid var(--n6);
  border-radius: 4px;
  background: rgba(26, 26, 26, 0.5);
  padding: 1rem 1.1rem;
}

.press-asset-link,
.press-asset-link:link,
.press-asset-link:visited {
  font-family: 'Syne', sans-serif;
  font-weight: 600;
  font-size: 1rem;
  color: var(--n1);
  text-decoration: none;
  border-bottom: 1px solid rgba(76, 175, 80, 0.4);
}

.press-asset-link:hover {
  color: var(--g);
  border-bottom-color: var(--g);
}

.press-asset-detail {
  display: block;
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.85rem;
  color: var(--n4);
  margin-top: 0.45rem;
}

/* Do and Don't side by side. Green and ember carry it, but each column says
   which it is in words above the list — the rule the whole site follows. */
.press-rules {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1.25rem;
  margin-top: 1.25rem;
}

.press-rule-set {
  border: 1px solid var(--n6);
  border-left-width: 3px;
  border-radius: 4px;
  padding: 1rem 1.2rem;
}

.press-rule-set.is-do {
  border-left-color: var(--g);
}

.press-rule-set.is-dont {
  border-left-color: var(--em);
}

.press-rule-heading {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--n3);
}

.press-rule-list {
  list-style: none;
  margin-top: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.press-rule-list li {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--n3);
  padding-left: 1.3rem;
  position: relative;
}

/* The tick and cross are decoration; aria-hidden isn't needed because ::before
   content isn't in the accessibility tree in any browser that matters, and the
   column heading already says which list this is. */
.press-do::before {
  content: "\2713";
  position: absolute;
  left: 0;
  color: var(--gl);
}

.press-dont::before {
  content: "\2717";
  position: absolute;
  left: 0;
  color: var(--em);
}

.press-spacing {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--n4);
  margin-top: 1rem;
  max-width: 66ch;
}

/* === Press kit: palette === */
.press-palette {
  display: grid;
  gap: 1.75rem;
  margin-top: 1.5rem;
}

.press-palette-name {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--n5);
}

.press-swatches {
  list-style: none;
  margin-top: 0.75rem;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 0.75rem;
}

.press-swatch {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

/* A border rather than nothing: #0D0D0D on a #0D0D0D page is an invisible
   swatch, and Black is a brand colour that has to be seen to be picked. */
.press-swatch-chip {
  display: block;
  height: 54px;
  border-radius: 4px;
  border: 1px solid var(--n6);
}

.press-swatch-name {
  font-family: 'Outfit', sans-serif;
  font-weight: 400;
  font-size: 0.85rem;
  color: var(--n2);
}

.press-swatch-hex {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.75rem;
  color: var(--n5);
  user-select: all;
}

/* === Press kit: typefaces === */
.press-faces {
  margin-top: 1.25rem;
}

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

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

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

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

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

.press-face-detail {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.88rem;
  color: var(--n3);
  margin-top: 0.25rem;
}

.press-socials {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  margin-top: 1rem;
}

.press-socials a,
.press-socials a:link,
.press-socials a:visited {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.78rem;
  color: var(--gl);
  text-decoration: none;
  border-bottom: 1px solid rgba(76, 175, 80, 0.3);
}

.press-socials a:hover {
  color: var(--g);
}

@media (max-width: 560px) {
  .press-face {
    grid-template-columns: 1fr;
  }

  .press-face-role {
    padding-top: 0;
  }
}

/* === Press kit: asset previews === */
/* Show what is about to be downloaded. The backdrop is per-asset and load
   bearing: a transparent mark shown only on this page's near-black would look
   like a file with a black background baked in, which is the one thing a
   downloader most needs to know it hasn't got. */
/* A plain block, NOT a grid — and that is the fix rather than the style.
   This was `display: grid; place-items: center` with the image clamped by
   max-width: 100%, and the image overflowed enormously: a grid with no
   explicit template gets one `auto` track, an auto track is sized from its
   item's max-content contribution — 2300px here — and it does not shrink
   below it. The percentage max-width never bit, because a percentage
   resolving against the track being sized counts as auto while that
   contribution is measured. The track became 2300px and overflow:hidden
   turned a preview into a crop.

   object-fit does the centring and the letterboxing on its own, so the
   centring machinery was only ever in the way. */
.press-asset-preview {
  display: block;
  height: 120px;
  border-radius: 3px;
  border: 1px solid var(--n6);
  margin-bottom: 0.9rem;
  padding: 0.6rem;
  overflow: hidden;
}

/* Definite width AND height, so object-fit has a box to work inside. contain,
   never cover: a cropped logo preview is a preview of the wrong thing. */
.press-asset-preview img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Neutral 100 — the brand's own white, straight off the palette above.
   Everything in this kit is dark artwork on transparency, so a dark ground
   showed an outline of nothing. The split ground that was here before was
   worse: it proved the transparency on one half and hid the logo on the
   other. */
.press-asset-preview.is-light {
  background: var(--n1);
}

/* For images that carry their own background and want no frame inside the
   frame. Still the brand white, because a 1.9:1 card letterboxes inside a
   shorter box and the ground shows either side of it. */
.press-asset-preview.is-none {
  background: var(--n1);
  padding: 0;
}

.press-asset-meta {
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  color: var(--n5);
  margin-top: 0.4rem;
}

/* === Contact === */
/* What's welcome and what isn't, above the form rather than below it. Saying
   no in advance stops the enquiries nobody wants before they're written. */
.contact-expectations {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1.25rem;
  margin-top: 2rem;
}

.contact-list {
  border: 1px solid var(--n6);
  border-left-width: 3px;
  border-radius: 4px;
  padding: 1rem 1.2rem;
}

.contact-list.is-open {
  border-left-color: var(--g);
}

.contact-list.is-closed {
  border-left-color: var(--em);
}

.contact-list-heading {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  color: var(--n3);
}

.contact-items {
  list-style: none;
  margin-top: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.contact-items li {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--n3);
  padding-left: 1.3rem;
  position: relative;
}

.contact-yes::before {
  content: "\2713";
  position: absolute;
  left: 0;
  color: var(--gl);
}

.contact-no::before {
  content: "\2717";
  position: absolute;
  left: 0;
  color: var(--em);
}

/* === Contact: the form === */
.contact-section {
  margin-top: 3rem;
}

.contact-form {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  margin-top: 1.5rem;
  max-width: 44rem;
}

.contact-field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.contact-field label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--n4);
}

.contact-field input,
.contact-field select,
.contact-field textarea {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.95rem;
  color: var(--n1);
  background: var(--dk);
  border: 1px solid var(--n6);
  border-radius: 4px;
  padding: 0.7rem 0.85rem;
  /* Without this, a textarea can be dragged wider than its column and the
     page scrolls sideways. */
  max-width: 100%;
  box-sizing: border-box;
  transition: border-color 0.2s;
}

.contact-field textarea {
  resize: vertical;
  min-height: 9rem;
  line-height: 1.6;
}

.contact-field input:focus,
.contact-field select:focus,
.contact-field textarea:focus {
  outline: none;
  border-color: var(--g);
}

/* The honeypot: out of sight, out of the tab order, out of the accessibility
   tree. Not display:none — some bots skip hidden fields, and a field that is
   merely somewhere else is more likely to be filled. */
.contact-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.contact-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
}

.contact-submit {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--bk);
  background: var(--g);
  border: none;
  border-radius: 4px;
  padding: 0.75rem 1.6rem;
  cursor: pointer;
  transition: background 0.2s;
}

.contact-submit:hover {
  background: var(--gl);
}

.contact-submit:disabled {
  background: var(--n6);
  color: var(--n4);
  cursor: default;
}

.contact-status {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.9rem;
  color: var(--n3);
}

.contact-status.is-good {
  color: var(--gl);
}

.contact-status.is-bad {
  color: var(--am);
}

.contact-fallback {
  font-family: 'Outfit', sans-serif;
  font-weight: 300;
  font-size: 0.9rem;
  color: var(--n4);
  border-left: 2px solid var(--n6);
  padding-left: 1rem;
  margin-top: 2rem;
  max-width: 66ch;
}

.press-contact-link {
  margin-top: 0.9rem;
}

.press-contact-link a,
.press-contact-link a:link,
.press-contact-link a:visited {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--gl);
  text-decoration: none;
  border-bottom: 1px solid rgba(76, 175, 80, 0.4);
}

.press-contact-link a:hover {
  color: var(--g);
}
