/* Guardian Comics. Palette and motion carried over from the 2023 app (frontend/src/components/_styles/abstracts.ts). */

@font-face {
  font-family: "Quattrocento Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/quattrocento-sans-400-v22.woff2") format("woff2");
}
@font-face {
  font-family: "Quattrocento Sans";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/quattrocento-sans-400-italic-v22.woff2") format("woff2");
}
@font-face {
  font-family: "Quattrocento Sans";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/quattrocento-sans-700-v22.woff2") format("woff2");
}

:root {
  --black: #000;
  /* The page sits a shade above black, which reads softer than pure black. */
  --page: #0b0c0e;
  --dark: #242b38;
  --light: #e5eff5;
  --whisper: #ecf2f7;
  --grey: #47494c;
  --grey-dark: #676b6f;
  --primary: #0e74cd;
  --primary-light: #5aa9f0;
  --primary-dark: #0b63b0;
  --muted: #a7b0b8;

  --font: "Quattrocento Sans", "Helvetica Neue", Arial, sans-serif;
  --gutter: clamp(1rem, 4vw, 2.5rem);
  --header-height: 5.5rem;
  --radius: 0.3125rem;

  --timing: 0.2s;
  --ease-std: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out: cubic-bezier(0.5, 1, 0.89, 1);
  --ease-io: cubic-bezier(0.76, 0, 0.24, 1);

  color-scheme: dark;
}

/* Base */

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

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

body {
  margin: 0;
  min-height: 100svh;
  background: var(--page);
  color: var(--light);
  font-family: var(--font);
  font-size: 1.0625rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3 {
  line-height: 1.15;
  font-weight: 700;
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

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

a {
  color: var(--primary-light);
  text-underline-offset: 0.15em;
}

button {
  font: inherit;
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--primary-light);
  outline-offset: 3px;
}

.site-footer ul,
.library__grid,
.series__issues,
.reader__pages,
.reader__scrub {
  list-style: none;
  margin: 0;
  padding: 0;
}

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

.skip-link {
  position: fixed;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 100;
  padding: 0.6rem 1rem;
  border-radius: var(--radius);
  background: white;
  color: var(--black);
  font-weight: 700;
  transform: translateY(-200%);
}
.skip-link:focus {
  transform: none;
}

.icon {
  width: 1.25em;
  height: 1.25em;
  flex: none;
}
/* Chevrons in links nudge the way they point on hover and keyboard focus. */
a .icon--back,
a .icon--next {
  transition: transform 0.25s var(--ease-out);
}
a:is(:hover, :focus-visible) .icon--back {
  transform: translateX(-3px);
}
a:is(:hover, :focus-visible) .icon--next {
  transform: translateX(3px);
}

.eyebrow {
  margin: 0 0 0.5rem;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
/* Small uppercase labels, a touch heavier: 700 is Quattrocento Sans's boldest, so a hairline stroke
   in the text's own colour thickens it further. Their wide letter spacing otherwise reads thin. */
.eyebrow,
.site-header__label,
.menu-comic__meta,
.menu-issue__label,
.issue__number {
  font-weight: 700;
  -webkit-text-stroke: 0.03em currentColor;
}

/* Buttons */

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 1.75rem;
}

/* A few links carry a specular light on their edge (data-specular, specular.js): a canvas behind the
   content, reaching 24px past the box for its glow, with a white light over a hairline in
   --specular-base. The light brightens and slides toward the pointer, so hover needs no other change.
   Every other button gets the same hairline, still. */
[data-specular] {
  --specular-line: #fff;
  --specular-base: #525252;
  position: relative;
  isolation: isolate;
}
/* Under the content (z-index -1 inside the element's own stacking context), above its background. */
.specular__light {
  position: absolute;
  inset: -24px;
  z-index: -1;
  display: block;
  width: calc(100% + 48px);
  height: calc(100% + 48px);
  pointer-events: none;
}
.btn {
  --specular-base: #525252;
  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 2.875rem;
  padding: 0.6rem 1.35rem;
  border: 0;
  border-radius: var(--radius);
  font-weight: 700;
  font-size: 1rem;
  line-height: 1.2;
  text-decoration: none;
  /* Buttons sit inside hero text blocks that carry a text shadow; they don't need it. */
  text-shadow: none;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--specular-base) 60%, transparent), 0 10px 24px -14px rgb(0 0 0 / 0.45);
  cursor: pointer;
  transition: background-color var(--timing) var(--ease-std), transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* The canvas draws the hairline on these. */
.btn[data-specular] {
  box-shadow: 0 10px 24px -14px rgb(0 0 0 / 0.45);
}
.btn:active {
  transform: scale(0.97);
}
/* The old app's blue, with the light on a paler blue hairline. */
.btn--primary {
  --specular-base: var(--primary-light);
  background: var(--primary);
  color: white;
}
.btn--primary:not([data-specular]):hover {
  filter: brightness(1.08);
}
.btn--primary:active {
  background: var(--primary-dark);
}
/* Glass: a faint white tint over whatever's behind, blurred so the label stays legible over art. */
.btn--ghost {
  background: rgb(255 255 255 / 0.06);
  color: white;
  backdrop-filter: blur(6px);
}
.btn--ghost:hover {
  background: rgb(255 255 255 / 0.1);
}

/* Header: one short, very dark bar fixed at the top centre of the screen (after osmo.supply's),
   with the logo on the left and a menu button on the right. Its texture lets the page show through:
   a frosted, translucent base, plus a halftone whose dots (generated at build time, /img/halftone-bar.svg)
   build up in some areas and thin out in others, each dot a window onto the art behind. */

:root {
  --header-inset: 0.75rem;
}

.site-header {
  position: fixed;
  top: var(--header-inset);
  left: 0;
  right: 0;
  z-index: 20;
  /* Just wider than the logo and the menu button, so the gap between them stays tight. */
  width: min(100% - 2 * var(--header-inset), 19rem);
  margin-inline: auto;
}

.site-header__bar {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 3.25rem; /* 52px */
  padding: 0 0.35rem 0 1rem;
  border-radius: 0.5rem;
}
/* The base and the halftone are sibling layers: an element with a backdrop-filter stops anything
   inside it from seeing the page behind, so the dots can't live inside the frosted base. They hang
   off their own element, so page transitions can stretch the background on its own. */
.site-header__bg,
.site-header__bg::before,
.site-header__bg::after {
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
}
.site-header__bg::before,
.site-header__bg::after {
  content: "";
}
.site-header__bg::before {
  background: rgb(14 14 16 / 0.82);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  backdrop-filter: blur(18px) saturate(140%);
  box-shadow: 0 0 0 1px rgb(255 255 255 / 0.07), 0 0.75rem 2rem -0.75rem rgb(0 0 0 / 0.7);
}
.site-header__bg::after {
  -webkit-backdrop-filter: blur(3px) saturate(170%) brightness(0.95);
  backdrop-filter: blur(3px) saturate(170%) brightness(0.95);
  background: rgb(255 255 255 / 0.03);
  -webkit-mask: url("/img/halftone-bar.svg") center / auto 100% no-repeat;
  mask: url("/img/halftone-bar.svg") center / auto 100% no-repeat;
}

.site-header__logo {
  display: block;
  color: white;
  flex: none;
}
.site-header__logo .logo {
  width: 7.5rem;
}

