/* ==========================================================================
   Design tokens
   ========================================================================== */

:root {
  color-scheme: light dark;

  /* Brand */
  --brand-h: 212;
  --accent: hsl(var(--brand-h) 88% 46%);
  --accent-hover: hsl(var(--brand-h) 88% 39%);
  --accent-soft: hsl(var(--brand-h) 88% 46% / 0.1);
  --accent-contrast: #ffffff;

  /* Surfaces & text (light) */
  --bg: #ffffff;
  --bg-subtle: #f6f8fa;
  --surface: #ffffff;
  --border: #d9dee5;
  --border-strong: #c2c9d3;
  --text: #11181c;
  --text-muted: #5b6672;

  /* Type */
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas,
    "Liberation Mono", monospace;

  /* Fluid type scale */
  --step--1: clamp(0.83rem, 0.8rem + 0.15vw, 0.9rem);
  --step-0: clamp(1rem, 0.96rem + 0.2vw, 1.09rem);
  --step-1: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
  --step-2: clamp(1.6rem, 1.4rem + 1vw, 2.15rem);
  --step-3: clamp(2.1rem, 1.7rem + 2vw, 3.2rem);

  /* Layout */
  --measure: 68ch;
  --container: 1120px;
  --gutter: 1.25rem;
  --radius: 10px;
  --radius-lg: 16px;
  --header-h: 4rem;

  --shadow-sm: 0 1px 2px rgb(16 24 40 / 0.06), 0 1px 3px rgb(16 24 40 / 0.08);
  --shadow-md: 0 4px 6px -2px rgb(16 24 40 / 0.05),
    0 12px 16px -4px rgb(16 24 40 / 0.1);
}

/* Dark palette, shared by the OS preference and the explicit toggle. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0d1117;
    --bg-subtle: #151b23;
    --surface: #161b22;
    --border: #2a313c;
    --border-strong: #3d444d;
    --text: #e6edf3;
    --text-muted: #9aa7b2;
    --accent: hsl(var(--brand-h) 90% 62%);
    --accent-hover: hsl(var(--brand-h) 90% 70%);
    --accent-soft: hsl(var(--brand-h) 90% 62% / 0.14);
    --accent-contrast: #07121f;
    --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
    --shadow-md: 0 8px 24px rgb(0 0 0 / 0.5);
  }
}

:root[data-theme="dark"] {
  --bg: #0d1117;
  --bg-subtle: #151b23;
  --surface: #161b22;
  --border: #2a313c;
  --border-strong: #3d444d;
  --text: #e6edf3;
  --text-muted: #9aa7b2;
  --accent: hsl(var(--brand-h) 90% 62%);
  --accent-hover: hsl(var(--brand-h) 90% 70%);
  --accent-soft: hsl(var(--brand-h) 90% 62% / 0.14);
  --accent-contrast: #07121f;
  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
  --shadow-md: 0 8px 24px rgb(0 0 0 / 0.5);
}

/* ==========================================================================
   Reset
   ========================================================================== */

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

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 1rem);
}

body {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.6;
  color: var(--text);
  background-color: var(--bg);
  -webkit-font-smoothing: antialiased;
}

img,
picture,
svg,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

h1,
h2,
h3 {
  line-height: 1.2;
  text-wrap: balance;
  font-weight: 650;
  letter-spacing: -0.02em;
}

p,
li {
  text-wrap: pretty;
}

a {
  color: var(--accent);
  text-underline-offset: 0.2em;
}

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

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

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

/* ==========================================================================
   Layout helpers
   ========================================================================== */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  padding-block: clamp(3rem, 8vw, 5.5rem);
}

.section--subtle {
  background-color: var(--bg-subtle);
  border-block: 1px solid var(--border);
}

.stack > * + * {
  margin-top: 1rem;
}

.prose {
  max-width: var(--measure);
}

.skip-link {
  position: absolute;
  left: 0.5rem;
  top: -4rem;
  z-index: 100;
  padding: 0.6rem 1rem;
  border-radius: var(--radius);
  background-color: var(--accent);
  color: var(--accent-contrast);
  font-weight: 600;
  text-decoration: none;
  transition: top 0.15s ease;
}

.skip-link:focus {
  top: 0.5rem;
  color: var(--accent-contrast);
}

.eyebrow {
  display: inline-block;
  padding: 0.3rem 0.7rem;
  border-radius: 999px;
  background-color: var(--accent-soft);
  color: var(--accent);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* ==========================================================================
   Header / navigation
   ========================================================================== */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background-color: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}

.site-header__inner {
  height: var(--header-h);
  display: flex;
  align-items: center;
  gap: 1rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  margin-inline-end: auto;
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: -0.02em;
  color: var(--text);
  text-decoration: none;
}

.brand__mark {
  width: 1.75rem;
  height: 1.75rem;
  flex: none;
}

.nav__list {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  list-style: none;
  padding: 0;
}

.nav__link {
  display: block;
  padding: 0.45rem 0.75rem;
  border-radius: var(--radius);
  color: var(--text-muted);
  font-weight: 550;
  font-size: var(--step--1);
  text-decoration: none;
  transition: background-color 0.15s ease, color 0.15s ease;
}

.nav__link:hover {
  color: var(--text);
  background-color: var(--bg-subtle);
}

.nav__link[aria-current="page"] {
  color: var(--accent);
  background-color: var(--accent-soft);
}

.header__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.icon-button {
  display: grid;
  place-items: center;
  width: 2.35rem;
  height: 2.35rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background-color: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease,
    border-color 0.15s ease;
}

.icon-button:hover {
  color: var(--text);
  border-color: var(--border-strong);
  background-color: var(--bg-subtle);
}

.icon-button svg {
  width: 1.15rem;
  height: 1.15rem;
}

/* The toggle shows one icon, picked by the resolved theme. */
.theme-toggle .icon-moon {
  display: none;
}

:root[data-theme="dark"] .theme-toggle .icon-sun {
  display: none;
}

:root[data-theme="dark"] .theme-toggle .icon-moon {
  display: block;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-sun {
    display: none;
  }

  :root:not([data-theme="light"]) .theme-toggle .icon-moon {
    display: block;
  }
}

.nav-toggle {
  display: none;
}

