/*
  Shared structure. Every colour, measure, font and grid count comes from a token the
  active theme defines; this file names no colour and no font family of its own, which is
  what makes a theme a directory rather than a fork.

  The layout rule the design follows is "gallery": the page is a room and the artwork is
  the only thing in it with colour. In practice that means the chrome here is almost
  entirely negative space and type -- no card backgrounds behind thumbnails, no mat around
  the strip, no filled buttons except the one element that is genuinely current. Anything
  that draws a box competes with the panel borders the art already has.

  Mobile-first throughout. The desktop view is the same layout with more air and a larger
  type scale -- not a second layout.
*/

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--font-size-base);
  line-height: var(--line-height);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

@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;
  }
}

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

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 0.2em; }
a:focus-visible, button:focus-visible, input:focus-visible, summary:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}

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

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--accent); color: var(--accent-ink);
  padding: 0.75rem 1rem;
}
.skip-link:focus { left: 0; }

/* --- containers ----------------------------------------------------------- */
/*
  Two widths, deliberately distinct: `--shell` is the room (masthead, footer, index grids)
  and `--measure` is the reading column. Keeping them separate is what lets the header line
  up with a wide thumbnail grid while prose underneath stays at a readable line length.
*/
.masthead__bar, .tagline, .masthead nav, .site-footer nav, .colophon,
.archive-grid, .cast-grid, .pagination {
  width: min(100% - (2 * var(--gutter)), var(--shell));
  margin-inline: auto;
}

/* ONE column at every width. `--measure` clamps prose; the strip is exempt below. */
.prose, .strip-meta, .strip-nav, .taxonomy, .transcript, .arrangements,
.archive-filter, .strip-title, .strip__caption {
  width: min(100% - (2 * var(--gutter)), var(--measure));
  margin-inline: auto;
}

.prose > * + * { margin-block-start: 1rem; }
.prose h1, .prose h2, .prose h3 {
  font-family: var(--font-display); line-height: 1.15; letter-spacing: -0.02em;
}
.prose h1 { font-size: calc(var(--font-size-display) * 1.35); margin-block-end: 0.25rem; }
.prose h2 { font-size: calc(var(--font-size-display) * 0.95); margin-block-start: 2rem; }
.prose h3 { font-size: var(--font-size-base); text-transform: uppercase; letter-spacing: 0.1em; }

/*
  The opening paragraph of a prose page is set larger. Content pages are short and mostly
  one voice, so a lead is the cheapest way to give them a shape -- without it every page is
  an undifferentiated block starting immediately under the heading.
*/
.prose > p:first-of-type { font-size: 1.1rem; line-height: 1.5; color: var(--ink); }
.prose p, .prose li { color: var(--ink-muted); }
.prose strong { color: var(--ink); font-weight: 650; }
.prose ul, .prose ol { padding-inline-start: 1.25rem; }
.prose li + li { margin-block-start: 0.4rem; }
.prose blockquote {
  margin-inline: 0; padding-inline-start: 1rem;
  border-inline-start: 2px solid var(--rule);
  color: var(--ink-muted); font-style: italic;
}
.prose code {
  font-size: 0.9em; padding: 0.1em 0.35em;
  background: var(--bg-raised); border-radius: var(--radius);
}
.prose pre { overflow-x: auto; padding: 1rem; background: var(--bg-raised); border-radius: var(--radius); }
.prose pre code { padding: 0; background: none; }
.prose hr { border: 0; border-block-start: 1px solid var(--rule); margin-block: 2rem; }

/* --- masthead ------------------------------------------------------------- */
/*
  The bar is sticky; the tagline below it is not. The tagline still has to sit above the
  strip and inside the initial viewport -- that is a binding obligation with a test -- but
  it does not need to follow the reader down four panels.
*/
.masthead__bar {
  position: sticky; top: 0; z-index: 60;
  background: var(--bg);
  padding-block: 0.6rem;
  border-block-end: 1px solid var(--rule);
}
.masthead__controls { display: flex; align-items: center; gap: 0.25rem; }