.site-header__toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  height: 2.75rem;
  padding: 0 0.85rem;
  border: 0;
  border-radius: 0.4rem;
  background: none;
  color: white;
  cursor: pointer;
}
.site-header__toggle:hover {
  background: rgb(255 255 255 / 0.08);
}
/* "Menu" rolls up out of view as "Close" rolls in beneath it. */
.site-header__label {
  display: grid;
  overflow: hidden;
  font-size: 0.8125rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.site-header__label span {
  grid-area: 1 / 1;
  transition: translate 0.45s var(--ease-io);
}
.site-header__label span:last-child {
  translate: 0 110%;
}
.site-header.is-open .site-header__label span:first-child {
  translate: 0 -110%;
}
.site-header.is-open .site-header__label span:last-child {
  translate: 0 0;
}
/* Two lines. Opening, they slide together to the middle, then both rotate into an X; closing
   reverses it (untwist, then separate). Separate translate and rotate properties let each step have
   its own timing. Keyed off the button's aria-expanded, so it follows whichever script opens the menu. */
.burger {
  position: relative;
  display: block;
  width: 1.2rem;
  height: 0.6rem;
}
.burger span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  border-radius: 1px;
  background: currentColor;
  transition:
    translate 0.2s var(--ease-std) 0.2s,
    rotate 0.2s var(--ease-std);
}
.burger span:nth-child(1) {
  top: 0;
}
.burger span:nth-child(2) {
  bottom: 0;
}
[aria-expanded="true"] > .burger span {
  transition:
    translate 0.2s var(--ease-std),
    rotate 0.25s cubic-bezier(0.34, 1.4, 0.64, 1) 0.2s;
}
[aria-expanded="true"] > .burger span:nth-child(1) {
  translate: 0 calc(0.3rem - 1px);
  rotate: 45deg;
}
[aria-expanded="true"] > .burger span:nth-child(2) {
  translate: 0 calc(-0.3rem + 1px);
  rotate: -45deg;
}

/* Menu: fixed under the page, after Osmo's fixed underlay navigation but dropping down instead of
   sliding aside. Opening it, site.js slides the page (.page) down to uncover it, leaving a strip of
   the page in view at the bottom; the menu fills the screen above that strip. The series titles sit
   on the left at display size, and the stage on the right shows the active series' covers. Where the
   pointer is along a title picks the issue: the left end is the first issue, the right end the last. */

:root {
  --menu-bg: #07090d;
  /* How much of the page stays in view below the open menu. */
  --menu-peek: clamp(4.5rem, 14svh, 8rem);
}

.page {
  position: relative;
  z-index: 2;
}
/* The backdrop: the menu's gradient as one fixed layer behind the page, which the menu (having no
   background of its own) shows through. The page's main content carries the opaque background
   instead of .page, and clips to its own box so the series art (fixed inside it) stays within it. */
body:has(> .page)::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  background: radial-gradient(ellipse 70% 80% at 78% 45%, #111724, var(--menu-bg) 70%);
  pointer-events: none;
}
.page > main {
  isolation: isolate;
  clip-path: inset(0);
  background: var(--page);
}
/* While the menu is open the page is scroll-locked at --lock (the scroll position, set by site.js),
   so everything above that is clipped away rather than shown over the menu, and the visible top
   edge rounds off (--page-radius, which site.js eases in and out with the slide). The page's transform makes it the containing block for fixed descendants, so
   the series backdrop is pinned back to the top of the screen by hand. */
.page.is-shifted {
  clip-path: inset(var(--lock, 0px) 0 0 0 round var(--page-radius, 0px) var(--page-radius, 0px) 0 0);
}
.page.is-shifted .series__covers {
  top: var(--lock, 0px);
  bottom: auto;
  height: 100svh;
}
/* Darkens the strip of page left in view; clicking it closes the menu. */
.page__dim {
  position: absolute;
  inset: var(--lock, 0px) 0 auto;
  z-index: 30;
  height: 100svh;
  background: var(--black);
  opacity: 0;
  visibility: hidden;
  cursor: pointer;
}

.site-menu {
  position: fixed;
  inset: 0 0 auto;
  z-index: 1;
  height: calc(100svh - var(--menu-peek));
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 1.5rem;
  padding: calc(var(--header-inset) + 3.25rem + 2rem) var(--gutter) 1.75rem;
  overflow: hidden;
}
.site-menu[hidden] {
  display: none;
}
.site-menu ul,
.site-menu ol {
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-menu__comics {
  display: grid;
  justify-items: start;
  gap: clamp(1.5rem, 5svh, 3.5rem);
  /* On smaller screens the titles sit at the top, leaving the space below them to the stage. */
  margin-bottom: auto !important;
}
.menu-comic__title {
  display: grid;
  gap: 0.6rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--light);
  font: inherit;
  text-align: left;
  cursor: pointer;
  /* Inactive titles recede; the active one comes forward. */
  opacity: 0.3;
  transition: opacity 0.35s var(--ease-std);
  -webkit-tap-highlight-color: transparent;
}
.menu-comic.is-active .menu-comic__title,
.menu-comic__title:hover,
.menu-comic__title:focus-visible {
  opacity: 1;
}
.menu-comic__name {
  display: block;
  font-size: clamp(2.25rem, 9.5vw, 4.5rem);
  font-weight: 700;
  line-height: 0.95;
  letter-spacing: -0.025em;
  white-space: nowrap;
}
.menu-comic__meta,
.menu-issue__label {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
.menu-comic__name,
.menu-comic__meta {
  /* site.js slides these in; inline elements can't be transformed. */
  will-change: transform;
}

/* The stage: every series' covers sit in the same place, and only the active series' can be seen or
   clicked. Each cover is placed by site.js: the active space at the left, large, with its label
   fading in underneath, and the rest queued smaller to its right, spilling off the screen and
   autoplaying through it. */
.menu-comic__issues {
  --cover: clamp(4.5rem, calc((100svh - var(--menu-peek) - 33rem) / 1.5), 40vw);
  position: absolute;
  left: var(--gutter);
  right: 0;
  bottom: calc(1.75rem + 3.5rem);
  height: calc(var(--cover) * 1.5 + 2.25rem);
  pointer-events: none;
}
.menu-comic.is-active .menu-comic__issues {
  pointer-events: auto;
}
.menu-issue {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--cover);
  /* Queued covers shrink towards the middle of the cover's left edge, so they line up on its centre. */
  transform-origin: 0 calc(var(--cover) * 0.75);
  visibility: hidden;
  opacity: 0;
}
.menu-issue__link {
  display: block;
  aspect-ratio: 2 / 3;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 2rem 4rem -1rem rgb(0 0 0 / 0.85);
}
.menu-issue__link:focus-visible {
  outline-offset: 4px;
}
.menu-issue__link img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Counts down the active cover's turn in the autoplay: a ring that fills clockwise (site.js). */
.menu-issue__timer {
  position: absolute;
  top: calc(var(--cover) * 1.5 - 2.75rem);
  left: calc(var(--cover) - 2.75rem);
  width: 2rem;
  height: 2rem;
  padding: 0.3rem;
  border-radius: 50%;
  background: rgb(0 0 0 / 0.5);
  backdrop-filter: blur(6px);
  pointer-events: none;
  visibility: hidden;
  opacity: 0;
}
.menu-issue__timer svg {
  display: block;
  width: 100%;
  height: 100%;
  rotate: -90deg;
}
.menu-issue__timer circle {
  fill: none;
  stroke: rgb(255 255 255 / 0.2);
  stroke-width: 2.5;
}
.menu-issue__timer .menu-issue__timer-fill {
  stroke: var(--light);
  stroke-linecap: round;
}
.menu-issue__label {
  position: absolute;
  /* Centred under the cover, with room either side for a name wider than it (GSAP owns transforms
     here, so this can't use translate). */
  left: -50%;
  right: -50%;
  text-align: center;
  color: var(--light);
  white-space: nowrap;
  visibility: hidden;
  opacity: 0;
}
/* Hovering a cover zooms it slightly (about its centre). Its label stays the same size but moves
   down by as much as the cover's bottom edge grows, so the cover never covers it, and the
   countdown ring keeps to the cover's corner. GSAP owns the transforms of the cover's .menu-issue
   and its label, so the zoom is on the link and the label moves by its top. */
.menu-issue {
  --zoom: 1.04;
  --zoomed: 0;
}
.menu-issue__link,
.menu-issue__timer {
  transition: scale 0.35s var(--ease-out), translate 0.35s var(--ease-out);
}
.menu-issue__label {
  /* Half the extra height: the cover is 1.5 × --cover tall and grows evenly top and bottom. */
  top: calc(100% + 0.9rem + var(--zoomed) * (var(--zoom) - 1) * var(--cover) * 0.75);
  transition: top 0.35s var(--ease-out);
}
.menu-issue__timer {
  translate: calc(var(--zoomed) * (var(--zoom) - 1) * var(--cover) * 0.5) calc(var(--zoomed) * (var(--zoom) - 1) * var(--cover) * 0.75);
}
@media (hover: hover) {
  .menu-issue:has(.menu-issue__link:hover) {
    --zoomed: 1;
  }
  .menu-issue:has(.menu-issue__link:hover) .menu-issue__link {
    scale: var(--zoom);
  }
}
.menu-issue:has(.menu-issue__link:focus-visible) {
  --zoomed: 1;
}
.menu-issue:has(.menu-issue__link:focus-visible) .menu-issue__link {
  scale: var(--zoom);
}
@media (prefers-reduced-motion: reduce) {
  .menu-issue__link,
  .menu-issue__timer,
  .menu-issue__label {
    transition: none;
  }
}