/* Mobile navigation */
@media (max-width: 47.99em) {
  .nav-toggle {
    display: grid;
  }

  .nav {
    position: fixed;
    inset: var(--header-h) 0 auto 0;
    display: none;
    padding: 0.5rem var(--gutter) 1rem;
    background-color: var(--bg);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-md);
  }

  .nav[data-open="true"] {
    display: block;
  }

  .nav__list {
    flex-direction: column;
    align-items: stretch;
    gap: 0.15rem;
  }

  .nav__link {
    padding: 0.7rem 0.75rem;
    font-size: var(--step-0);
  }
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.65rem 1.25rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-weight: 600;
  font-size: var(--step-0);
  line-height: 1.3;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease,
    color 0.15s ease, transform 0.05s ease;
}

.button:active {
  transform: translateY(1px);
}

.button--primary {
  background-color: var(--accent);
  color: var(--accent-contrast);
}

.button--primary:hover {
  background-color: var(--accent-hover);
  color: var(--accent-contrast);
}

.button--secondary {
  background-color: var(--surface);
  border-color: var(--border);
  color: var(--text);
}

.button--secondary:hover {
  border-color: var(--border-strong);
  background-color: var(--bg-subtle);
  color: var(--text);
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* ==========================================================================
   Hero
   ========================================================================== */

.hero {
  padding-block: clamp(3.5rem, 10vw, 7rem);
  background: radial-gradient(
    60rem 30rem at 50% -8rem,
    var(--accent-soft),
    transparent 70%
  );
}

.hero__inner {
  max-width: 46rem;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.25rem;
  text-align: center;
}

.hero h1 {
  font-size: var(--step-3);
}

.hero__lede {
  max-width: 38rem;
  font-size: var(--step-1);
  color: var(--text-muted);
}

/* ==========================================================================
   Sections & cards
   ========================================================================== */

.section__head {
  max-width: var(--measure);
  margin-bottom: 2.5rem;
}

.section__head h2 {
  margin-top: 0.5rem;
  font-size: var(--step-2);
}

.section__head p {
  margin-top: 0.75rem;
  color: var(--text-muted);
}

.grid {
  display: grid;
  gap: 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}

.card {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 1.5rem;
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  transition: border-color 0.15s ease, box-shadow 0.15s ease,
    transform 0.15s ease;
}

.card:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

.card__icon {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius);
  background-color: var(--accent-soft);
  color: var(--accent);
}

.card__icon svg {
  width: 1.3rem;
  height: 1.3rem;
}

.card h3 {
  font-size: var(--step-1);
}

.card p {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--step--1);
}

/* ==========================================================================
   Footer
   ========================================================================== */

.site-footer {
  margin-top: auto;
  padding-block: 2rem;
  border-top: 1px solid var(--border);
  background-color: var(--bg-subtle);
  color: var(--text-muted);
  font-size: var(--step--1);
}

.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.site-footer a {
  color: var(--text-muted);
  text-decoration: none;
}

.site-footer a:hover {
  color: var(--accent);
  text-decoration: underline;
}

.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  list-style: none;
  padding: 0;
}

/* ==========================================================================
   Error page
   ========================================================================== */

.error-page {
  flex: 1;
  display: grid;
  place-items: center;
  padding-block: clamp(3rem, 12vw, 7rem);
  text-align: center;
}

.error-page__code {
  font-family: var(--font-mono);
  font-size: clamp(4rem, 18vw, 8rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--accent);
}

/* ==========================================================================
   Landing scene (the 3D stage + the panels that flank it)
   ========================================================================== */

:root {
  --stage-bg: #f2f2f2;
  --stage-line: #b7ebdc;

  --panel-fill: #322b8f;
  --panel-fill-hover: #221c66;
  /* The marks are always white; it is the box behind them that takes the
     brand colour, which is what keeps every logo legible. */
  --panel-logo: #ffffff;
  --brand-discord: #5865f2;
  --brand-youtube: #ff0000;
  /* Instagram's brand is a gradient rather than a flat colour -- and a flat
     #e4405f would sit directly under YouTube's red in the same column. */
  --brand-instagram: radial-gradient(
    circle at 30% 107%,
    #fdf497 0%,
    #fdf497 5%,
    #fd5949 45%,
    #d6249f 60%,
    #285aeb 90%
  );
  /* Lifted off GitHub's own #181717 so it stays distinct from the outline
     that now runs round every box. */
  --brand-github: #3a3a3a;

  /* One outline for every box on the page -- the four landing panels and the
     project cards alike -- whatever each one is filled with. It does not
     follow the theme: the boxes it frames are brand colours, which do not
     either. */
  --panel-outline: #181717;
}

/* No overflow clipping on the body: clipping one axis there forces the other
   to clip too, which would stop the page scrolling entirely. */
.page--stage {
  background-color: var(--stage-bg);
}

/* Taller than the viewport, which is what gives the pinned scene its scroll. */
.scene {
  position: relative;
  height: 160svh;
}

.scene__sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden; /* hides the panels while they are parked off-screen */
}

/* Fixed, so one backdrop serves every section and can crossfade between
   them. Everything else stacks above it. */
.backdrop {
  position: fixed;
  inset: 0;
  z-index: 0;
  background-color: var(--stage-bg);
}

.backdrop__canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.9s ease 0.1s;
}

#main {
  position: relative;
  z-index: 1;
}

[data-stage][data-state="ready"] .backdrop__canvas {
  opacity: 1;
}

.stage__status {
  position: absolute;
  inset: auto 0 clamp(2rem, 8vh, 4rem);
  margin: 0;
  text-align: center;
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgb(17 24 28 / 0.42);
  transition: opacity 0.5s ease;
}

[data-stage][data-state="ready"] .stage__status {
  opacity: 0;
  visibility: hidden;
}

[data-stage][data-state="error"] .stage__status {
  color: #b23b3b;
  text-transform: none;
  letter-spacing: 0;
}

/* --------------------------------------------------------------- panels -- */

.panels {
  position: absolute;
  inset: 0;
  display: grid;
  /* Two flanking columns with the character held in the middle. */
  grid-template-columns: 1fr minmax(210px, 30%) 1fr;
  align-content: center;
  padding-inline: clamp(1rem, 4vw, 4.5rem);
  /* Let the pointer through to the canvas; the panels opt back in. */
  pointer-events: none;
}

.panels__col {
  display: grid;
  gap: clamp(0.75rem, 2vh, 1.75rem);
  justify-items: center;
  pointer-events: auto;
}

/* main.js sets this once the exit fade has taken the panels below visibility.
   They are still laid out, and the sticky can still be covering part of the
   viewport at that point, so without this three invisible links would stay
   clickable. */
.scene--gone .panels__col {
  pointer-events: none;
}