.menu-button {
  display: inline-flex; align-items: center; gap: 0.45rem;
  min-height: var(--tap-target); padding-inline: 0.6rem;
  border: 0; background: none; color: var(--ink);
  font: inherit; font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer;
}
.menu-button__bars, .menu-button__bars::before, .menu-button__bars::after {
  display: block; width: 18px; height: 2px; background: currentColor; content: "";
}
.menu-button__bars { position: relative; }
.menu-button__bars::before { position: absolute; top: -6px; }
.menu-button__bars::after { position: absolute; top: 6px; }

/* One menu at a time: the inline nav is the wide-screen form of the same data. */
@media (min-width: 48rem) { .menu-button { display: none; } }

.sheet {
  position: fixed; inset: 0; z-index: 200;
  background: rgb(0 0 0 / 0.55);
  display: flex; justify-content: flex-end;
}
.sheet[hidden] { display: none; }
body.has-sheet {  }
.sheet__panel {
  width: min(20rem, 86vw); height: 100%;
  background: var(--bg); border-inline-start: 1px solid var(--rule);
  padding: var(--gutter);
  padding-block-start: max(env(safe-area-inset-top), var(--gutter));
  display: flex; flex-direction: column; gap: var(--stack);
  overflow-y: auto;
}
.sheet__head { display: flex; align-items: center; justify-content: space-between; }
.sheet__title {
  font-family: var(--font-display); font-weight: 800;
  text-transform: uppercase; letter-spacing: 0.02em;
}
.sheet__close {
  min-width: var(--tap-target); min-height: var(--tap-target);
  border: 0; background: none; color: var(--ink-muted);
  font-size: 1.75rem; line-height: 1; cursor: pointer;
}
.sheet__nav ul { list-style: none; margin: 0; padding: 0; }
.sheet__nav a {
  display: flex; align-items: center;
  min-height: 52px;
  border-block-end: 1px solid var(--rule);
  color: var(--ink); text-decoration: none;
  font-size: 1.15rem;
}
.sheet__nav a:hover { color: var(--accent); }
.sheet__themes { margin-block-start: auto; padding-block-start: var(--stack); }
.sheet__themes-label {
  font-size: 0.65rem; font-weight: 700; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-muted);
}
.sheet__themes ul {
  display: flex; gap: 0.5rem; flex-wrap: wrap;
  list-style: none; margin: 0.5rem 0 0; padding: 0;
}
.sheet__theme {
  display: inline-flex; align-items: center;
  min-height: var(--tap-target); padding-inline: 0.85rem;
  border: 1px solid var(--rule); border-radius: var(--radius);
  color: var(--ink-muted); text-decoration: none;
  font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase;
}
.sheet__theme.is-current { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }

.sheet__tagline { margin: var(--stack) 0 0; color: var(--ink-muted); font-size: 0.85rem; }

.masthead {
  padding-block: 0 0.5rem;
  /* A visible end to the site furniture. Without it the masthead and the page's own
     controls ran together into one undifferentiated column of text. */
  border-block-end: 1px solid var(--rule);
  margin-block-end: var(--stack);
}
.masthead__bar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--tap-gap);
}
/*
  The lockup: the balloon mark and the wordmark, set on one baseline. The mark is the same
  geometry as the favicon and inherits ink via currentColor, so the tab icon and the
  masthead are recognisably one thing in both palettes.
*/
.masthead__lockup {
  display: inline-flex; align-items: center; gap: 0.5rem;
  color: var(--ink); text-decoration: none;
  min-height: var(--tap-target);
}
.mark {
  width: 1.15em; height: 1.15em; flex: 0 0 auto;
  /* Optically centred against the caps: the tail hangs below the balloon body, so
     geometric centring sits the mark visibly low next to uppercase letterforms. */
  transform: translateY(-0.06em);
}