@media (min-width: 64em) {
  .site-menu {
    padding-top: calc(var(--header-inset) + 3.25rem + 3rem);
  }
  .site-menu__comics {
    gap: clamp(2.5rem, 8svh, 5.5rem);
    margin-block: auto !important;
  }
  .menu-comic__title {
    gap: 0.9rem;
  }
  .menu-comic__name {
    font-size: clamp(3rem, 5vw, 6.5rem);
  }
  /* The stage moves to the right of the titles and fills the menu's height, the active cover
     taking most of it. */
  .menu-comic__issues {
    --cover: min(30vw, calc((100svh - var(--menu-peek) - 16rem) / 1.5));
    left: 56vw;
    bottom: auto;
    top: 50%;
    /* Trimmed from the top: the bottom stays where it was. */
    translate: 0 calc(-50% + 2rem);
  }
}

/* Home hero */

.hero {
  position: relative;
  min-height: min(92svh, 60rem);
  display: flex;
  align-items: center;
  isolation: isolate;
  background: var(--dark);
}

.hero__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
}
.hero__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 75% 35%;
}
.hero__bg::before,
.hero__bg::after {
  content: "";
  position: absolute;
  inset: 0;
}
.hero__bg::before {
  background: linear-gradient(to right, rgb(0 0 0 / 0.85) 0%, rgb(0 0 0 / 0.45) 40%, rgb(0 0 0 / 0) 70%);
}
.hero__bg::after {
  top: auto;
  height: 25rem;
  background: linear-gradient(to top, var(--page) 0%, transparent 100%);
}
.hero__bg--empty {
  background: radial-gradient(ellipse at 70% 30%, var(--dark), var(--page) 70%);
}

.hero__content {
  width: 100%;
  max-width: calc(34rem + 2 * var(--gutter));
  padding: calc(var(--header-height) + 3rem) var(--gutter) 10rem;
  font-size: 1.125rem;
  text-shadow: 0 1px 12px rgb(0 0 0 / 0.5);
}
.hero__title {
  margin: 0 0 0.75rem;
  font-size: clamp(2.25rem, 6vw, 3.75rem);
}
.hero__title .title-art {
  width: auto;
  max-width: min(100%, 26rem);
  margin-inline-start: -0.5rem;
  filter: drop-shadow(0 4px 18px rgb(0 0 0 / 0.45));
}
.hero__issue {
  margin: 0 0 0.75rem;
  font-weight: 700;
  font-size: 1.25rem;
  color: white;
}
.hero__description p {
  margin: 0 0 0.75rem;
}

@media (max-width: 47.99em) {
  .hero {
    align-items: flex-end;
  }
  .hero__bg::before {
    background: linear-gradient(to top, rgb(0 0 0 / 0.95) 15%, rgb(0 0 0 / 0) 70%);
  }
  .hero__content {
    padding-top: 40svh;
  }
  .hero__description p + p {
    display: none;
  }
}

/* Library: every issue in one grid (cards from the app's feed) */

.library {
  position: relative;
  z-index: 1;
  margin-top: -6rem;
  padding: 0 var(--gutter) clamp(3rem, 6vw, 5rem);
}
.page-home main > .library:first-child {
  margin-top: 0;
  padding-top: calc(var(--header-height) + 2rem);
}
.library__title {
  margin: 0 0 1.25rem;
  font-size: clamp(1.25rem, 2.5vw, 1.625rem);
}
.library__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1rem, 2.5vw, 2rem);
}
@media (min-width: 40em) {
  .library__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
@media (min-width: 64em) {
  .library__grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* Card */

.card {
  position: relative;
  display: block;
  aspect-ratio: 2 / 3;
  transition: transform 0.6s var(--ease-out);
  color: white;
  text-decoration: none;
}
/* The home grid shows plain cards: no page edges or drop shadow. */
.card .book {
  height: 100%;
  background: var(--dark);
  box-shadow: none;
}
.card__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.card__overlay {
  position: absolute;
  inset: 0;
  z-index: 3;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 1.25rem 1rem;
}
/* The gradients live on their own layers so they can fade and move; CSS can't animate from one
   gradient to another. ::before is the full hover shade, ::after the faint one under a progress bar. */
.card__overlay::before,
.card__overlay::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
.card__overlay::before {
  background: linear-gradient(to top, rgb(0 0 0 / 0.85) 5%, rgb(0 0 0 / 0.4) 30%, rgb(0 0 0 / 0) 55%);
  transform-origin: bottom;
}
.card__overlay::after {
  background: linear-gradient(to top, rgb(0 0 0 / 0.6), rgb(0 0 0 / 0) 25%);
  opacity: 0;
}
.card__title {
  font-weight: 700;
  line-height: 1.2;
}
.card__subtitle {
  font-size: 0.875rem;
  line-height: 1.25;
  opacity: 0.7;
  margin-top: 0.15rem;
}
.card .progress {
  margin-top: 0.65rem;
}

@media (hover: hover) {
  /* A very slight zoom of the whole card. */
  .card:hover,
  .card:focus-visible {
    z-index: 1;
    transform: scale(1.03);
  }

  /* At rest: the shade is gone and pulled down, the text dropped and hidden. On hover the shade
     rises and fades in first, then the text follows, top line first. Leaving reverses together. */
  .card__overlay::before,
  .card__overlay::after {
    transition: opacity 0.5s var(--ease-std), transform 0.6s var(--ease-out);
  }
  .card__overlay > * {
    transition: opacity 0.35s var(--ease-std), transform 0.45s var(--ease-out);
  }
  .card:not(:hover):not(:focus-visible) .card__overlay::before {
    opacity: 0;
    transform: scaleY(0.6);
  }
  .card:not(:hover):not(:focus-visible) .card__overlay > * {
    opacity: 0;
    transform: translateY(0.625rem);
  }
  .card:is(:hover, :focus-visible) .card__overlay > :nth-child(1) {
    transition-delay: 0.08s;
  }
  .card:is(:hover, :focus-visible) .card__overlay > :nth-child(2) {
    transition-delay: 0.13s;
  }
  .card:is(:hover, :focus-visible) .card__overlay > :nth-child(3) {
    transition-delay: 0.18s;
  }

  /* Started cards keep their progress bar, over a faint shade that hands over to the full one. */
  .card:not(:hover):not(:focus-visible) .card__overlay:has(.progress:not([hidden]))::after {
    opacity: 1;
  }
  .card:not(:hover):not(:focus-visible) .card__overlay > .progress:not([hidden]) {
    opacity: 1;
    transform: none;
  }
}

/* Comic book treatment. Home grid cards are a flat .book (art plus gloss) that zooms slightly on
   hover. Series covers ([data-tilt]) are magnetic, tilt toward the pointer and get a glare that
   follows it (site.js).

   The [data-tilt] link is a still frame that never transforms; its .tilt-shield receives every
   pointer event, and the .book inside it tilts with pointer-events off. Measuring and hit-testing
   against the still frame stops edge jitter as the art moves. site.js sets --tx/--ty (the magnetic
   drift toward a pointer in the zone around the cover), --rx/--ry (a tilt derived from that drift)
   and --mx/--my (glare) on the frame, and .book inherits them. */

[data-tilt] {
  position: relative;
  display: block;
}
.tilt-shield {
  position: absolute;
  inset: 0;
  z-index: 4;
}
.book {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: 0.2rem;
  pointer-events: none;
}
.book::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  /* Glossy cover stock. */
  background: linear-gradient(125deg, rgb(255 255 255 / 0.16) 0%, rgb(255 255 255 / 0) 32%, rgb(255 255 255 / 0) 68%, rgb(0 0 0 / 0.12) 100%);
}

/* Series covers: a flat cover that drifts toward the pointer and tilts in perspective about its
   centre. Drift and tilt are both tweened together by site.js every frame, so neither transitions
   in CSS. */
.book--tilt {
  translate: var(--tx, 0px) var(--ty, 0px);
  transform: perspective(1000px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  box-shadow: 0 1.25rem 2rem -0.75rem rgb(0 0 0 / 0.75);
}

.book__shine {
  position: absolute;
  inset: 0;
  z-index: 2;
  /* Screen blending keeps the glare visible on light and dark covers alike. */
  background: radial-gradient(circle at var(--mx, 50%) var(--my, 50%), rgb(255 255 255 / 0.21) 0%, rgb(255 255 255 / 0.07) 18%, rgb(255 255 255 / 0) 45%);
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 0.4s var(--ease-std);
}
.is-tilting .book__shine {
  opacity: 1;
}

/* Progress bar */

.progress {
  display: grid;
  width: 100%;
  height: 0.25rem;
  border-radius: 0.125rem;
  background: black;
  box-shadow: 0 0 0 1px rgb(255 255 255 / 0.12);
  overflow: hidden;
}
.progress[hidden] {
  display: none;
}
.progress__bar {
  width: calc(var(--complete, 0) * 1%);
  height: 100%;
  background: white;
}
.issue__progress {
  display: grid;
  gap: 0.4rem;
  max-width: 20rem;
  margin-top: 1.5rem;
}
.issue__progress[hidden] {
  display: none;
}
.progress__text {
  font-size: 0.875rem;
  color: var(--muted);
}

/* Footer: a statement piece, after Studio Freight's Huxley footer, and nothing but the wordmark at
   full width: tone-on-tone charcoal on near-black, a step darker than the page above it. The
   footer takes up its space in the page as usual, but its content is fixed to the bottom of the
   screen and clipped to the footer's box (clip-path clips fixed descendants, unlike overflow), so
   the page above seems to slide up off a footer that was underneath all along.
   The ::before spacer gives the footer the same height as its fixed content. */

.site-footer {
  --footer-pad-top: clamp(3rem, 8vw, 8rem);
  --fineprint-height: 3.25rem; /* the fine print row under the wordmark, with its gap */
  /* The wordmark is 149.9 × 40.22; it narrows if need be so the footer never outgrows the screen. */
  --wordmark-width: min(100%, (100svh - var(--footer-pad-top) - var(--fineprint-height) - var(--gutter)) * 3.727);
  position: relative;
  z-index: 1;
  clip-path: inset(0);
  padding: var(--footer-pad-top) var(--gutter) var(--gutter);
}
.site-footer::before {
  content: "";
  display: block;
  width: var(--wordmark-width);
  aspect-ratio: 149.9 / 40.22;
  margin-bottom: var(--fineprint-height);
}
.site-footer__inner {
  position: fixed;
  inset: auto 0 0;
  padding: inherit;
  /* Solid, so the menu's backdrop doesn't show through. The wordmark is a neutral charcoal just
     lighter than the page (--page), so it reads as raised lettering. */
  background: #050506;
  color: #141519;
}
.site-footer__wordmark {
  width: var(--wordmark-width);
}
/* Parallax: as the footer comes into view, the wordmark rises at 40% of the scroll speed instead
   of sitting still, so it reads as a layer between the page and the backdrop. A scroll-driven
   animation over the footer's entry, so no JS; without support (or with reduced motion) it simply
   stays put. */
@keyframes footer-parallax {
  from {
    translate: 0 40%;
  }
}
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .site-footer {
      view-timeline: --footer block;
    }
    .site-footer__inner {
      animation: footer-parallax linear both;
      animation-timeline: --footer;
      animation-range: entry 0% entry 100%;
    }
  }
}
.site-footer__wordmark svg {
  width: 100%;
}
/* Spotlight, after Payload's footer but quieter: a soft light follows the pointer, lifting the
   charcoal letters under it (a gradient-filled copy of the wordmark, see build.mjs) and faintly
   warming the near-black behind them. site.js sets the position (--spot-x/--spot-y on the footer,
   the gradient's centre in the SVG) and .is-lit while the pointer is over the footer. */