/* --p is written by site.js from the scroll position: 0 parked off-side,
   1 fully arrived. Everything here is a pure function of it, which is what
   makes the reveal scrubbable rather than a one-shot transition.

   Deliberately no transition: site.js already eases --p towards the scroll
   position frame by frame, and a transition on top of that would smooth an
   already-smoothed value -- and, being fixed-duration, would end in the abrupt
   stop this glide exists to avoid. */
.panel {
  --p: 0;
  --travel: 0px;

  width: min(100%, 30svh);
  aspect-ratio: 1;

  /* --scene-exit is published by main.js from the viewer's eased transition,
     so the panels dissolve on exactly the same curve as the character. */
  opacity: calc(var(--p) * (1 - var(--scene-exit, 0)));
  transform: translateX(calc((1 - var(--p)) * var(--travel)));
  will-change: transform, opacity;
}

/* A panel arriving from the left is travelling in +x, so it should overshoot
   into +x; one from the right mirrors it. */
.panels__col--left .panel {
  --travel: min(-60vw, -420px);
  --settle-dir: 1;
}

.panels__col--right .panel {
  --travel: max(60vw, 420px);
  --settle-dir: -1;
}

/* The settle is a one-shot animation rather than part of the scrub, so it runs
   at its own pace however fast the page was scrolled. Nested inside .panel so
   it composes with the scrub transform instead of overwriting it. */
.panel__settle {
  width: 100%;
  height: 100%;
}

.panel__settle.is-settling {
  animation: panel-settle 560ms cubic-bezier(0.33, 0.9, 0.35, 1) both;
}

@keyframes panel-settle {
  0% {
    transform: translateX(0) rotate(0deg);
  }
  20% {
    transform: translateX(calc(var(--settle-dir, 1) * 4%)) rotate(calc(var(--settle-dir, 1) * 0.7deg));
  }
  45% {
    transform: translateX(calc(var(--settle-dir, 1) * -1.9%)) rotate(calc(var(--settle-dir, 1) * -0.32deg));
  }
  68% {
    transform: translateX(calc(var(--settle-dir, 1) * 0.85%)) rotate(calc(var(--settle-dir, 1) * 0.14deg));
  }
  86% {
    transform: translateX(calc(var(--settle-dir, 1) * -0.3%)) rotate(0deg);
  }
  100% {
    transform: translateX(0) rotate(0deg);
  }
}

/* All four are anchors now, so the shared box rules have to hold up as links. */
.panel__inner {
  position: relative; /* for the lift shadow */
  display: grid;
  place-items: center;
  text-decoration: none;
  width: 100%;
  height: 100%;
  color: var(--panel-logo);
  border: 4px solid var(--panel-outline);
  border-radius: var(--card-radius);
  background-color: var(--panel-fill);
  /* The two 1px rings are not decoration. Scaling a rounded, bordered box
     magnifies the antialiasing on its corner arcs, and the border, the fill
     and (on a card) the clipped image are each rasterised separately -- so a
     hairline of whatever is behind can show between them at the four corners.
     The rings sit just outside and just inside the border, in the border's own
     colour, so anything that slips through lands on more border instead of on
     the page. Identical in both states, so they never animate. */
  box-shadow:
    0 0 0 1px var(--panel-outline),
    inset 0 0 0 1px var(--panel-outline),
    0 16px 38px -20px rgb(50 43 143 / 0.7);
  /* box-shadow is deliberately not animated. Three shadow layers, one of them
     a 38px blur, re-rasterising every frame while the box scales is what makes
     the edges crawl -- and transform on its own is a compositor operation the
     paint never has to follow. The lift reads from the scale alone. */
  transition:
    transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
    background-color 0.35s ease,
    border-color var(--theme-fade);
}

/* Sized off the box rather than fixed, so it holds its proportion of the
   panel at every viewport, and it rides the panel's hover scale for free by
   being a child of the element that scales. */
/* The base stylesheet colours links, and `a:hover` outranks a plain class --
   so the mark, which is drawn in currentColor, would turn blue on hover.
   Pinning every link state keeps it the colour the panel chose. */
.panel__inner,
.panel__inner:hover,
.panel__inner:focus,
.panel__inner:active,
.panel__inner:visited {
  color: var(--panel-logo);
}

.panel__inner:focus-visible {
  outline: 3px solid var(--loader-outline);
  outline-offset: 4px;
}

.panel__logo {
  width: 42%;
  height: auto;
  fill: currentColor;
  transition: color var(--theme-fade);
}

/* A branded panel only names its colour; the hover shade is derived from it,
   so each one darkens on hover the way the default panel does. The border is
   not derived -- every box shares --panel-outline. */
.panel__inner--discord {
  --brand: var(--brand-discord);
}

.panel__inner--youtube {
  --brand: var(--brand-youtube);
}

.panel__inner--github {
  --brand: var(--brand-github);
}

/* A gradient cannot go through --panel-fill, which is a background-color, so
   this one paints a background-image over it. The flat colour underneath is
   the gradient's midpoint, which is what shows if the image ever fails. */
.panel__inner--instagram {
  --panel-fill: #d6249f;
  background-image: var(--brand-instagram);
}

/* The --panel-fill-hover swap is invisible under a gradient, so the darkening
   is a translucent layer stacked over it instead. */
.panel__inner--instagram:hover {
  background-image: linear-gradient(rgb(0 0 0 / 0.16), rgb(0 0 0 / 0.16)),
    var(--brand-instagram);
}

.panel__inner--discord,
.panel__inner--youtube,
.panel__inner--github {
  --panel-fill: var(--brand);
  --panel-fill-hover: color-mix(in srgb, var(--brand) 72%, #000000);
}

/* The depth on hover is a pseudo-element that only fades. box-shadow cannot be
   animated without repainting the whole blur every frame; opacity is a
   compositor property, so this costs nothing and the edges stay still. It sits
   inside the panel, so it scales with it. */
.panel__inner::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: var(--card-lift-shadow);
  opacity: 0;
  transition: opacity 0.35s ease;
  pointer-events: none;
}

.panel__inner:hover::after {
  opacity: 1;
}

.panel__inner:hover {
  transform: scale(1.05);
  background-color: var(--panel-fill-hover);
}

/* On a narrow screen there is no room either side of the character, so the
   panels gather into a 2x2 block low in the frame instead. */
@media (max-width: 47.99em) {
  .panels {
    grid-template-columns: 1fr 1fr;
    align-content: end;
    padding-bottom: clamp(1.5rem, 6vh, 3rem);
    gap: clamp(0.6rem, 2vw, 1rem);
  }

  .panels__gap {
    display: none;
  }

  .panel {
    width: min(100%, 21svh);
  }
}

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

/* ==========================================================================
   About
   ========================================================================== */