/*
  The wordmark carries the identity, so it is the one place type is allowed to be loud:
  heavy, uppercase, tightly tracked. Everything else is quiet by comparison, which is what
  makes the page read as deliberate rather than as a default document.
*/
.masthead__title {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink); text-decoration: none;
  line-height: 1;
}

/*
  A binding obligation from creation, and tested for computed size, contrast and position:
  it must sit above the strip, inside the initial viewport, at a size a reader actually
  reads. Quiet is allowed; sr-only or 10px grey is not.
*/
.tagline {
  margin: 0.6rem auto 0;
  font-size: 0.95rem;
  line-height: 1.4;
  color: var(--ink-muted);
}

/*
  Navigation is unboxed, but it is NOT small-caps like the rest of the chrome.

  It was, and the result was four near-identical rows stacked down the top of every strip
  page -- site nav, the date line, the arrangement switcher and the strip controls, all the
  same size, weight and colour. Every one of them read as decoration and none of them read
  as a menu. The operator's question was literally "where is the menu": it was the first of
  the four.

  So the site's own navigation is set at reading size in full-strength ink, and everything
  that is a page-level CONTROL stays small-caps and muted. One row of small caps is a
  system; four is soup.
*/
.masthead nav ul, .site-footer nav ul {
  display: flex; flex-wrap: wrap; gap: 1.75rem;
  list-style: none; margin: 0; padding: 0;
}
.masthead nav { margin-block-start: 0.5rem; }
/* On a phone the sheet is the menu; showing both would be the row-soup this replaced. */
@media (max-width: 47.99rem) { .masthead nav { display: none; } }
.masthead nav a {
  display: inline-flex; align-items: center;
  min-height: var(--tap-target);
  font-size: 1rem; font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink); text-decoration: none;
}
.masthead nav a:hover { color: var(--accent); }

/* The footer is a colophon, not a second menu, so it stays quiet. */
.site-footer nav a {
  display: inline-flex; align-items: center;
  min-height: var(--tap-target);
  font-size: 0.75rem; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-muted); text-decoration: none;
}
.site-footer nav a:hover { color: var(--ink); }

.theme-toggle {
  min-width: var(--tap-target); min-height: var(--tap-target);
  margin-inline-end: -0.6rem;
  border: 0; background: none; color: var(--ink-muted);
  font-size: 1.1rem; cursor: pointer; line-height: 1;
}
.theme-toggle:hover { color: var(--ink); }

/* --- the strip ------------------------------------------------------------ */
/*
  EXEMPT from the prose measure, and given no frame of its own. The artwork already carries
  its own white margin and panel borders; a mat, a radius or a shadow around that is a
  second frame around a frame.

  It takes min(viewport, its own master width) -- the cap is an inline custom property per
  strip, because master widths differ and always will.
*/
.strip, .carousel {
  width: min(100%, var(--strip-max, 100%));
  margin: calc(var(--stack) * 1.1) auto;
  padding: 0;
  background: var(--strip-mat);
}
.strip__zoom {
  display: block; width: 100%; padding: 0; border: 0; margin: 0;
  background: none; cursor: zoom-in;
}
.strip__picture img, .carousel__picture img { width: 100%; height: auto; }

.carousel { list-style: none; display: grid; gap: var(--stack); }

.strip-title {
  margin: 0 auto;
  font-family: var(--font-display);
  font-size: var(--font-size-display);
  font-weight: 700; letter-spacing: -0.01em; line-height: 1.2;
}

/*
  The caption is the SECOND joke, in the xkcd title-text tradition -- not metadata. It had
  been styled like every other small-caps row below the strip, which made the punchline look
  like a form label. Now the closed state is a quiet centred invitation and the open state is
  the line itself, set larger and in full-strength ink, so the reveal actually feels like
  being told something.
*/
.strip__caption { margin: var(--stack) auto 0; }
.caption-reveal {
  display: block; width: 100%;
  min-height: var(--tap-target);
  padding: 0.9rem 1rem;
  border: 0; border-radius: var(--radius);
  background: var(--bg-raised); color: var(--ink-muted);
  font: inherit; text-align: center; cursor: pointer;
}
.caption-reveal__hint {
  letter-spacing: 0.14em; text-transform: uppercase; font-size: 0.68rem; font-weight: 600;
}
.caption-reveal__text {
  display: none; color: var(--ink);
  font-size: 1.05rem; font-style: italic; line-height: 1.45;
}
.caption-reveal[aria-expanded="true"] { background: none; cursor: default; }
.caption-reveal[aria-expanded="true"] .caption-reveal__hint { display: none; }
.caption-reveal[aria-expanded="true"] .caption-reveal__text { display: block; }