.site-footer__inner::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    circle 32rem at var(--spot-x, 50%) var(--spot-y, 50%),
    rgb(255 255 255 / 0.025),
    rgb(255 255 255 / 0.01) 40%,
    rgb(255 255 255 / 0) 100%
  );
  opacity: 0;
  transition: opacity 0.8s var(--ease-std);
}
.site-footer__spot {
  opacity: 0;
  transition: opacity 0.8s var(--ease-std);
}
.site-footer__inner.is-lit::before,
.site-footer__inner.is-lit .site-footer__spot {
  opacity: 1;
}
.site-footer__wordmark,
.site-footer__fineprint {
  position: relative;
}
/* A halftone over the whole footer, wordmark included: the same 45° screen of ink dots as the
   header's and the series backdrop's, but even and faint (only a hint of their variation), as a
   seamless tile generated at build time (/img/halftone-footer.svg). The fine print sits above it
   so it stays crisp. */
.site-footer__inner::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* Anchored at 0 0 so the tiles land on whole pixels, with no hairline joins. */
  background: url("/img/halftone-footer.svg") 0 0 / 480px repeat;
  opacity: 0.6;
}
.site-footer__fineprint {
  z-index: 2;
}
/* Fine print: quiet, a few steps up from the wordmark so it can still be read. */
.site-footer__fineprint {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1.5rem;
  min-height: calc(var(--fineprint-height) - 1.25rem);
  margin-top: 1.25rem;
  font-size: 0.8125rem;
  color: #5c6168;
}
.site-footer__fineprint p {
  margin: 0;
}
.site-footer__fineprint ul {
  display: flex;
  gap: 1.5rem;
}
.site-footer__fineprint a {
  color: inherit;
  text-decoration: none;
  transition: color var(--timing) var(--ease-std);
}
.site-footer__fineprint a:hover,
.site-footer__fineprint a:focus-visible {
  color: var(--light);
}
.prose__updated {
  margin-top: 2.5rem;
  font-size: 0.9375rem;
  opacity: 0.6;
}

/* Series page: a hero introducing the series, then one large issue per screen. On desktop each
   issue's cover sits centred over its description until the issue reaches the middle of the
   screen, then the two slide apart (see Reveal, below). */

.series__intro {
  position: relative;
  isolation: isolate;
  /* Short enough for the first comic to peek up from below. */
  min-height: min(68svh, 44rem);
  display: flex;
  align-items: flex-end;
  padding: calc(var(--header-height) + 2rem) var(--gutter) 4rem;
}
/* The backdrop: the active issue's cover, fixed behind the whole page, faintly blurred (so its
   detail still reads) and darkened so the sharp cover in front stands out, and crossfading as each
   issue reaches the middle (see site.js). It sits before the intro in the DOM, so the intro and
   issues paint over it. */
.series__covers {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background: var(--page);
}
.series__covers img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(6px) brightness(0.4) saturate(0.75);
  /* Enough zoom to keep the blurred edges off the screen. */
  scale: 1.05;
  opacity: 0;
  transition: opacity 0.9s var(--ease-io);
}
.series__covers img.is-current {
  opacity: 1;
}
/* A halftone vignette over the art: the header's irregular dot screen, as a repeating ink tile
   (/img/halftone-tile.svg, generated at build time), masked so the dots are faint in the middle and
   build up toward the screen edges. */