/* Monocraft (SIL OFL 1.1, github.com/IdreesInc/Monocraft) -- a font drawn from
   the Minecraft typeface, self-hosted with its licence alongside it in
   /fonts. Mojangles itself is Mojang's and cannot be redistributed, so it is
   named first in the stack and used if the visitor happens to have it. */
@font-face {
  font-family: "Monocraft";
  src: url("/fonts/monocraft-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Monocraft";
  src: url("/fonts/monocraft-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --font-pixel: "Mojangles", "Minecraft", "Monocraft", "Courier New", monospace;
  --about-text: #e4e6e6;
  --about-shadow: #4a5a58;
  --about-link: #ffffff;
  --about-link-hover: #5fe3d8;
}

/* Every section is sized to its content and splits --section-gap with its
   neighbour, so each pair sits exactly that far apart whatever the order.
   None of them reserves a full viewport: a section that holds a heading and a
   paragraph would leave half a screen of nothing under it. */
.about {
  display: grid;
  justify-items: center;
  align-content: start;
  padding: calc(var(--section-gap) / 2) var(--gutter);
}

/* The last one does not end the page -- the ribbons and the sponsor call come
   after it -- so it leaves a set gap down to them rather than half a screen. */
main > .about:last-child {
  padding-bottom: var(--footer-gap);
}

/* The other half of that pair. Projects' own top padding is the generous kind
   it needs when it follows the landing scene; with a section above it, it
   takes half the gap instead. */
.about + .projects {
  padding-top: calc(var(--section-gap) / 2);
}

.about__inner {
  width: 100%;
  max-width: 46rem;
  text-align: center;
}

.about__title {
  display: inline-block;
  font-family: var(--font-pixel);
  font-variant-ligatures: none; /* Monocraft ships coding ligatures */
  font-weight: 700;
  font-size: clamp(2rem, 6.5vw, 3.6rem);
  letter-spacing: 0.02em;
  color: var(--heading-text);
  /* Minecraft draws its text with a hard offset shadow, which also gives the
     letterforms definition against the backdrop. */
  text-shadow: 0.09em 0.09em 0 var(--heading-shadow);
  transition: color var(--theme-fade), text-shadow var(--theme-fade);
}

.about__body {
  max-width: 34rem;
  margin: clamp(1.75rem, 5vh, 2.75rem) auto 0;
  padding: clamp(1.1rem, 3vw, 1.75rem) clamp(1.25rem, 3.5vw, 2rem);
  border-radius: 14px;
  background-color: rgb(255 255 255 / 0.16);
  border: 1px solid rgb(255 255 255 / 0.4);
  backdrop-filter: blur(2px);
  transition:
    background-color var(--theme-fade),
    border-color var(--theme-fade);
}

.about__body p {
  margin: 0;
  font-family: var(--font-pixel);
  font-variant-ligatures: none;
  font-size: clamp(0.8rem, 1.6vw, 0.95rem);
  line-height: 1.9;
  color: var(--about-text);
  text-shadow: 0.07em 0.07em 0 var(--about-shadow);
  transition: color var(--theme-fade), text-shadow var(--theme-fade);
}

.about__body p + p {
  margin-top: 1.2em;
}

/* A link in the body copy. The surrounding text is already near-white, so the
   underline does the signalling rather than a brightness step, offset and
   thinned because a default one runs through the descenders of a pixel face.
   Every link state is pinned: the base stylesheet's `a:hover` outranks a plain
   class and would turn these blue. */
.about__link,
.about__link:hover,
.about__link:focus,
.about__link:active,
.about__link:visited {
  color: var(--about-link);
}

.about__link {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.28em;
  transition: color var(--theme-fade);
}

.about__link:hover,
.about__link:focus-visible {
  color: var(--about-link-hover);
}

.about__link:focus-visible {
  outline: 2px solid var(--about-link-hover);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ==========================================================================
   Projects
   ========================================================================== */

/* Shared by both section headings, in either theme. The shadow is the same
   colour quartered, which is how Minecraft draws its own text shadow.
   The card titles take a pastel version of the same hue so they sit a step
   below the section heading; the dark shadow is what keeps them legible
   against the pale mint backdrop of the light theme. */
:root {
  --card-radius: clamp(14px, 2vw, 26px);
  --card-lift-shadow: 0 22px 46px -18px rgb(34 28 102 / 0.75);

  --heading-text: #5d8c6e;
  --heading-shadow: #17231b;
  --project-title: #a9d7b6;
}

/* Dark mode takes the pale mint instead of the green. It sits close to the
   light theme's own background, which is what makes the two themes read as one
   idea seen from either side, and it clears both dark backdrops comfortably:
   7.8:1 on the landing slate, 6.2:1 on the purple. */
:root[data-theme="dark"] {
  --heading-text: #c9f2e4;
  --heading-shadow: #32453f;
}

/* Sized to its content rather than a full viewport: holding 100svh here left
   half a screen of nothing between the cards and the About heading. The two
   sections then split --section-gap between them, so the distance is set in
   one place rather than having to add two paddings up. */
:root {
  --section-gap: 120px;
  /* From the last section's copy down to the first ribbon. */
  --footer-gap: 160px;
}

.projects {
  display: grid;
  justify-items: center;
  align-content: center;
  padding: clamp(4rem, 11vh, 7rem) var(--gutter) calc(var(--section-gap) / 2);
}

.projects__inner {
  width: 100%;
  max-width: var(--container);
}

.projects__title {
  margin: 0 0 clamp(1.75rem, 5vh, 3rem);
  text-align: center;
  font-family: var(--font-pixel);
  font-variant-ligatures: none;
  font-weight: 700;
  font-size: clamp(1.9rem, 6vw, 3.4rem);
  letter-spacing: 0.02em;
  color: var(--heading-text);
  text-shadow: 0.07em 0.07em 0 var(--heading-shadow);
  transition: color var(--theme-fade), text-shadow var(--theme-fade);
}

.projects__grid {
  display: grid;
  gap: clamp(1rem, 2.5vw, 2rem);
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* The card is just a stack now: a title that sits outside the box, and the box
   itself, which is the link. */
.project {
  position: relative; /* for the lift shadow */
  display: grid;
  gap: 0.7rem;
}

/* The box clips its children, which would eat a shadow drawn inside it -- so
   this one sits on the card and behind the box, matched to its geometry: the
   box is the last row and square, hence `inset: auto 0 0 0` plus a 1:1 ratio.
   It carries the same scale and easing as the box, so the two move as one. */
.project::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  aspect-ratio: 1;
  z-index: -1;
  border-radius: var(--card-radius);
  box-shadow: var(--card-lift-shadow);
  opacity: 0;
  pointer-events: none;
  transition:
    opacity 0.35s ease,
    transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.project:has(.project__box:hover)::after {
  opacity: 1;
  transform: scale(1.03);
}

.project__title {
  margin: 0;
  text-align: center;
  font-family: var(--font-pixel);
  font-variant-ligatures: none;
  font-weight: 700;
  font-size: clamp(0.95rem, 2vw, 1.15rem);
  letter-spacing: 0.02em;
  color: var(--project-title);
  text-shadow: 0.07em 0.07em 0 var(--heading-shadow);
  transition: color var(--theme-fade), text-shadow var(--theme-fade);
}

/* The clickable surface. Everything the card does on hover happens here, so
   the affordance is on the thing that actually navigates. */
/* Square, so a square logo fills it exactly with nothing cropped. Padding
   lives on the hint rather than here, so an image can reach the edges. */
.project__box {
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  overflow: hidden;
  border: 4px solid var(--panel-outline);
  border-radius: var(--card-radius);
  background-color: var(--panel-fill);
  box-shadow:
    0 0 0 1px var(--panel-outline),
    inset 0 0 0 1px var(--panel-outline),
    0 16px 38px -20px rgb(50 43 143 / 0.7);
  text-align: center;
  text-decoration: none;
  /* box-shadow is deliberately not animated. Three shadow layers, one of them
     a 38px blur, re-rasterising every frame while the box scales is what makes
     the edges crawl -- and transform on its own is a compositor operation the
     paint never has to follow. The lift reads from the scale alone. */
  transition:
    transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
    background-color 0.35s ease,
    border-color var(--theme-fade);
}

/* Every card lifts, link or not, so the row reacts as one set. A card with no
   href still keeps the default cursor rather than a pointer, so the lift reads
   as the card waking up rather than as a promise to navigate. */
.project__box:hover {
  transform: scale(1.03);
  background-color: var(--panel-fill-hover);
}

/* The artwork carries its own background, so the box gets out of the way and
   the outline does the framing.

   The card's own colour should never be visible -- but a box that clips its
   child to a rounded corner and then scales on hover leaves a hairline of
   antialiasing at each corner, and whatever sits under the artwork shows
   through it. So --card-bg is set per card to that artwork's own edge colour,
   sampled from the image, and held through the hover rather than swapped for
   --panel-fill-hover. Black is the default because most of the logos are on
   black. It is also what shows before an image has loaded. */
.project__box--image {
  background-color: var(--card-bg, #000000);
}

.project__box--image:hover,
.project__box--mark:hover {
  background-color: var(--card-bg, #000000);
}

.project__box--image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(var(--logo-bleed, 1));
}

/* For a logo that carries its own rounded corners, the corners would otherwise
   show the box through them. Pushing it out slightly puts them past the edge,
   where overflow clips them -- the artwork keeps its proportions, it just
   bleeds. 1.16 is the least that clears a 29/128 corner radius. */
.project__box--bleed {
  --logo-bleed: 1.16;
}

/* A bare mark rather than full-bleed artwork: held inside the box with room
   around it instead of reaching the edges, and flipped to white -- the source
   is black on transparency, which would be invisible on a black card. The
   inset also keeps a small source from being blown up as far. */
.project__box--mark {
  padding: 25%;
  background-color: var(--card-bg, #000000);
}

.project__box--mark img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: invert(1);
}

.project__box:focus-visible {
  outline: 3px solid var(--loader-outline);
  outline-offset: 3px;
}

/* A box can hold an embed instead; 16:9, held by the ratio. Use a <div> rather
   than the <a> for that -- an iframe inside a link cannot be clicked through. */
.project__media {
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: 8px;
}

.project__media iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

@media (max-width: 61.99em) {
  .projects__grid {
    grid-template-columns: minmax(0, 1fr);
    max-width: 30rem;
    margin-inline: auto;
  }
}

/* ==========================================================================
   Theme toggle + dark mode
   ========================================================================== */

/* The floating controls. One pill, two corners: this describes the look, and
   each control below only says which side it lives on. */
.theme-toggle--floating,
.home-button {
  position: fixed;
  top: clamp(0.85rem, 2.5vh, 1.5rem);
  z-index: 60;

  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  padding: 0;

  border: 2px solid var(--toggle-border);
  border-radius: 12px;
  background-color: var(--toggle-bg);
  cursor: pointer;
  text-decoration: none;
  transition:
    background-color 0.35s ease,
    border-color 0.35s ease,
    color 0.35s ease,
    transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}

.theme-toggle--floating {
  left: clamp(0.85rem, 2.5vw, 1.5rem);
}

.home-button {
  right: clamp(0.85rem, 2.5vw, 1.5rem);
}

/* The home button is a link, and the base stylesheet's `a:hover` outranks a
   plain class -- so without pinning every state the icon, drawn in
   currentColor, would go blue. */
.theme-toggle--floating,
.home-button,
.home-button:hover,
.home-button:focus,
.home-button:active,
.home-button:visited {
  color: var(--toggle-ink);
}

.theme-toggle--floating:hover,
.home-button:hover {
  transform: scale(1.06);
  background-color: var(--toggle-bg-hover);
}

.theme-toggle--floating:focus-visible,
.home-button:focus-visible {
  outline: 3px solid var(--toggle-ink);
  outline-offset: 3px;
}

.theme-toggle--floating svg,
.home-button svg {
  width: 1.3rem;
  height: 1.3rem;
}

/* The backdrop eases between themes in the shader; these match its pace so the
   text crosses over with it rather than snapping. A custom property cannot be
   transitioned itself, but the color / text-shadow that READ one can be. */
:root {
  --theme-fade: 0.5s ease-out;

  --toggle-bg: rgb(255 255 255 / 0.55);
  --toggle-bg-hover: rgb(255 255 255 / 0.85);
  --toggle-border: rgb(55 48 154 / 0.35);
  --toggle-ink: #37309a;
}

/* --------------------------------------------------------------- dark -- */

:root[data-theme="dark"] {
  --stage-bg: #3e4754;

  --toggle-bg: rgb(94 75 128 / 0.55);
  --toggle-bg-hover: rgb(94 75 128 / 0.85);
  --toggle-border: rgb(203 214 232 / 0.4);
  --toggle-ink: #e6edf7;

  --about-text: #e4e8ef;
  --about-shadow: #0d1a33;
  --about-link: #ffffff;
  --about-link-hover: #7ff0e6;

  --panel-fill: #2a2470;
  --panel-fill-hover: #1b1650;
}

:root[data-theme="dark"] .stage__status {
  color: rgb(230 237 247 / 0.55);
}

:root[data-theme="dark"] .about__body {
  background-color: rgb(9 20 40 / 0.28);
  border-color: rgb(210 222 240 / 0.28);
}

/* ==========================================================================
   Loading screen
   ========================================================================== */

/* The overlay is hidden by default and only revealed when site.js decides the
   load is dragging, so a quick load never shows it at all. It fades in when it
   does appear -- popping in over already-visible content reads as a fault. */
.loader {
  transition:
    opacity 0.25s ease,
    visibility 0s linear 0.25s;
}

html.loading .loader {
  transition: opacity 0.25s ease;
}

:root {
  --loader-outline: #5fe3d8;
  --loader-bg: #f2f2f2;
  --loader-icon: clamp(9rem, 30vw, 14rem);
  --loader-icon-radius: clamp(14px, 2vw, 22px);
  --loader-exit: 0.85s cubic-bezier(0.76, 0, 0.24, 1);
}

:root[data-theme="dark"] {
  --loader-bg: #3e4754;
}

/* The halves carry the background and the centre line and part on exit. They
   are transparent themselves -- ::before does the painting, so it can leave a
   hole. */
.loader__half {
  position: absolute;
  top: 0;
  height: 100%;
  width: 50%;
  overflow: hidden;
  transition: transform var(--loader-exit);
}

.loader__half--left {
  left: 0;
}

.loader__half--right {
  right: 0;
}

/* The background, with an icon-shaped hole at the inner edge. The element is
   the hole; its enormous shadow spread is what actually paints the half, and
   the parent's overflow clips that to the half's own area.
   
   The hole travels with its half, which is exactly right: at every point in
   the slide it covers precisely the part of the icon that half would otherwise
   have passed behind, so the icon always fades against the page rather than
   against moving background. */
.loader__half::before {
  content: "";
  position: absolute;
  top: 50%;
  width: var(--loader-icon);
  height: var(--loader-icon);
  border-radius: var(--loader-icon-radius);
  box-shadow: 0 0 0 100vmax var(--loader-bg);
  transform: translateY(-50%);
}

.loader__half--left::before {
  right: calc(var(--loader-icon) / -2);
}

.loader__half--right::before {
  left: calc(var(--loader-icon) / -2);
}

/* Half the centre line each, so it parts with them. The gradient leaves a gap
   exactly the height of the hole, so the line stops either side of the icon
   instead of crossing it. */
.loader__half::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  background-image: linear-gradient(
    to bottom,
    var(--loader-outline) 0 calc(50% - var(--loader-icon) / 2),
    transparent calc(50% - var(--loader-icon) / 2)
      calc(50% + var(--loader-icon) / 2),
    var(--loader-outline) calc(50% + var(--loader-icon) / 2) 100%
  );
  filter: drop-shadow(0 0 6px rgb(95 227 216 / 0.55));
}

.loader__half--left::after {
  right: 0;
}

.loader__half--right::after {
  left: 0;
}

/* Held dead centre above the halves: it never moves, it just dissolves while
   they pull away either side of it. */
.loader__avatar {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 1;
  width: var(--loader-icon);
  height: auto;
  border: 4px solid var(--loader-outline);
  border-radius: var(--loader-icon-radius);
  box-shadow: 0 0 34px -6px rgb(95 227 216 / 0.45);
  transform: translate(-50%, -50%);
  transition:
    opacity 0.5s ease,
    transform 0.7s cubic-bezier(0.4, 0, 0.6, 1);
}

/* Exit: the halves part, the icon stays put and fades. */
.loader[data-state="done"] {
  pointer-events: none;
}

.loader[data-state="done"] .loader__half--left {
  transform: translateX(-100%);
}

.loader[data-state="done"] .loader__half--right {
  transform: translateX(100%);
}

.loader[data-state="done"] .loader__avatar {
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.94);
}

@media (prefers-reduced-motion: reduce) {
  .loader__half {
    transition: opacity 0.3s ease;
  }

  .loader[data-state="done"] .loader__half--left,
  .loader[data-state="done"] .loader__half--right {
    transform: none;
    opacity: 0;
  }

  .loader[data-state="done"] .loader__avatar {
    transform: translate(-50%, -50%);
  }
}

/* ==========================================================================
   Sponsor ribbon
   ========================================================================== */

:root {
  --ribbon-bg: #ffffff;
  --ribbon-text: #5b6672;
  /* The 20-character cap, made structural rather than just documented. */
  --ribbon-slot: 20ch;
  --ribbon-gap: clamp(1.5rem, 4vw, 3rem);
  /* How far the band sits off the bottom edge of the page, and how far apart
     stacked bands sit from each other. */
  --ribbon-offset: 60px;
  --ribbon-stack-gap: 150px;
  /* The grey frame around the white band. This is a border, not a margin, so
     the grey reaches the screen edge rather than letting the backdrop through
     -- the ribbon reads as a plaque with a mount rather than a floating bar. */
  --ribbon-frame: #d9dee5;
  --ribbon-frame-width: 8px;

  /* The call to action between the bands, in the same green-and-mint family as
     the headings. The call is always the colour furthest from the backdrop
     behind it -- darkest on the light theme, lightest on the dark one -- so
     the clickable half leads the eye in both. The shadows are each colour
     taken down towards black, which is how Minecraft draws its own. */
  --sponsor-lead: #5d8c6e;
  --sponsor-lead-shadow: #17231b;
  --sponsor-call: #35604a;
  --sponsor-call-shadow: #0d1812;
}

:root[data-theme="dark"] {
  --sponsor-lead: #a3cdbd;
  --sponsor-lead-shadow: #28332f;
  --sponsor-call: #c9f2e4;
  --sponsor-call-shadow: #32453f;
}

/* Full bleed, with the grey carried by a thick border on all four sides. The
   viewport's own `overflow: hidden` sits inside it, so the track is clipped
   before it can reach the grey. */
.ribbon {
  position: relative; /* the backdrop is fixed at z-index 0 */
  z-index: 1;
  margin-bottom: var(--ribbon-offset);
  background-color: var(--ribbon-bg);
  border: var(--ribbon-frame-width) solid var(--ribbon-frame);
  transition:
    background-color var(--theme-fade),
    border-color var(--theme-fade);
}

/* The window the track slides behind. */
/* --ribbon-offset is the clearance to the bottom of the page, so it belongs to
   the last band only. A band with something below it hands that clearance over
   to whatever follows -- either the next band's own spacing or, as here, the
   call to action, which *is* the gap. */
.ribbon:has(+ .ribbon),
.ribbon:has(+ .sponsor-cta) {
  margin-bottom: 0;
}

/* The gap between the two bands, with the call to action centred inside it --
   so the distance between the bands is one number and the text sits in it
   rather than pushing them apart by an amount nothing states. */
.sponsor-cta {
  position: relative; /* the backdrop is fixed at z-index 0 */
  z-index: 1;
  display: grid;
  place-items: center;
  height: var(--ribbon-stack-gap);
  padding-inline: var(--gutter);
}

/* One line. It wraps only when a narrow screen leaves no choice -- the gap is
   150px tall, so a second line still sits comfortably rather than overflowing
   into the bands. */
.sponsor-cta__line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  /* Wider than a word space: the question and the call are two separate
     thoughts, and only the second one is clickable, so the break between them
     should read as deliberate. */
  gap: 0.2em 0.9em;
  margin: 0;
  font-family: var(--font-pixel);
  font-variant-ligatures: none;
  font-weight: 700;
  line-height: 1.1;
  text-align: center;
  /* The size lives here, not on the two halves: `gap` in em resolves against
     *this* element's font-size, so setting it on the children only would leave
     the gap pinned to the inherited body size while the text scaled away from
     it. */
  font-size: clamp(0.72rem, 2.3vw, 1.35rem);
  letter-spacing: 0.02em;
}

/* Minecraft's hard offset shadow, as on the section headings -- it is what
   gives the letterforms definition against the backdrop behind them. */
.sponsor-cta__lead {
  transition: color var(--theme-fade), text-shadow var(--theme-fade);
}

/* Both halves inherit that one size, so the line reads as a single sentence
   and the two colours carry the emphasis on their own. The floor is set by the
   width the whole line needs: 36 characters of a monospaced face come to about
   25x the font size, which has to clear the viewport less the gutters. At
   0.72rem that holds one line down to roughly 330px, below which it wraps
   rather than shrinking the text further. */

.sponsor-cta__lead {
  color: var(--sponsor-lead);
  text-shadow: 0.08em 0.08em 0 var(--sponsor-lead-shadow);
}

/* The only clickable part of the line. It carries its own hover and focus, so
   the affordance sits on exactly what navigates rather than on the sentence
   around it. */
.sponsor-cta__call,
.sponsor-cta__call:hover,
.sponsor-cta__call:focus,
.sponsor-cta__call:active,
.sponsor-cta__call:visited {
  color: var(--sponsor-call);
}

.sponsor-cta__call {
  text-shadow: 0.08em 0.08em 0 var(--sponsor-call-shadow);
  text-decoration: none;
  transition:
    transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
    color var(--theme-fade),
    text-shadow var(--theme-fade);
}

.sponsor-cta__call:hover,
.sponsor-cta__call:focus-visible {
  transform: scale(1.06);
}

.sponsor-cta__call:focus-visible {
  outline: 3px solid var(--sponsor-call);
  outline-offset: 6px;
  border-radius: 4px;
}

.ribbon__viewport {
  overflow: hidden;
}

.ribbon__track {
  display: flex;
  width: max-content;
  margin: 0;
  padding: 0.45rem 0;
  list-style: none;
}

/* Every slot is exactly 20 characters wide, so the ribbon keeps an even
   rhythm whatever the names are. The spacing is margin, NOT the track's
   `gap`: with `gap`, 60 slots carry 59 gaps, so half the track is not a whole
   copy and the loop lands half a gap out every cycle. As margin it is 60
   identical slot+gap periods and half is exactly one copy. */
.ribbon__slot {
  position: relative; /* the separator hangs off this */
  flex: 0 0 auto;
  width: var(--ribbon-slot);
  margin-right: var(--ribbon-gap);
  font-family: var(--font-pixel);
  font-variant-ligatures: none;
  font-size: clamp(0.62rem, 1.1vw, 0.72rem);
  line-height: 1.7;
  text-align: center;
  color: var(--ribbon-text);
  transition: color var(--theme-fade);
}

/* The clipping lives here rather than on the slot: the slot has to stay
   unclipped so the separator, which sits out in the margin beside it, is not
   cut off. */
.ribbon__name {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* The divider between sponsors: a hairline leaning like a slash, parked in
   the middle of the gap. It is absolutely positioned and so contributes no
   width -- the slot period stays exactly `slot + gap`, which is what keeps
   half the track equal to one whole copy and the loop seamless. Every slot
   gets one, including the last of each copy, so the pattern holds across the
   seam as well. It rides along with the track, so the slashes drift with the
   names rather than sitting still behind them. */
.ribbon__slot::after {
  content: "";
  position: absolute;
  top: 50%;
  left: calc(100% + var(--ribbon-gap) / 2);
  /* Same stroke weight as the frame around the band, and deliberately far
     taller than the band is: the viewport's `overflow: hidden` crops it, which
     is what makes it meet the grey flush at both ends whatever the font or the
     band height works out to. */
  width: var(--ribbon-frame-width);
  height: 300%;
  background-color: var(--ribbon-frame);
  /* Rotating an upright bar clockwise leans its top to the right, which is the
     way a "/" falls. */
  transform: translate(-50%, -50%) rotate(18deg);
}

/* Only once site.js has cloned the list: animating a single copy would drag a
   gap the width of the page across the ribbon. Without JS this stays a
   static, horizontally scrollable row rather than a broken animation. */
.ribbon--looping .ribbon__track {
  animation: ribbon-drift var(--ribbon-duration, 90s) linear infinite;
}

/* Left to right: the track starts shifted one whole copy to the left and
   travels back to zero. Both ends of the cycle show an identical frame, so
   there is no seam to hide. */
@keyframes ribbon-drift {
  from {
    transform: translateX(-50%);
  }
  to {
    transform: translateX(0);
  }
}


@media (prefers-reduced-motion: reduce) {
  .sponsor-field__inner {
    animation: none;
    transform: translateX(calc(-0.5 * var(--field-pass, 0px)));
  }

  .ribbon--looping .ribbon__track {
    animation: none;
  }

  /* Nothing moves now, so the row has to be reachable by hand. */
  .ribbon__viewport {
    overflow-x: auto;
  }
}

/* ==========================================================================
   Sponsor page
   ========================================================================== */

/* A standalone page, so there is no WebGL backdrop behind it. It paints the
   colour the landing page's backdrop has settled on by the time you reach the
   ribbon, so arriving here does not feel like leaving the site. */
:root {
  --sponsor-page-bg: #b7ebdc;
  /* Deep enough to read as text, faint enough that the copy in front of it
     still wins. One number per theme if it wants tuning. */
  --sponsor-field-ink: rgb(21 88 74 / 0.14);

}

:root[data-theme="dark"] {
  --sponsor-page-bg: #5e4b80;
  --sponsor-field-ink: rgb(228 218 248 / 0.15);
}

.page--sponsor {
  background-color: var(--sponsor-page-bg);
  transition: background-color var(--theme-fade);
}

/* The sponsor list laid diagonally behind the page. Oversized by half the
   viewport in every direction so the rotation still has something to show in
   the corners, and clipped back to the screen by its own overflow. */
.sponsor-field {
  position: fixed;
  inset: -50%;
  z-index: 0;
  display: grid;
  align-content: center;
  overflow: hidden;
  transform: rotate(-22deg);
  pointer-events: none;
  user-select: none;
  font-family: var(--font-pixel);
  font-variant-ligatures: none;
  font-weight: 700;
  font-size: clamp(0.8rem, 1.7vw, 1.25rem);
  line-height: 2.6;
  white-space: nowrap;
  color: var(--sponsor-field-ink);
  transition: color var(--theme-fade);
}

/* The rows all drift together, so one animated wrapper stands in for forty-odd
   animated rows. It is built a full pass wider than the layer, and travels
   exactly one pass before looping -- the same trick as the ribbon, except the
   layer is rotated, so the travel reads as diagonal. Very slow on purpose:
   `FIELD_SPEED_PX_PER_SEC` in site.js is 12, against the ribbon's 70. */
.sponsor-field__inner {
  animation: sponsor-drift var(--field-duration, 200s) linear infinite;
}

@keyframes sponsor-drift {
  from {
    transform: translateX(calc(-1 * var(--field-pass, 0px)));
  }
  to {
    transform: translateX(0);
  }
}

/* Rows already differ in content; the offset stops their word breaks lining
   up into accidental vertical channels. */
.sponsor-field__row:nth-child(even) {
  margin-left: -7%;
}

.sponsor-field__row:nth-child(3n) {
  margin-left: 4%;
}

.sponsor-page {
  position: relative; /* above the field */
  z-index: 1;
  display: grid;
  justify-items: center;
  align-content: start; /* the heading belongs at the top, not centred */
  min-height: 100svh;
  padding: clamp(1.5rem, 5vh, 3rem) var(--gutter) clamp(3rem, 10vh, 6rem);
}

.sponsor-page__inner {
  width: 100%;
  text-align: center;
}

/* The heading spans the screen; the copy under it stays a column, but a wider
   one than the landing page's -- this page has no competing content beside it.
   44rem is about 78 characters of the pixel face, still inside the range a
   line stays comfortable to read. */
.sponsor-page .about__body {
  max-width: 44rem;
}

/* One line, one size, one colour, sized off the viewport so it spans the
   screen rather than sitting in a column, at a bit over half its width. (8vw
   is what fills the gutters exactly, if it ever wants to be edge to edge --
   counting the offset shadow, which hangs 0.08em past the last glyph.) */
.sponsor-page__heading {
  margin: 0;
  max-width: none;
  font-family: var(--font-pixel);
  font-variant-ligatures: none;
  font-weight: 700;
  font-size: clamp(1.15rem, 5.5vw, 6rem);
  line-height: 1.15;
  letter-spacing: 0.02em;
  white-space: nowrap;
  /* The same pair the section headings on the landing page use, so the two
     pages agree in both themes. */
  color: var(--heading-text);
  text-shadow: 0.08em 0.08em 0 var(--heading-shadow);
  transition: color var(--theme-fade), text-shadow var(--theme-fade);
}

.sponsor-page .about__body {
  margin-top: clamp(2rem, 6vh, 3rem);
}


/* ==========================================================================
   Sponsor logos
   ========================================================================== */

:root {
  /* Soft rather than drawn: a pale translucent ring that reads as an edge
     without outlining the circle the way the project cards are outlined. */
  --sponsor-logo-ring: rgb(255 255 255 / 0.7);
  --sponsor-logo-bg: rgb(255 255 255 / 0.22);
}

:root[data-theme="dark"] {
  --sponsor-logo-ring: rgb(228 218 248 / 0.3);
  --sponsor-logo-bg: rgb(9 20 40 / 0.26);
}

.sponsor-logos {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(1rem, 3vw, 2rem);
  /* The gap the brief asked for, measured from the bottom of the copy. */
  margin: 80px 0 0;
  padding: 0;
  list-style: none;
}

.sponsor-logos__item {
  display: grid;
  place-items: center;
  width: clamp(4rem, 11vw, 6.5rem);
  aspect-ratio: 1;
  overflow: hidden;
  border: 3px solid var(--sponsor-logo-ring);
  border-radius: 50%;
  background-color: var(--sponsor-logo-bg);
  box-shadow: 0 8px 22px -12px rgb(0 0 0 / 0.45);
  transition:
    border-color var(--theme-fade),
    background-color var(--theme-fade),
    color var(--theme-fade);
}

.sponsor-logos__item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Fills the circle so the whole thing is the target, not just the artwork. */
.sponsor-logos__link {
  display: block;
  width: 100%;
  height: 100%;
}

/* Only the ones that go somewhere lift, so the circles that are not links do
   not advertise a click. The scale is on the circle rather than the anchor
   inside it, which `overflow: hidden` would clip. */
.sponsor-logos__item--linked {
  transition:
    transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
    border-color var(--theme-fade),
    background-color var(--theme-fade),
    color var(--theme-fade);
}

/* Focus-within would stick here: clicking a circle focuses its anchor, the
   link opens in a new tab, and coming back leaves it focused and therefore
   still raised with the pointer nowhere near it. Focus-visible only matches
   focus the browser judges worth showing -- keyboard, not a mouse click -- so
   the lift still pairs with the focus ring without surviving a click. */
.sponsor-logos__item--linked:hover,
.sponsor-logos__item--linked:has(.sponsor-logos__link:focus-visible) {
  transform: scale(1.07);
}

.sponsor-logos__link:focus-visible {
  outline: 3px solid var(--heading-text);
  outline-offset: 4px;
  border-radius: 50%;
}

/* The fallback for a sponsor with no logo: its initial, in the site's face. */
.sponsor-logos__item--initial {
  font-family: var(--font-pixel);
  font-variant-ligatures: none;
  font-weight: 700;
  font-size: clamp(1.4rem, 4vw, 2.2rem);
  color: var(--heading-text);
  text-shadow: 0.08em 0.08em 0 var(--heading-shadow);
}