/* --- arrangement switcher -------------------------------------------------- */
.arrangements__list {
  display: flex; gap: 1.25rem; list-style: none; margin: 0.75rem auto 0; padding: 0;
  flex-wrap: wrap;
}
.arrangements__item {
  display: inline-flex; align-items: center; min-height: var(--tap-target);
  font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-muted); text-decoration: none;
  border-block-end: 2px solid transparent;
}
/* Roughly the only marked element on the page. */
.arrangements__item.is-current { color: var(--ink); border-block-end-color: var(--accent); }

/* --- home ------------------------------------------------------------------ */
/*
  The landing page adds arrival to the permalink: a first-time reader who followed someone
  else's link needs to learn what this is and be given somewhere to go, without scrolling
  past the joke to find out. Hence the eyebrow above and the welcome below -- the comic
  still comes first, because the joke is the pitch.
*/
.eyebrow {
  width: min(100% - (2 * var(--gutter)), var(--measure));
  margin: 0 auto 0.4rem;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline;
  font-size: 0.68rem; font-weight: 700;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-muted);
}
.eyebrow__now { color: var(--accent); }
.eyebrow__number { font-variant-numeric: tabular-nums; }
.eyebrow__sep { opacity: 0.5; }

.permalink-line {
  width: min(100% - (2 * var(--gutter)), var(--measure));
  margin: 0 auto;
}

.welcome {
  margin-block: calc(var(--stack) * 1.5);
  padding-block: var(--stack);
  border-block: 1px solid var(--rule);
}
.welcome__pitch { font-size: 1.05rem; color: var(--ink); }
.welcome__start { margin-block-start: 1rem; }

/* The only filled control on the site. It is the one thing a new reader should do next. */
.button {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap-target); padding-inline: 1.25rem;
  background: var(--accent); color: var(--accent-ink);
  border-radius: var(--radius); text-decoration: none;
  font-size: 0.75rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase;
}

.recent { margin-block: calc(var(--stack) * 1.5); }
.recent__label {
  font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-muted);
}

/* --- strip meta and navigation --------------------------------------------- */
.strip-meta {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--tap-gap); flex-wrap: wrap;
  border-block-end: 1px solid var(--rule);
}
.permalink {
  display: inline-flex; align-items: center; gap: 0.6rem;
  min-height: var(--tap-target);
  color: var(--ink-muted); text-decoration: none; font-size: 0.85rem;
}
.permalink__number {
  font-variant-numeric: tabular-nums; font-weight: 700; color: var(--ink);
  letter-spacing: 0.04em;
}
.share {
  min-height: var(--tap-target); padding: 0;
  border: 0; background: none; color: var(--ink-muted);
  font: inherit; font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer;
}
.share:hover { color: var(--ink); }

/*
  Thumb-zone controls: >=48x48 targets with >=8px gaps, Prev and Next at the edges. Text
  links with middots across a 390px viewport fail the tap-target audit the budget demands.
  Unboxed, for the same reason the nav is.
*/
.strip-nav {
  display: flex; gap: var(--tap-gap); justify-content: space-between; align-items: center;
  margin-block: 0.5rem var(--stack);
}
.strip-nav__button {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--tap-target); min-height: var(--tap-target);
  padding-inline: 0.35rem;
  border: 0;
  color: var(--ink); text-decoration: none;
  font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
}
.strip-nav__button:hover { color: var(--accent); }
.strip-nav__button.is-disabled { color: var(--ink-muted); opacity: 0.35; }