.series__covers::before,
.series__covers::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.series__covers::before {
  z-index: 1;
  /* Anchored at 0 0 (not centred) so the tiles always land on whole pixels, with no hairline joins. */
  background: url("/img/halftone-tile.svg") 0 0 / 720px repeat;
  opacity: 0.85;
  -webkit-mask: radial-gradient(ellipse at center, rgb(0 0 0 / 0.08) 20%, rgb(0 0 0 / 0.6) 60%, #000 100%);
  mask: radial-gradient(ellipse at center, rgb(0 0 0 / 0.08) 20%, rgb(0 0 0 / 0.6) 60%, #000 100%);
}
/* Then a plain vignette for depth, shade on the left so the intro text stays readable, and a wash of
   the site's dark navy (--dark) so every cover sits in the same cool, dark tone under the text. */
.series__covers::after {
  z-index: 2;
  background:
    linear-gradient(to right, rgb(0 0 0 / 0.55), rgb(0 0 0 / 0) 55%),
    linear-gradient(rgb(36 43 56 / 0.5), rgb(36 43 56 / 0.5)),
    radial-gradient(ellipse at center, rgb(0 0 0 / 0) 30%, rgb(0 0 0 / 0.55) 75%, rgb(0 0 0 / 0.9) 100%);
}
@media (prefers-reduced-motion: reduce) {
  .series__covers img {
    transition: none;
  }
}
.series__content {
  max-width: 36rem;
  /* The art behind the hero is bright and has no shading, so the text carries its own contrast: a
     tight shadow for crisp edges and a wide glow, even over a light sky. */
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.7), 0 2px 18px rgb(0 0 0 / 0.65);
}
.series__content .eyebrow {
  color: white;
}
.series__title {
  margin: 0 0 1rem;
  font-size: clamp(2.25rem, 6vw, 3.75rem);
}
.series__title .title-art {
  width: auto;
  max-width: min(100%, 26rem);
  margin-inline-start: -0.5rem;
  filter: drop-shadow(0 4px 18px rgb(0 0 0 / 0.45));
}
.series__description {
  margin: 0;
  font-size: clamp(1.25rem, 1.1rem + 0.5vw, 1.5rem);
  line-height: 1.5;
}

/* Parallax: site.js sets --hero-scroll from 0 (top) to 1 (the intro's height). On smaller screens,
   where the intro stacks above the issues, its content rises faster than the page and fades. On
   desktop the intro is a sticky column and stays put. */
@media (max-width: 63.99em) and (prefers-reduced-motion: no-preference) {
  .series__content {
    translate: 0 calc(var(--hero-scroll, 0) * -6rem);
    opacity: calc(1 - var(--hero-scroll, 0) * 0.9);
  }
}

/* Positioned so it paints above the hero's fixed art. */
.series__issues {
  position: relative;
  z-index: 1;
  padding: 2rem max(var(--gutter), (100% - 72rem) / 2) 5rem;
  display: grid;
  gap: clamp(3rem, 8vw, 6rem);
}

.issue {
  display: grid;
  gap: 1.5rem 3rem;
  align-items: center;
}
.issue__cover {
  display: block;
  width: min(100%, 26rem);
}
.issue__body {
  max-width: 32rem;
}
.issue__title {
  margin: 0 0 1rem;
  display: grid;
  gap: 0.25rem;
}
.issue__number {
  font-size: 0.875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
.issue__name {
  font-size: clamp(1.5rem, 3vw, 2.25rem);
}
.issue__number:only-child {
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  letter-spacing: 0;
  text-transform: none;
  color: white;
}
.issue__body p {
  margin: 0 0 0.75rem;
}
.issue__meta {
  color: var(--muted);
  font-size: 0.9375rem;
}

@media (min-width: 40em) {
  .issue {
    grid-template-columns: minmax(12rem, 22rem) 1fr;
  }
}

/* Desktop: the intro is a sticky left column beside the issues. The first issue fills the first
   screen, centred and already open (build.mjs renders it .is-active); the rest open as they reach
   the middle. */
@media (min-width: 64em) {
  .series {
    display: grid;
    grid-template-columns: minmax(22rem, 36%) 1fr;
    align-items: start;
  }
  .series__intro {
    position: sticky;
    top: 0;
    height: 100svh;
    min-height: 0;
    align-items: center;
    padding-bottom: var(--header-height);
  }
  /* With the issue's min-height below, 3rem top and bottom centres the first issue in the screen. */
  .series__issues {
    padding-top: 3rem;
  }
  /* Each issue is as wide as its cover plus its text, centred in the column, so the open pair is
     balanced (and the reveal's "centre of the row" is the centre of the column). */
  .issue {
    grid-template-columns: min(24rem, 42%) 1fr;
    width: min(100%, 58rem);
    margin-inline: auto;
    min-height: calc(100svh - 6rem);
  }
}

/* Reveal (desktop), after the live guardiancomics.com's sliding covers: issues are closed by default
   in the stylesheet, with the cover and description both centred in the row and the text tucked
   behind the cover, so nothing animates on load. When site.js marks an issue .is-active (it reached
   the middle of the screen) the two columns slide apart into place, the text emerging from behind
   the cover; leaving slides them back together. Without JavaScript, /css/noscript.css opens them. */
@media (min-width: 64em) and (prefers-reduced-motion: no-preference) {
  .series__issues .issue {
    container-type: inline-size;
  }
  .series__issues .issue__cover {
    position: relative;
    z-index: 2;
    transition: transform 0.7s var(--ease-io);
  }
  .series__issues .issue__body {
    position: relative;
    z-index: 1;
    transition: transform 0.7s var(--ease-io), opacity 0.45s var(--ease-std);
  }
  /* Centre of the row for each column: half the row's width less half the column's own width,
     measured from where the column normally sits. */
  .series__issues .issue:not(.is-active) .issue__cover {
    transform: translateX(calc((100cqw - 100%) / 2));
  }
  .series__issues .issue:not(.is-active) .issue__body {
    transform: translateX(calc((100% - 100cqw) / 2));
    opacity: 0;
  }
  /* Text fades in once it starts to clear the cover, and out quickly as it tucks back behind. */
  .series__issues .issue.is-active .issue__body {
    transition-delay: 0s, 0.15s;
  }
}

/* Fine print (terms, privacy) & 404: a comfortable measure for reading, about 65 characters a
   line, with generous room above (clear of the header) and below (before the footer). */

.prose-page {
  max-width: calc(38rem + 2 * var(--gutter));
  margin-inline: auto;
  padding: calc(var(--header-height) + clamp(3rem, 8vw, 6rem)) var(--gutter) clamp(5rem, 12vw, 9rem);
}
.prose-page--center {
  min-height: 80svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.prose-page h1 {
  margin: 0;
  font-size: clamp(2rem, 5vw, 3.25rem);
}
.prose-page__header {
  margin-bottom: 2.5rem;
}
.prose {
  font-size: 1.125rem;
}
.prose h2 {
  margin: 2.5rem 0 0.75rem;
  font-size: 1.625rem;
}
.prose h3 {
  margin: 2rem 0 0.5rem;
  font-size: 1.25rem;
}
.prose p {
  margin: 0 0 1rem;
  color: rgb(229 239 245 / 0.88);
}
/* Links in running text: white and underlined, then the underline gives way to the accent colour on
   hover or focus. */
.prose a {
  color: white;
  text-decoration: underline;
  transition: color var(--timing) var(--ease-std);
}
.prose a:is(:hover, :focus-visible) {
  color: var(--primary-light);
  text-decoration: none;
}
.prose-page__cta {
  margin-top: 3rem;
}

/* Page transitions (see transitions.js): same-site navigations crossfade, and an issue's cover
   morphs into the reader and back. */

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.35s;
}
/* Quad ease-in-out. */
::view-transition-group(cover) {
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
/* The cover being left stays under the arriving one until the morph is nearly done, so there's no
   gap if the reader's page is still loading, then goes so its slightly different crop doesn't show
   at the edges. Replacing the default animations also drops their additive blend, which would wash
   the cover out while the two overlap. */
::view-transition-old(cover) {
  animation: view-fade-out 0.15s ease-in 0.35s both;
}
::view-transition-new(cover) {
  animation: view-fade-in 0.3s ease-out both;
}
@keyframes view-fade-in {
  from {
    opacity: 0;
  }
}
@keyframes view-fade-out {
  to {
    opacity: 0;
  }
}

/* The header carries across, its background, logo and menu button holding still between pages. */
::view-transition-group(header-bg),
::view-transition-group(header-logo),
::view-transition-group(header-menu) {
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
::view-transition-old(header-bg),
::view-transition-new(header-bg) {
  width: 100%;
  height: 100%;
}

/* Reader */

.page-reader {
  /* Half the site's side padding, so the reader's controls sit closer to the edges. */
  --gutter: clamp(1rem, 2vw, 1.25rem);
  overflow-x: hidden;
}
/* No fixed art to keep in, and the clip would cut off the reader's fixed bars. */
.page-reader .page > main {
  clip-path: none;
}
/* The header hides with the reader's controls while reading. */
.page-reader .site-header {
  transition: opacity 0.3s var(--ease-std), translate 0.3s var(--ease-std);
}
.page-reader:has(.reader.is-immersed:not(.is-peek-top)) .site-header:not(:has(:focus-visible)) {
  opacity: 0;
  translate: 0 -0.5rem;
  pointer-events: none;
}

/* The lockup: back to the series as a pretitle, over the issue's name in bold ("CLUTCHING AT
   STRAWS" / "Issue 3: Once upon a pirate"). It's first in the markup but sits at the bottom left,
   centred on the round buttons' row and stopping short of the buttons (--round-width: the buttons
   and their gap) and the panel dots (--frames-half, from reader.js). It hides and peeks with the
   bottom controls. */
.reader__bar {
  --round-width: calc(2 * 2.25rem + 0.5rem);
  position: fixed;
  left: calc(var(--gutter) + var(--row-inset));
  right: calc(var(--gutter) + var(--row-inset) + var(--round-width) + 1rem);
  /* Above the scrubber's row: the arrows' height, over the nav's bottom padding, plus a little room. */
  bottom: calc(max(0.25rem, env(safe-area-inset-bottom)) + 3.25rem);
  z-index: 11;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  height: var(--controls-row);
  min-width: 0;
  transition: opacity 0.3s var(--ease-std), transform 0.3s var(--ease-std);
}
/* Issues without captions have only the settings button. */
.reader:not(:has(.reader__cc)) .reader__bar {
  --round-width: 2.25rem;
}
.reader.has-frames .reader__bar {
  right: calc(50% + var(--frames-half, 0px) + 1rem);
}
/* On hover and keyboard focus, the pretitle slides right and a back chevron zooms in behind it. */
.reader__back {
  position: relative;
  display: flex;
  align-items: center;
  max-width: 100%;
  min-width: 0;
  text-decoration: none;
}
.reader__back .icon {
  display: none;
}
.reader__series {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: color 0.2s var(--ease-std);
}
.reader__back:is(:hover, :focus-visible) .reader__series {
  color: var(--light);
}
/* The issue's name. */
.reader__status {
  max-width: 100%;
  min-width: 0;
}
.reader__title {
  min-width: 0;
  margin: 0.125rem 0 0;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.2;
  color: white;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Without JavaScript there are no bottom controls, so the lockup sits at the top of the pages. */
.reader:has(> .reader__nav[hidden]) .reader__bar {
  position: static;
  height: auto;
  padding: calc(var(--header-inset) + 3.25rem + 1rem) var(--gutter) 0;
}
.reader:has(> .reader__nav[hidden]) .reader__pages {
  padding-top: 1rem;
}
/* The settings menu, opening upwards from its cog: sections, each with a title. */
.reader__menu {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  right: 0;
  z-index: 12;
  min-width: 12rem;
  padding: 0.4rem;
  border: 1px solid rgb(255 255 255 / 0.12);
  border-radius: 0.75rem;
  background: #1b1d22;
  box-shadow: 0 1rem 2.5rem rgb(0 0 0 / 0.55);
}
.reader__menu[hidden] {
  display: none;
}
.reader__menu-section + .reader__menu-section {
  margin-top: 0.4rem;
  padding-top: 0.4rem;
  border-top: 1px solid rgb(255 255 255 / 0.08);
}
.reader__menu-title {
  margin: 0;
  padding: 0.4rem 0.6rem 0.35rem;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
.reader__menu-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.55rem 0.6rem;
  border: 0;
  border-radius: 0.45rem;
  background: none;
  color: var(--muted);
  font-size: 0.9375rem;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
}
.reader__menu-item .icon {
  width: 1rem;
  height: 1rem;
}
.reader__menu-item span {
  flex: 1;
}
.reader__menu-item:hover,
.reader__menu-item:focus-visible {
  background: rgb(255 255 255 / 0.08);
  color: var(--light);
}
.reader__menu-item:focus-visible {
  outline: 2px solid var(--primary-light);
  outline-offset: -2px;
}
/* The current mode: in full colour, with a tick. */
.reader__menu-item[aria-checked="true"] {
  color: white;
}
.reader__menu-item .icon--check {
  visibility: hidden;
  color: var(--primary-light);
}
.reader__menu-item[aria-checked="true"] .icon--check {
  visibility: visible;
}
@media (max-width: 47.99em) {
  /* Small screens keep the series, but not the issue's name (the status stays, for its count). */
  .reader__title {
    display: none;
  }
}

/* Without JavaScript, and in scroll mode, pages stack vertically. */
.reader__pages {
  /* Clear of the fixed site header (its inset plus its 3.25rem bar), with 1rem to spare: the same
     gap a page lands at in scroll mode (HEADER_GAP in reader.js). */
  --reader-top: calc(var(--header-inset) + 3.25rem + 1rem);
  list-style: none;
  margin: 0;
  padding: var(--reader-top) 0 6rem;
  display: grid;
  gap: 1rem;
  justify-items: center;
}
.reader__page,
.reader__end {
  width: min(100%, 60rem);
}
.reader__figure {
  margin: 0;
}
.reader__figure img {
  width: 100%;
  height: auto;
  background: rgb(255 255 255 / 0.04);
  box-shadow: 0 0 0 1px rgb(255 255 255 / 0.15), 0 0 2.5rem 0 rgb(0 0 0 / 0.5);
}
.reader__end {
  padding: 4rem var(--gutter);
}
.end {
  max-width: 34rem;
  margin-inline: auto;
  text-align: center;
}
.end__title {
  margin: 0 0 1.5rem;
  font-size: clamp(1.5rem, 4vw, 2.25rem);
}
/* Focused by reader.js so screen readers announce the end of the issue. */
.end__title:focus {
  outline: none;
}
.end__next {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  margin: 2rem auto 0;
  width: fit-content;
  padding: 0.75rem 1.25rem 0.75rem 0.75rem;
  border-radius: var(--radius);
  background: rgb(255 255 255 / 0.06);
  color: white;
  text-align: start;
  text-decoration: none;
  transition: background-color var(--timing) var(--ease-std);
}
.end__next:hover {
  background: rgb(255 255 255 / 0.12);
}
.end__next img {
  width: 5.5rem;
  border-radius: 0.2rem;
  box-shadow: 0 0 1.25rem rgb(0 0 0 / 0.5);
}
.end__next strong {
  display: block;
  font-size: 1.125rem;
}
.end__next .eyebrow {
  display: block;
  margin-bottom: 0.25rem;
}
.end .actions {
  justify-content: center;
}

/* Paged mode (set by reader.js): one page at a time, fitted to the viewport with a little room above
   and below. The header, the lockup and the bottom controls lie over it and take no room from it. With
   captions on, the page also leaves room at the bottom for the caption strip, so a line sits under the
   page rather than over it (a caption that wraps grows up over the page). */
.reader {
  --page-pad: clamp(1rem, 4svh, 2.5rem);
  --caption-min: 4.25rem;
  --caption-bottom: max(1rem, env(safe-area-inset-bottom));
}
.reader.is-paged .reader__pages {
  display: block;
  padding: 0;
}
.reader.is-paged .reader__page,
.reader.is-paged .reader__end {
  display: none;
  width: 100%;
}
.reader.is-paged .reader__page.is-current {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100svh;
  padding: var(--page-pad) 0;
  cursor: default;
}
.reader.is-paged.has-captions .reader__page.is-current {
  padding-bottom: calc(var(--caption-bottom) + var(--caption-min) + 0.75rem);
}
.reader.is-paged .reader__end.is-current {
  display: flex;
  align-items: center;
  min-height: 100svh;
}
.reader.is-paged .reader__figure {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}
.reader.is-paged .reader__figure img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
}

/* Panels mode: reader.js scales and clips the page to one panel at a time. */
.reader.is-panels .reader__figure,
.reader.has-captions .reader__figure {
  position: relative;
}
.reader.is-panels .reader__figure img {
  box-shadow: none;
  will-change: transform, clip-path;
}
/* On the page in Panels mode, moving and clipping with it (reader.js): covers over the parts of later
   panels that reach into the current one, which fade in so a glide doesn't end with a sudden block,
   and with captions on, a ring round whoever is speaking, gliding from face to face within a panel
   and crossfading into another (reader.js). */
.reader__overlay {
  position: absolute;
  transform-origin: 0 0;
  pointer-events: none;
}
.reader__mask {
  position: absolute;
  background: var(--page);
  animation: mask-in 0.3s var(--ease-std) both;
}
@keyframes mask-in {
  from {
    opacity: 0;
  }
}
/* The speaker's ring: their colour between two fine dark keylines, so it reads on light and dark
   art alike, with a soft glow of their colour around it. Sizes are on-screen pixels (--px undoes
   the Panels zoom). */
.reader__face {
  --px: calc(1px / var(--panel-scale, 1));
  position: absolute;
  border: calc(3 * var(--px)) solid var(--speaker, white);
  border-radius: 50%;
  box-shadow:
    0 0 0 var(--px) rgb(0 0 0 / 0.55),
    inset 0 0 0 var(--px) rgb(0 0 0 / 0.55),
    0 0 calc(14 * var(--px)) calc(2 * var(--px)) color-mix(in srgb, var(--speaker, white) 55%, transparent);
  opacity: 0;
  transition:
    opacity 0.25s var(--ease-std),
    left 0.35s var(--ease-io),
    top 0.35s var(--ease-io),
    width 0.35s var(--ease-io),
    height 0.35s var(--ease-io),
    border-color 0.35s var(--ease-std),
    box-shadow 0.35s var(--ease-std);
}
.reader__face.is-on {
  opacity: 1;
}
/* Arriving in a panel, a ring ripples out once from it (a new ring each panel, reader.js; within a
   panel it glides instead). */
.reader__face::after {
  content: "";
  position: absolute;
  inset: calc(-3 * var(--px));
  border: calc(2 * var(--px)) solid var(--speaker, white);
  border-radius: 50%;
  opacity: 0;
}
.reader__face.is-on::after {
  animation: face-ripple 0.7s var(--ease-out) 0.1s;
}
@keyframes face-ripple {
  from {
    opacity: 0.8;
    scale: 1;
  }
  to {
    opacity: 0;
    scale: 1.35;
  }
}
/* Captions in Slides and Scroll (reader.js): the line's panel stands out, the rest of the page
   dimmed around it, gliding from panel to panel. */
.reader__spot {
  position: absolute;
  border-radius: 2px;
  box-shadow: 0 0 0 200vmax rgb(0 0 0 / 0.65);
  opacity: 0;
  transition:
    opacity 0.25s var(--ease-std),
    left 0.35s var(--ease-io),
    top 0.35s var(--ease-io),
    width 0.35s var(--ease-io),
    height 0.35s var(--ease-io);
}
.reader__spot.is-on {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .reader__mask {
    animation: none;
  }
  .reader__face,
  .reader__spot {
    transition: opacity 0.15s linear;
  }
  .reader__face.is-on::after {
    animation: none;
  }
}

/* Closed captions: a strip at the bottom of the screen with the speaker's face, their name in their
   colour, and the line. It stays put as the controls come and go, under them while they show. */
.reader__caption {
  position: fixed;
  left: 50%;
  bottom: var(--caption-bottom);
  z-index: 9; /* under the nav */
  translate: -50% 0;
  display: flex;
  align-items: center;
  gap: 0.85rem;
  width: max-content;
  min-width: min(22rem, 100% - 2 * var(--gutter));
  max-width: min(40rem, 100% - 2 * var(--gutter));
  min-height: var(--caption-min);
  padding: 0.65rem 1.25rem 0.65rem 0.65rem;
  border: 1px solid rgb(255 255 255 / 0.12);
  border-radius: 1.25rem;
  background: rgb(5 5 6 / 0.88);
  backdrop-filter: blur(12px);
  box-shadow: 0 0.75rem 2.5rem rgb(0 0 0 / 0.5);
}
.reader__caption[hidden] {
  display: none;
}
.reader__avatar {
  position: relative;
  flex: none;
  width: 2.95rem;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 50%;
  background: var(--dark);
  box-shadow: 0 0 0 2px var(--speaker, white);
}
.reader__avatar[hidden] {
  display: none;
}
.reader__avatar img {
  position: absolute;
  max-width: none;
  height: auto;
}
.reader__caption p {
  margin: 0;
  font-size: 1.0625rem;
  line-height: 1.4;
}
.reader__caption b {
  display: block;
  color: var(--speaker, var(--muted));
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.reader__caption b:empty {
  display: none;
}
.reader__caption[data-type="thought"] p > span,
.reader__caption[data-type="narration"] p > span {
  font-style: italic;
}
/* A wordless panel's scene: described, quietly, like a film's [door slams]. */
.reader__caption[data-type="scene"] p > span {
  font-style: italic;
  color: var(--muted);
}
.reader__caption[data-type="whisper"] p > span {
  color: var(--muted);
}
.reader__caption[data-type="shout"] p > span {
  font-weight: 700;
}
/* Sound effects read like a TV caption: [KRAK!] */
.reader__caption:has(.reader__avatar[hidden]) {
  padding-inline-start: 1.25rem;
}
.reader__caption[data-type="sfx"] p > span {
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.reader__caption[data-type="sfx"] p > span::before {
  content: "[";
}
.reader__caption[data-type="sfx"] p > span::after {
  content: "]";
}
@media (max-width: 47.99em) {
  .reader__caption {
    border-radius: 1rem;
  }
  .reader__caption p {
    font-size: 1rem;
  }
}

.reader.is-paged.is-zoomed .reader__page.is-current {
  display: block;
  height: auto;
  min-height: 100svh;
  padding-block: 0;
}
.reader.is-zoomed .reader__figure {
  height: auto;
}
.reader.is-zoomed .reader__figure img {
  max-height: none;
  width: min(100%, 75rem);
}

/* Controls faded out after a while or by clicking the page (reader.js); each edge's controls slide
   back in when the mouse reaches it (is-peek-top, is-peek-bottom) or one of them has keyboard focus. */
.reader.is-immersed:not(.is-peek-bottom) :is(.reader__bar, .reader__nav):not(:has(:focus-visible)) {
  opacity: 0;
  transform: translateY(0.5rem);
  pointer-events: none;
}
/* Ghost arrow hints: faint prev/next chevrons fixed at the viewport sides when controls are immersed.
   --ghost-base transitions smoothly when is-immersed changes (@property); --ghost-boost is set
   directly by reader.js on each pointermove for the proximity effect, with no transition so it
   tracks the pointer immediately. Multiplying base by the boost factor means base=0 (controls
   visible) always produces opacity 0, regardless of any leftover boost value. */
@property --ghost-base {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
.reader__ghost {
  position: fixed;
  top: 0;
  bottom: 0;
  z-index: 9;
  display: flex;
  align-items: center;
  /* The hit area: full height, and the outer half of the space beside the page (--ghost-zone, set by
     reader.js from the page on screen), never narrower than the chevron's own reach. */
  width: max(3.5rem, var(--ghost-zone, 0px));
  padding: 0 0.5rem;
  pointer-events: none;
  color: white;
  opacity: calc(var(--ghost-base) * (1 + var(--ghost-boost, 0) * 4.67));
  transition: --ghost-base 0.4s var(--ease-std);
  cursor: pointer;
}
.reader__ghost--prev { left: 0; justify-content: flex-start; }
.reader__ghost--next { right: 0; justify-content: flex-end; }
.reader__ghost .icon {
  display: block;
  width: 1.5rem;
  height: 1.5rem;
  transform: scale(calc(1 + var(--ghost-boost, 0) * 0.5));
}
.reader.is-immersed:not(:has([data-prev]:disabled)) .reader__ghost--prev,
.reader.is-immersed:not(:has([data-next]:disabled)) .reader__ghost--next {
  --ghost-base: 0.15;
  pointer-events: auto;
}
/* Bottom page nav: arrows either side of a segmented scrubber (from the app's ReaderNav), under a row
   with the panel dots in the middle and round buttons at the right, as on a video player, over the
   page. --nav-height is its height: the row and the scrubber, less a little of the fade above them. */
.reader {
  --controls-row: 2.5rem;
  --row-inset: 20px; /* the lockup and round buttons sit in from the arrows */
  --nav-height: calc(var(--controls-row) + 3.5rem);
}
.reader__nav {
  position: fixed;
  inset: auto 0 0;
  z-index: 10;
  display: grid;
  grid-template-columns: 2.75rem 1fr 2.75rem;
  grid-template-rows: var(--controls-row) auto;
  align-items: center;
  gap: 0 0.5rem;
  padding: 1.25rem var(--gutter) max(0.25rem, env(safe-area-inset-bottom));
  background: linear-gradient(to top, rgb(0 0 0 / 1) 0%, rgb(0 0 0 / 0.8) 45%, rgb(0 0 0 / 0));
  transition: opacity 0.3s var(--ease-std), transform 0.3s var(--ease-std);
}
.reader__nav[hidden] {
  display: none;
}
/* The panel dots and the buttons share a row above the arrows and scrubber. */
.reader__nav > :is(.reader__arrow, .reader__scrub) {
  grid-row: 2;
}
.reader__controls {
  grid-row: 1;
  grid-column: 1 / -1;
  justify-self: end;
  display: flex;
  gap: 0.5rem;
  margin-inline-end: var(--row-inset);
}
.reader__round {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid rgb(255 255 255 / 0.18);
  border-radius: 50%;
  background: rgb(255 255 255 / 0.08);
  backdrop-filter: blur(8px);
  cursor: pointer;
  transition: transform var(--timing) var(--ease-std), background-color var(--timing) var(--ease-std), color var(--timing) var(--ease-std), border-color var(--timing) var(--ease-std);
}
.reader__round:hover {
  transform: scale(1.08);
  background: rgb(255 255 255 / 0.16);
}
.reader__round .icon {
  width: 1.25rem;
  height: 1.25rem;
}
/* Captions on: a white button. */
.reader__cc[aria-pressed="true"] {
  border-color: white;
  background: white;
  color: black;
}
.reader__settings {
  position: relative;
}
/* The cog turns a little as its menu opens. */
.reader__settings-toggle .icon {
  transition: rotate 0.4s var(--ease-io);
}
.reader__settings-toggle[aria-expanded="true"] {
  background: rgb(255 255 255 / 0.16);
}
.reader__settings-toggle[aria-expanded="true"] .icon {
  rotate: 60deg;
}
.reader__arrow {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
  transition: transform var(--timing) var(--ease-std), background-color var(--timing) var(--ease-std), opacity var(--timing) var(--ease-std);
}
.reader__arrow:hover:not(:disabled) {
  transform: scale(1.1);
  background: rgb(255 255 255 / 0.08);
}
.reader__arrow:disabled {
  opacity: 0.35;
  cursor: default;
}
.reader__scrub {
  display: flex;
  gap: 0.25rem;
  min-width: 0;
}
.reader__scrub li {
  flex: 1 1 0;
  min-width: 0;
}
.reader__scrub a {
  display: flex;
  align-items: center;
  height: 2.75rem;
}
.reader__scrub a::before {
  content: "";
  width: 100%;
  height: 0.1875rem;
  border-radius: 1px;
  background: var(--grey);
  transition: background-color var(--timing) var(--ease-std), height var(--timing) var(--ease-std);
}
.reader__scrub a.is-read::before {
  background: var(--grey-dark);
}
.reader__scrub a.is-current::before {
  background: white;
}
.reader__scrub a:hover::before,
.reader__scrub a:focus-visible::before {
  height: 0.375rem;
  background: white;
}
.reader__scrub a:focus-visible {
  outline-offset: -2px;
}

/* Panels mode, on pages with more than one panel: a dot for each panel between the panel and the
   scrubber, in the buttons' row. The line into each dot fills as the reader reaches it, and the dot
   lights once the line arrives; going back, the dot goes out first and the line drains. */
.reader {
  --frames-row: 1.75rem;
}
.reader__frames {
  grid-row: 1;
  grid-column: 2;
  justify-self: center;
  display: flex;
  align-items: center;
  height: var(--frames-row);
  margin: 0;
  padding: 0;
  list-style: none;
}
.reader__frames[hidden] {
  display: none;
}
.reader__frames li {
  position: relative;
  display: flex;
  align-items: center;
}
/* The line in from the previous dot: a grey track, with a white fill that grows along it. */
.reader__frames li + li::before,
.reader__frames li + li::after {
  content: "";
  width: 1.25rem;
  height: 1px;
  background: var(--grey);
}
.reader__frames li + li::after {
  position: absolute;
  left: 0;
  background: white;
  transform: scaleX(0);
  transform-origin: left;
  /* Timed per step by reader.js, so a jump of several panels sweeps along the row a line at a time. */
  transition: transform var(--line-duration, 0.45s) var(--line-ease, var(--ease-io)) var(--line-delay, 0s);
}
.reader__frames li + li.is-reached::after {
  transform: none;
}
.reader__frames button {
  display: grid;
  place-items: center;
  width: 1.25rem;
  height: var(--frames-row);
  margin-inline: -0.25rem; /* a bigger target than the dot, tucked under the lines */
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.reader__frames button::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--grey);
  transition: background-color 0.15s var(--ease-std), transform 0.25s var(--ease-std), box-shadow 0.25s var(--ease-std);
  /* Lights (or goes out) as the sweep passes it (reader.js). */
  transition-delay: var(--dot-delay, 0s);
}
.reader__frames li.is-reached button::before {
  background: white;
}
.reader__frames li.is-current button::before {
  transform: scale(1.4);
  box-shadow: 0 0 0 3px rgb(255 255 255 / 0.18);
}
.reader__frames button:hover::before,
.reader__frames button:focus-visible::before {
  transform: scale(1.4);
}
@media (prefers-reduced-motion: reduce) {
  .reader__frames li + li::after,
  .reader__frames button::before {
    transition: none !important;
  }
}

.reader__peek {
  --peek-radius: 0.25rem; /* the page's corners */
  /* Fixed beside the nav rather than inside it, above the issue's name (z-index 11), and over the
     buttons' row so its notch points straight at the scrubber: the nav's bottom padding, half the
     scrubber's row (1.375rem), half the hovered segment, the notch and a little room. */
  position: fixed;
  bottom: calc(2.75rem + max(0.25rem, env(safe-area-inset-bottom)));
  left: 0;
  z-index: 12;
  padding: 0.5rem 0.5rem 0.4rem;
  /* Concentric with the page's corners: their radius plus the padding around the page. */
  border-radius: calc(var(--peek-radius) + 0.5rem);
  background: var(--grey);
  box-shadow: 0 0.5rem 2rem rgb(0 0 0 / 0.5);
  text-align: center;
  font-size: 0.8125rem;
  font-weight: 700;
  pointer-events: none;
  /* Positioned, centred and swung by GSAP in reader.js, pivoting from below like a pendulum
     hung from the scrubber. */
  transform-origin: 50% 135%;
  will-change: transform;
}
.reader__peek[hidden] {
  display: none;
}
.reader__peek::after {
  content: "";
  position: absolute;
  top: 100%;
  /* Off centre when the preview is held in from the bar's end (--notch-x, set by reader.js). */
  left: calc(50% + var(--notch-x, 0px));
  border: 0.3125rem solid transparent;
  border-top-color: var(--grey);
  transform: translateX(-50%);
}
.reader__peek img {
  width: auto;
  height: 10rem;
  max-width: 14rem;
  margin-bottom: 0.35rem;
  border-radius: var(--peek-radius);
  background: var(--black);
}

.reader__toast {
  position: fixed;
  left: 50%;
  bottom: 5rem;
  z-index: 11;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.6rem 0.6rem 0.6rem 1rem;
  border-radius: var(--radius);
  background: var(--whisper);
  color: var(--black);
  font-size: 0.9375rem;
  box-shadow: 0 0.5rem 2rem rgb(0 0 0 / 0.5);
  transform: translateX(-50%);
  width: max-content;
  max-width: calc(100vw - 2rem);
  animation: toast-in 0.3s var(--ease-std);
}
.reader__toast button {
  padding: 0.4rem 0.75rem;
  border: 0;
  border-radius: 0.2rem;
  background: var(--black);
  color: white;
  font-weight: 700;
  cursor: pointer;
}
@keyframes toast-in {
  from {
    opacity: 0;
    transform: translate(-50%, 0.5rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}