.taxonomy { margin-block: var(--stack); }
.taxonomy__list {
  display: flex; flex-wrap: wrap; gap: 1rem; list-style: none; padding: 0; margin: 0;
}
.taxonomy__list a {
  display: inline-flex; align-items: center; min-height: var(--tap-target);
  color: var(--ink-muted); text-decoration: none;
  font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase;
}
.taxonomy__list a:hover { color: var(--accent); }
.taxonomy__list--cast a { color: var(--ink); }

.transcript { margin-block: var(--stack); border-block-start: 1px solid var(--rule); }
.transcript summary {
  min-height: var(--tap-target); display: flex; align-items: center; cursor: pointer;
  color: var(--ink-muted);
  font-size: 0.72rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase;
}
.transcript__body {
  white-space: pre-wrap; color: var(--ink-muted);
  font-size: 0.95rem; padding-block-end: var(--stack);
}

/*
  The cast line: faces, not another row of uppercase links. A reader recognises Luci by her
  face long before they read her name, and the portraits already exist for /cast/.
*/
.cast-line {
  display: flex; flex-wrap: wrap; gap: 1rem;
  list-style: none; padding: 0; margin: 0 0 1rem;
}
.cast-chip {
  display: inline-flex; align-items: center; gap: 0.55rem;
  min-height: var(--tap-target);
  color: var(--ink); text-decoration: none;
}
.cast-chip__face {
  width: 34px; height: 34px; flex: 0 0 34px;
  border-radius: 50%; 
}
.cast-chip__face img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 30%; }
.cast-chip__face--none {
  display: grid; place-items: center;
  background: var(--bg-raised); color: var(--ink-muted);
  font-size: 0.85rem; font-weight: 700;
}
.cast-chip__name { font-size: 0.9rem; }
.cast-chip:hover .cast-chip__name { color: var(--accent); }

/* --- index grids ----------------------------------------------------------- */
/*
  The one-column law scopes to the reading column; index pages are grids and their column
  counts are tokens. Cards carry no background: prints on a wall, not tiles in a dashboard.
*/
.archive-grid, .cast-grid {
  margin-block: var(--stack);
  display: grid; gap: var(--stack) var(--gutter);
  list-style: none; padding: 0;
  grid-template-columns: repeat(var(--archive-columns), minmax(0, 1fr));
}
.cast-grid { grid-template-columns: repeat(var(--cast-columns), minmax(0, 1fr)); }

.archive-card, .cast-card {
  display: flex; flex-direction: column; gap: 0.5rem;
  min-height: var(--tap-target);
  color: var(--ink); text-decoration: none;
}
.archive-card__picture img { width: 100%; }
.archive-card__number, .cast-card__role {
  font-variant-numeric: tabular-nums; color: var(--ink-muted);
  font-size: 0.68rem; letter-spacing: 0.12em; text-transform: uppercase;
}
.archive-card__title, .cast-card__name { font-size: 0.95rem; line-height: 1.3; }
.archive-card:hover .archive-card__title { color: var(--accent); }
.archive-card--retracted { opacity: 0.45; }
.archive-card__note { font-size: 0.8rem; color: var(--ink-muted); }

/*
  Cast cards lead with the face. The portraits are head-and-shoulders crops of the
  character reference sheets, so they share a frame and a scale and read as one set --
  which is the only reason a grid of twelve does not look like twelve unrelated pictures.
*/
.cast-group { margin-block: calc(var(--stack) * 1.5); }
.cast-group__label {
  font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-muted);
  padding-block-end: 0.5rem; border-block-end: 1px solid var(--rule);
}
.cast-group__note { font-size: 0.9rem; color: var(--ink-muted); max-width: 34rem; }

.cast-card { gap: 0.4rem; }
.cast-card__portrait { border-radius: var(--radius);  }
/*
  Belt as well as braces: the portraits are already normalised to one size at authoring
  time, and this keeps one odd master from making a row ragged if a future one is not.
*/
.cast-card__portrait img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; }
/* A character with no art yet gets an initial, not a broken frame or a placeholder face. */
.cast-card__portrait--none {
  display: grid; place-items: center;
  aspect-ratio: 4 / 5;
  background: var(--bg-raised); color: var(--ink-muted);
  font-family: var(--font-display); font-size: 2rem; font-weight: 700;
}
.cast-card__name { font-weight: 600; }
.cast-card:hover .cast-card__name { color: var(--accent); }

.cast-bio__portrait {
  width: 180px; border-radius: var(--radius); 
  margin-block-end: var(--stack);
}
.cast-role {
  font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-muted);
}

.archive-filter { margin-block: var(--stack); }
.archive-filter input {
  width: 100%; min-height: var(--tap-target); padding: 0 0 0.5rem;
  border: 0; border-block-end: 1px solid var(--rule);
  background: none; color: var(--ink); font: inherit;
}
.archive-filter input::placeholder { color: var(--ink-muted); }

/*
  Thumb-zone controls, and three states that have to LOOK like three states. Rendered
  identically -- which they were until this was looked at -- the reader cannot tell which
  page they are on or which way they can still go: `aria-current` and `aria-disabled` are
  announced to a screen reader and invisible to everyone else, which is precisely the
  failure of conveying state through one channel.

  The current page is the one genuinely-current element on the page, so it takes the
  accent fill the gallery direction reserves for exactly that. Disabled edges drop to the
  faintest ink the palette has and take no pointer.
*/
.pagination {
  display: flex; gap: var(--tap-gap); justify-content: center; margin-block: var(--stack);
  flex-wrap: wrap;
}
.pagination a, .pagination span {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--tap-target); min-height: var(--tap-target);
  padding-inline: 0.5rem; border-radius: var(--radius);
  text-decoration: none; color: var(--ink-muted);
}
.pagination a:hover { color: var(--ink); }
.pagination [aria-current="page"] {
  background: var(--accent); color: var(--accent-ink); font-weight: 650;
}
.pagination [aria-disabled="true"] { color: var(--rule); cursor: default; }

/* --- ad slots -------------------------------------------------------------- */
/*
  Space is reserved by aspect-ratio whether or not a slot is filled, so turning advertising
  on never shifts layout. A slot with nothing in it renders nothing and occupies nothing.
  Overflow is clipped because the box's size is the page's promise: a creative taller than
  the slot it bought must not reflow every page it lands on — that is the layout shift the
  reservation exists to prevent, measured in test_ads.py.
*/
.ad-slot { margin: var(--stack) auto; width: min(100% - (2 * var(--gutter)), var(--measure)); }
.ad-slot[data-reserved] {
  aspect-ratio: var(--ad-aspect, 4 / 1); background: var(--bg-raised); overflow: hidden; 
}

/* --- footer and badge ------------------------------------------------------ */
.site-footer {
  margin-block-start: calc(var(--stack) * 2);
  padding-block: var(--stack);
  border-block-start: 1px solid var(--rule);
}
.colophon { margin: 1rem auto 0; color: var(--ink-muted); font-size: 0.8rem; }
.colophon--credit { margin-block-start: 0.25rem; }

.env-badge {
  display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center;
  padding: 0.4rem 0.75rem;
  background: var(--bg-raised); color: var(--ink-muted);
  font-size: 0.65rem; letter-spacing: 0.08em; text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}
.env-badge--public { background: #b3001b; color: #fff; font-size: 0.85rem; letter-spacing: 0.1em; }

/* --- lightbox -------------------------------------------------------------- */
.lightbox {
  position: fixed; inset: 0; z-index: 200;
  background: rgb(0 0 0 / 0.94);
  display: grid; place-items: center;
  overflow: auto; overscroll-behavior: contain;
  touch-action: pinch-zoom;
}
.lightbox[hidden] { display: none; }
.lightbox__figure {
  margin: 0; padding: var(--gutter) 0;
  display: flex; flex-direction: column; align-items: center; gap: 0.75rem;
}
/*
  Fits the viewport on open. The zoom is still the point -- the source is the native-cap
  derivative, so pinching reveals real detail the page could never show, because browsers
  fix srcset at layout time and do not re-fetch on visual-viewport zoom. But opening at
  native size would drop the reader into the corner of a 1000px page with no idea where
  they are.
*/
.lightbox img {
  max-width: min(100vw, 100%);
  height: auto;
  cursor: zoom-out;
}
.lightbox__hint {
  color: rgb(255 255 255 / 0.55);
  font-size: 0.65rem; letter-spacing: 0.16em; text-transform: uppercase;
  padding-block-end: env(safe-area-inset-bottom, 0);
}
.lightbox__close {
  position: fixed;
  inset-block-start: max(env(safe-area-inset-top), 0.5rem); inset-inline-end: 0.5rem;
  min-width: var(--tap-target); min-height: var(--tap-target);
  border: 0; border-radius: 50%;
  background: rgb(255 255 255 / 0.9); color: #000; font-size: 1.5rem; cursor: pointer;
}

/* --- the coming-soon holding page ------------------------------------------- */
/*
  Centred in the viewport rather than parked at the top of an empty column. There is nothing
  below it to scroll to, and a short block of text pinned to the top of a tall screen reads
  as a page whose rest failed to load -- which is the one impression a holding page cannot
  afford to give.
*/
.coming-soon {
  /*
    Anchored to the top, not centred. Centring inside a 58vh box put roughly a hundred
    pixels of dead space between the tagline and the headline on a phone, which read as a
    page that had failed to load its own top half -- the one impression a holding page
    cannot afford. The min-height still reserves the column so the footer does not ride up
    under three lines of text on a tall screen; it just no longer buys that by pushing the
    headline down to the middle to find it.
  */
  min-height: 46vh;
  display: flex; flex-direction: column; justify-content: flex-start;
  margin-block: calc(var(--stack) * 0.5) calc(var(--stack) * 2);
}
/*
  Selector doubled deliberately. `.prose p` is (0,1,1) and out-specifies a single class, so
  the bare `.coming-soon__closing` lost its colour to the muted body rule and the page's one
  piece of punctuation rendered as grey body text. Caught by looking at it.
*/
.coming-soon .coming-soon__closing {
  margin-block-start: calc(var(--stack) * 0.75);
  font-size: 0.78rem; font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--accent);
}
.coming-soon__signup {
  display: flex; flex-wrap: wrap; gap: 0.5rem;
  margin-block-start: var(--stack);
}
.coming-soon__signup input {
  flex: 1 1 14rem;
  min-height: var(--tap-target); padding-inline: 0.75rem;
  border: 1px solid var(--rule); border-radius: var(--radius);
  background: var(--bg-raised); color: var(--ink);
  /* 1rem is a floor, not a taste: iOS Safari zooms the whole page on focusing an input
     under 16px, and the reader lands mid-form on a page they now have to pinch back out. */
  font: inherit; font-size: 1rem;
}
.coming-soon__signup button {
  min-height: var(--tap-target); padding-inline: 1.25rem;
  border: 0; border-radius: var(--radius);
  background: var(--accent); color: var(--accent-ink);
  font-size: 0.75rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase;
  cursor: pointer;
}
.coming-soon__feed {
  margin-block-start: var(--stack);
  font-size: 0.85rem; color: var(--ink-muted);
}

/* --- wider viewports -------------------------------------------------------- */
@media (min-width: 48rem) {
  :root { font-size: 18px; }
  .masthead { padding-block-start: calc(var(--stack) * 1.25); }
  .masthead__title { font-size: 1.9rem; }
  .tagline { font-size: 1rem; }
  .archive-grid { grid-template-columns: repeat(var(--archive-columns-wide), minmax(0, 1fr)); }
  .cast-grid { grid-template-columns: repeat(var(--cast-columns-wide), minmax(0, 1fr)); }
}
