/*
 * The library, dressed.
 *
 * Every colour, every type setting, and every value another view will need to
 * agree with comes from tokens.css — if one of those is not a token yet, the
 * token is the fix. Geometry that belongs to a single rule and nothing else
 * (how far a gradient reaches, how much a thumbnail grows) stays here, because
 * promoting it would only spread one rule across two files.
 */

* {
  box-sizing: border-box;
}

/* Anything the page scrolls to deliberately stops below the sticky bar rather
   than under it. */
html {
  scroll-padding-top: var(--chrome-height);
}

body {
  margin: 0;
  padding: 0 var(--page-gutter) var(--space-8);
  background: var(--surface-page);
  color: var(--ink);
  font: var(--text-body) / var(--leading-body) var(--font-body);
}

/* One ring, in the interaction role, for everything that can take focus. */
:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/*
 * The one thing an author stylesheet has to say for `hidden` to keep working.
 *
 * The browser's own `[hidden] { display: none }` is in the user-agent origin, so
 * *any* `display` set on a class here beats it however specific it is — and the
 * page hides several things by the property rather than by a class: the way out
 * of a search, the way back from a filter, the play status until there is
 * something to say. Without this they are all permanently on screen.
 */
[hidden] {
  display: none !important;
}

/*
 * Read aloud, drawn nowhere. For the places where a mark carries its meaning
 * visually — a count of plays, a heart — and the sentence behind it still has to
 * be available.
 */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- The hero ----------------------------------------------------------- */

/*
 * The library opens on the series rather than on a toolbar: the show's own
 * backdrop across the top of the window, with one episode offered at the foot of
 * it. What names the show and what opens the reference behind it take the two top
 * corners, and what is left between them is the artwork — the one thing up here
 * nobody has to be told how to read.
 *
 * Out past the page's own gutter and back again, so the artwork reaches the
 * window's edges while everything drawn on it stays on the page's column. The
 * padding is the same top and bottom because it is the hero's own frame, and a
 * frame is even; underneath the offer it is also the air between the card and the
 * bar below.
 *
 * Nothing here collapses or shrinks as the page is scrolled. It simply scrolls
 * away, and the bar underneath it sticks: a header that animates its own height
 * while the page moves is a header that fights the scroll it is answering. It
 * does grow, though — the minimum height is a floor and never a ceiling, so a
 * card with more to say takes the room rather than being squashed into it.
 */
.hero {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-height: var(--hero-height);
  margin: 0 calc(-1 * var(--page-gutter));
  padding: var(--space-6) var(--page-gutter);
}

/*
 * The backdrop, and the two scrims that make everything over it readable.
 *
 * Scrims in the page's own colour rather than a flat black wash or a low
 * opacity, for the reason the season watermark uses one: the artwork has to end
 * in the page rather than stop at an edge. One fades it upward into the top of
 * the window and down into the grid below; the other darkens the left, where the
 * wordmark sits.
 */
.hero-art {
  /* How far the picture steps back. Nothing while the hero is the hero; enough
     to read a reference panel over once one has been opened over it. */
  --hero-veil: 0;

  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(
      rgb(var(--surface-page-channels) / var(--hero-veil)),
      rgb(var(--surface-page-channels) / var(--hero-veil))
    ),
    linear-gradient(
      to top,
      rgb(var(--surface-page-channels) / 1) 0%,
      rgb(var(--surface-page-channels) / 0.94) 16%,
      rgb(var(--surface-page-channels) / 0.62) 62%,
      rgb(var(--surface-page-channels) / 0.5) 100%
    ),
    linear-gradient(
      to right,
      rgb(var(--surface-page-channels) / 0.88) 0%,
      rgb(var(--surface-page-channels) / 0.4) 52%,
      rgb(var(--surface-page-channels) / 0.1) 100%
    ),
    var(--hero-backdrop, none);
  background-repeat: no-repeat;
  background-position:
    0 0,
    0 0,
    0 0,
    50% 28%;
  background-size:
    auto,
    auto,
    auto,
    cover;
}

/*
 * The picture steps back when the panel is opened over it. Which is the right
 * feeling as well as the right contrast: asking the hero for detail is asking it
 * to stop being a poster for a moment.
 */
body[data-about="open"] .hero-art {
  --hero-veil: 0.5;
}

/*
 * The two corners and what stands under them: the wordmark top-left, the way
 * into the panel top-right, and the offer at the bottom of the space they leave.
 *
 * Two rows — one sized to the corners themselves, and the free space beneath it.
 * The two columns are `minmax(0, 1fr)` so that the offer, which spans both, can
 * never widen the corners above it.
 *
 * The free row grows past its share when the card needs more, and the hero grows
 * with it.
 */
.hero-panel {
  position: relative;
  display: grid;
  flex: 1;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  gap: var(--space-5);
}

/*
 * The heading is the cell itself. There was a wrapper laying it out beside the
 * series' poster, and with the poster gone it had one child and nothing left to
 * lay out.
 *
 * The placement is here rather than on the shared `.wordmark` class because the
 * season-tabs bar wears that class too, inside a grid of its own; putting
 * `grid-column` on the shared class would place both marks.
 *
 * Both alignments are stated, for the reason the other corner states its own: the
 * plate is scaled by its width and takes its height from an aspect ratio, and an
 * aspect ratio is only honoured while the other axis is free. A stretched cell
 * would hand this a definite height the moment anything else made the row taller,
 * and the crop would distort rather than the corner growing.
 */
.hero-wordmark {
  grid-row: 1;
  grid-column: 1;
  align-self: start;
  justify-self: start;
}

/*
 * The other corner, holding the one control the hero has. Both alignments are
 * stated: while the row it sits in was a fixed height, sitting at the top of it
 * was what a stretched cell happened to look like, and now that the row sizes to
 * whatever the mark opposite comes to, staying in the corner is a thing to ask
 * for.
 */
.hero-about {
  grid-row: 1;
  grid-column: 2;
  align-self: start;
  justify-self: end;
}

/*
 * And the foot of the panel, with only ever one thing in it: the offer, or the
 * sentence standing where an offer cannot be made. At the start of the row, so
 * that its left edge is on the same line as the corner above it and the first
 * episode card below it, and the page reads as one column.
 *
 * Across both columns rather than confined to the left one. The width is
 * `min(100%, sum)`, and inside a half-width track that `100%` would resolve
 * against half the panel and shrink the card on any window under about 1200px;
 * spanning both, it holds its full width until the hero itself is narrower than
 * it and then comes down with the hero.
 *
 * The sum is stated on the pair rather than on the card, because the notice is
 * held to it too — and the body width along with it, because a `var()` inside a
 * custom property is substituted on whichever element reads it, so both operands
 * have to be declared wherever the sum is. Every part of the card is in the sum:
 * the picture, the body beside it, the gap between them, the card's own padding
 * either side and its two edges. Roughly 558px, where the picture and the body
 * put it at 706px before.
 */
.up-next,
.hero-notice {
  --up-next-body-width: 13rem;
  --up-next-width: min(
    100%,
    calc(
      var(--up-next-thumb-width) + var(--up-next-body-width) + var(--space-4) * 3 + 2px
    )
  );

  grid-row: 2;
  grid-column: 1 / -1;
  place-self: end start;
}

/*
 * Plain text until the banner artwork arrives — a heavy display-type fallback
 * that is still recognisably the library's name, and the whole of the mark in one
 * voice rather than a brush-drawn suffix beside system-ui capitals.
 *
 * The width is a property rather than a value so that the same plate can be worn
 * at two sizes: the hero's, and the small one the bar takes when the hero has
 * scrolled away. It is the *plate's* width and not the lockup's, which is what
 * lets the bar keep its own mark unchanged by overriding this one number. Every
 * part of the crop is proportional, and the suffix is placed in multiples of this,
 * so the hero's whole size is still this number alone — at its ceiling the plate
 * stands about 280×121 and the lockup around 366px, and the floor of 160px keeps
 * that lockup inside its half of the panel down to about a 480px window, where the
 * column measures (480 − 32 − 24) / 2 = 212px against a 209px lockup. So it still
 * never reaches the About control opposite it, with about 3px to spare.
 *
 * The viewport term is set so the ceiling engages at about a 1333px window, which
 * is where this mark has always reached its full size: it was 12vw against a
 * 160px ceiling before the corner was recomposed, and that ratio is kept.
 */
.wordmark {
  --wordmark-width: clamp(160px, 21vw, 280px);

  width: var(--wordmark-width);
  margin: 0;
  color: var(--brand-text);
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-tracking);
  line-height: var(--leading-tight);
  text-transform: uppercase;
}

/*
 * Once the artwork is there the heading stops being the mark and becomes the
 * lockup that holds it: the plate, then the library's own suffix beside it.
 *
 * The plate has to move off this element and onto a pseudo-element for the suffix
 * to be able to stand next to it at all — the crop is finished by a clip path
 * following the plate's four tilted corners, and a clip path on an element clips
 * everything inside it, so a suffix laid out here would be cut away along with the
 * banner's white field.
 *
 * The width is `max-content` rather than the sum of the parts: the lockup *is* its
 * parts, and stating the sum as well would be a second place to keep in step with
 * nothing enforcing that the two agree. `position: relative` stays for the
 * heading's own text, which is still in here and still hidden.
 */
.wordmark[data-artwork] {
  position: relative;
  display: flex;
  align-items: flex-start;
  width: max-content;
}

/*
 * The series' half: the orange plate cropped straight out of its banner artwork.
 * Nothing is generated — background positioning frames the plate's bounding box
 * and the clip path follows its four tilted corners, so the white field the plate
 * sits on in the source never reaches the page.
 *
 * Every part of the crop is proportional, so the width is the whole of what it
 * takes to scale it — which is also what makes the bar's collapse an animation of
 * this box, with the lockup laying out from it each frame.
 */
.wordmark[data-artwork]::before {
  content: '';
  flex: none;
  width: var(--wordmark-width);
  aspect-ratio: var(--wordmark-aspect);
  background-image: var(--wordmark-source);
  background-repeat: no-repeat;
  background-size: var(--wordmark-size);
  background-position: var(--wordmark-position);
  clip-path: var(--wordmark-plate);
}

/*
 * The library's half, and the only part of the mark that is the library's own:
 * `flix`, in the same hand the plate's lettering is drawn in, shipped as a vector
 * beside this stylesheet rather than found in the folder the library reads.
 *
 * The whole file is drawn, transparent margin and all, because that margin is the
 * room its drop shadow needs; the box therefore takes the file's own aspect ratio
 * so that placing it can never distort it, and `contain` holds the drawing to that
 * box whole. The three placements are multiples of the plate's width, measured off
 * the mockup — see the tokens for what was measured against what.
 *
 * Only the hero wears it. The bar's mark is the plate alone, on purpose: at the
 * bar's own ceiling this would come to about 29px of brush script, which is the
 * size at which four hand-drawn letters stop being a word and start being dirt on
 * the glass, while the plate is recognisable on its own.
 */
.hero-wordmark[data-artwork]::after {
  content: '';
  flex: none;
  width: calc(var(--wordmark-width) * var(--wordmark-suffix-width));
  margin-top: calc(var(--wordmark-width) * var(--wordmark-suffix-rise));
  margin-left: calc(var(--wordmark-width) * var(--wordmark-suffix-gap));
  aspect-ratio: var(--wordmark-suffix-aspect);
  background-image: url("/flix.svg");
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

/* The name is still the heading's text — it is only no longer the way it is drawn. */
.wordmark[data-artwork] > span {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/*
 * What the hero says while there is no card to put there: that the library is
 * being read, and — where it could not be — why.
 *
 * In the card's own place, because that is where the eye is: the offer is what
 * this space is for, and its absence is the thing being reported. The card's own
 * corner, the card's own left edge, and never wider than the card — a sentence is
 * one line where the card is a panel, so this is not the card's whole footprint,
 * but it stands inside it and the eye has nowhere to travel when the library
 * arrives. A whole sentence rather than a row of stamped fields, which is why it
 * is set as prose; the play status is the one other place in the page that is, for
 * the same reason.
 */
.hero-notice {
  max-width: var(--up-next-width);
  margin: 0;
  color: var(--ink-dim);
  font-size: var(--text-body-small);
  line-height: var(--leading-body);
}

/* A library that could not be read is not the same as one still arriving, and
   is worth full ink for it. */
.hero-notice[data-tone="failed"] {
  color: var(--ink);
}

/* ---- The About panel ----------------------------------------------------- */

/*
 * Everything the series file knows, across the width of the hero: the synopsis
 * beside the production facts, and the fourteen main cast on a row of their own
 * underneath. All three fall into one column as the window narrows.
 *
 * Wrapping flex rather than a grid, because the cast has to be able to claim a
 * whole row: an item spanning two columns of a grid that has narrowed to one
 * would conjure a second column back out of nothing and push the panel off the
 * page.
 */
.about {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5) var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--edge);
}

.about-section {
  display: flex;
  flex: 1 1 16rem;
  flex-direction: column;
  gap: var(--space-3);
}

/* The whole synopsis, given twice the share of the facts beside it: prose reads
   at a measure, and a paragraph in a 16rem column is a column of two words. */
.about-synopsis {
  display: flex;
  flex: 2 1 22rem;
  flex-direction: column;
  gap: var(--space-3);
  max-width: 44rem;
  color: var(--ink-dim);
  font-size: var(--text-body-small);
  line-height: var(--leading-body);
}

/* A row of its own: fourteen pairs read as a list across the panel rather than
   as a very long column down one third of it. */
.about-cast {
  flex-basis: 100%;
}

.about-synopsis > p {
  margin: 0;
}

/*
 * The fourteen, in as many columns as fit. Each name and the character it plays
 * are wrapped together so a pair can never be split across two of them.
 */
.cast {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: var(--space-3) var(--space-4);
  margin: 0;
}

.cast-member {
  display: flex;
  flex-direction: column;
}

.cast-name {
  color: var(--ink);
  font-size: var(--text-body-small);
  line-height: var(--leading-tight);
}

/*
 * The character, which is the half of this pair that does the work — an actor's
 * name is only useful here once it is attached to somebody you recognise.
 */
.cast-role {
  margin: 0;
  color: var(--ink-faint);
  font-size: var(--text-body-small);
  line-height: var(--leading-tight);
}

/* The caret is the part that moves; the label says the same thing in both
   states, because `aria-expanded` is what carries which one this is. */
.about-caret {
  display: inline-block;
  transition: transform var(--duration-hover) var(--ease);
}

.about-toggle[aria-expanded="true"] .about-caret {
  transform: rotate(180deg);
}

/* ---- Up next ------------------------------------------------------------- */

/*
 * The most useful thing on the page: one episode, offered, in the hero's
 * bottom-left corner. It is the answer to "what now" rather than another way of
 * browsing, so it carries the largest play button in the library, with nothing
 * else to decide. It is still the most substantial thing up here — a card at this
 * size could hardly not be — but it has stopped being the only thing up here,
 * which is the whole of what moving it out of the middle bought.
 *
 * Its width is the sum stated with its placement, because the notice standing in
 * for it is held to the same number. Under that sum — a narrow window — the whole
 * card shrinks rather than the hero scrolling sideways.
 */
.up-next {
  width: var(--up-next-width);
}

/*
 * The page's own sunk surface, at enough opacity to read the offer over whatever
 * frame of the backdrop is behind it. It lifts under the pointer exactly as an
 * episode card does, because it is one.
 *
 * Where it stands is the foot of the backdrop, where the vertical scrim has
 * nearly closed into the page's own colour: its top edge is over the picture, and
 * its lower half is on very nearly flat page colour — so it reads as smoked glass
 * across the top of it and as a panel with a hairline edge across the bottom. The
 * scrim is not retuned for that; a screenshot is the only fair way to judge four
 * tuned colour stops.
 *
 * Everything the offer says is on this panel rather than above it, for that
 * reason alone — the backdrop is a photograph nobody chose for its contrast.
 */
.up-next-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  border: 1px solid var(--edge-strong);
  border-radius: var(--radius);
  background: rgb(var(--surface-sunk-channels) / 0.86);
  box-shadow: var(--shadow-lift);
  cursor: pointer;
  transition:
    transform var(--duration-hover) var(--ease),
    border-color var(--duration-hover) var(--ease);
}

/* Stamped like a dossier field name: this labels the offer under it rather than
   heading a section to be read. */
.up-next-label {
  margin: 0;
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: var(--text-dossier-small);
  font-weight: 400;
  letter-spacing: var(--dossier-tracking);
  text-transform: uppercase;
}

/* Why this episode and not another — said only when the answer is not simply
   "it is the next one". */
.up-next-note {
  margin: 0;
  color: var(--ink-dim);
  font-size: var(--text-body-small);
  line-height: var(--leading-tight);
}

.up-next-episode {
  display: flex;
  gap: var(--space-4);
}

.up-next-card:hover,
.up-next-card:focus-within {
  transform: translateY(calc(-1 * var(--lift)));
  border-color: var(--interaction);
}

/*
 * The picture the whole card is sized from: drawn at the width chosen for it,
 * under a ceiling of the width the file actually holds. The basis is the choice
 * and the ceiling is the fact. At 300px against 400px the ceiling does nothing
 * today; it is written down so that raising the drawn width past the fact cannot
 * quietly become the picture enlarging itself, which is the one thing no image on
 * this page does.
 *
 * It still shrinks, so a narrow window narrows the picture rather than pushing the
 * card off the hero — which takes the explicit floor, because a flex item's
 * automatic one is its own content, and for an image declaring its intrinsic size
 * that is the 400px it is being asked to come down from. Left to that, the body
 * beside it is crushed to nothing and the window grows a horizontal scrollbar
 * instead.
 */
.up-next-thumb {
  flex: 0 1 var(--up-next-thumb-width);
  width: auto;
  height: auto;
  min-width: 0;
  max-width: var(--thumb-native-width);
  aspect-ratio: var(--thumb-aspect);
  object-fit: cover;
  border-radius: var(--radius-tight);
  background: var(--surface-sunk);
}

.up-next-body {
  display: flex;
  flex: 1 1 var(--up-next-body-width);
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.up-next-identity {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin: 0;
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: var(--text-dossier-small);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--dossier-tracking);
  text-transform: uppercase;
}

/* `01x13`, never `01X13` — the lowercase x is part of the canonical episode code. */
.up-next-code {
  color: var(--ink-dim);
  letter-spacing: var(--dossier-tracking-numeric);
  text-transform: none;
}

/* At the size a title is set at when it is the only one on the screen — the
   card's own size is what a grid of ninety-one has to hold to, and this is one. */
.up-next-title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-tracking);
  line-height: var(--leading-tight);
}

/* The play action is the point of the card, so it sits at the bottom of it
   however short the title turned out to be. */
.up-next-body .play {
  margin-top: auto;
}

/* ---- The bar the hero rolls up into -------------------------------------- */

/*
 * The wordmark, every season, and the search field — the three things worth
 * having wherever you are in 91 episodes. It follows the hero in the flow and
 * sticks at the top of the window, so the hero rolling out of the way and this
 * taking over are one movement and not two states being animated against each
 * other.
 *
 * Out past the gutter and back, like the hero: the artwork bleeds to the edges
 * of the window, so what covers it has to as well or the backdrop shows through
 * either side of the bar as it scrolls under.
 *
 * Three columns, and the whole of what each of the three is entitled to. The
 * mark takes the width it is drawn at, which is nothing until the hero has gone.
 * The strip takes exactly what six tabs come to and stops — it is the only thing
 * here that would rather wrap than shrink, and a strip that has wrapped is a bar
 * a row taller. What is left over is the search's, and on a window with nothing
 * left over that is the magnifier's own square and no more: the one thing the
 * field may never do is take a pixel the tabs were standing on. Where the column
 * runs out it stands over them instead, which is the field's business and
 * described there.
 *
 * Columns rather than a wrapping row for that last reason. Sharing free space is
 * what a row does — it hands every hungry item a proportion of it — and the field
 * must have a share of exactly nothing until the tabs are satisfied. A fraction
 * track is that rule stated once: it is sized from what the fixed columns left.
 *
 * No vertical padding of its own. The tab strip's own padding sets the height,
 * which is what lets the current tab's underline sit on the bar's bottom rule.
 */
.chrome {
  /* The magnifier's own square: the least the search may be given, and the
     height the field keeps whether it is folded into it or standing open. */
  --search-square: 2rem;

  position: sticky;
  top: 0;
  z-index: var(--layer-chrome);
  display: grid;
  grid-template-columns: auto minmax(0, max-content) minmax(var(--search-square), 1fr) auto;
  align-items: center;
  gap: var(--space-2) var(--space-5);
  margin: 0 calc(-1 * var(--page-gutter));
  padding: 0 var(--page-gutter);
  border-bottom: 1px solid var(--edge);
  background: var(--surface-page);
}

/*
 * The plate the hero wears, small, and only once the hero has gone. It grows out
 * of nothing rather than fading in place, so the tabs move aside for it instead of
 * it landing on top of them.
 *
 * The plate alone, without the library's suffix beside it. Two reasons, and the
 * second is the hard one: at this ceiling the suffix would come to about 29px of
 * brush script, and the mark has to stay shorter than the tab strip — at its
 * widest the plate stands about 47px against a strip of about 52px, so growing it
 * leaves the bar's height alone. That matters more than it looks: the height of
 * this bar must not depend on whether it has collapsed, or a pinned season heading
 * would step down the moment the wordmark arrived.
 *
 * Width and opacity only, and the width is overridden here rather than the mark
 * being sized from the hero's — which is exactly why the shared property means the
 * plate's width and not the whole lockup's.
 */
.chrome-wordmark {
  --wordmark-width: 0px;

  overflow: hidden;
  opacity: 0;
  transition:
    width var(--duration-reveal) var(--ease),
    opacity var(--duration-reveal) var(--ease);
}

/*
 * Once the artwork has arrived the box that grows is the plate's, not this
 * element's: the lockup is `max-content` by then, and max-content does not
 * interpolate. The plate's width animates and the element lays out from it each
 * frame, which comes to the same movement. Reduced motion reaches this the way it
 * reaches the rest — the duration itself goes to zero.
 */
.chrome-wordmark[data-artwork]::before {
  transition: width var(--duration-reveal) var(--ease);
}

body[data-hero="collapsed"] .chrome-wordmark {
  --wordmark-width: clamp(84px, 8vw, 108px);

  opacity: 1;
}

/* Once there is page underneath it rather than hero, the bar says it is over
   something rather than part of it. */
body[data-hero="collapsed"] .chrome {
  box-shadow: var(--shadow-chrome);
}

/* ---- Who is watching ---------------------------------------------------- */

/*
 * The last column of the bar: a name and a way out, or a way in. Quiet next to
 * the tabs — it is said once and rarely acted on — and never wrapping, so the
 * bar's height does not depend on who signed in.
 */
.sign-in {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  white-space: nowrap;
  font-size: var(--text-body-small);
}

.sign-in-name {
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--ink-dim);
}

/* Plain, not alarming: being signed in but not approved is not a fault. */
.sign-in-refused {
  color: var(--ink);
}

.sign-in-action {
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--edge-strong);
  border-radius: var(--radius-tight);
  background: none;
  color: var(--ink);
  font: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--duration-hover) var(--ease);
}

.sign-in-action:hover {
  border-color: var(--ink-faint);
}

/* Narrow windows keep the way in and out, and drop the name it belongs to. */
@media (max-width: 40rem) {
  .sign-in-name {
    display: none;
  }
}

/* ---- The search field --------------------------------------------------- */

/*
 * A magnifier until it is asked for, and a field once it is.
 *
 * Pushed to the far edge of the bar, where a search is looked for, and last in
 * its reading order — so a tab through the bar reaches the seasons and then the
 * search.
 *
 * It is laid out in the bar's last column and nothing else, which the bar sizes
 * from what the mark and the season tabs did not need. That is where the hard
 * constraint is kept rather than here: the bar publishes its measured height to
 * the rest of the page, and a field that pushed the tabs onto a second row would
 * shift the whole page underneath somebody who is typing into it.
 *
 * What animates is the field's own ceiling. It opens from the magnifier's square
 * to the width a search field wants, and the column stops it wherever the bar
 * runs out — so the expansion is a length ramping rather than a layout being
 * renegotiated, and on a bar with little to spare it simply arrives sooner. Off
 * the page's reveal duration, so the system reduced-motion setting zeroes it with
 * everything else.
 *
 * Under the column there is a floor, and it is the width this field never went
 * below in the years it stood open all day. On a narrow window six tabs and a mark
 * leave the search less than that — at the far end, the magnifier's square and
 * nothing else — and a field held to what is left draws its query and its way out
 * of the search on top of each other. That is not a smaller field but an
 * unreadable one, which is a worse answer than the slab of bar the fold was
 * written to reclaim. So it opens to the floor and stands over the right of the
 * tab strip until it is folded again, and that costs the layout nothing: the
 * column is the same square whether the field is folded into it or open across the
 * bar, so the strip is covered while somebody is typing rather than moved.
 *
 * Relative and a fixed height, because everything in it is positioned: the field
 * over the whole of it, and the way out of a search inside its right edge.
 */
.search {
  /* The least a field may be. The insets below are shares of it, so moving it
     means checking them. */
  --search-floor: 14rem;

  position: relative;
  display: flex;
  justify-self: end;
  /* The column, or the floor, whichever is the wider. */
  width: max(100%, var(--search-floor));
  min-width: 0;
  /* Wider than this and a search field stops being a control in a bar; on most
     windows the column runs out before this does. */
  max-width: 22rem;
  height: var(--search-square);
  transition: max-width var(--duration-reveal) var(--ease);
}

.search[data-field="closed"] {
  max-width: var(--search-square);
}

/*
 * The field fills whatever the form came out at.
 *
 * Laid over the form rather than in it, so that the form's width is the flex
 * layout's business alone: a field in the flow could never be narrower than its
 * own padding, and would hold the bar open at eighty pixels when it is supposed
 * to be folded into thirty-two. Only once the page's script is answering for the
 * fold, though — with no script there is no magnifier to give the form a size,
 * and a field lifted out of the flow would leave it empty.
 */
.search[data-field] .search-input {
  position: absolute;
  inset: 0;
}

.search-input {
  flex: 1 1 0;
  min-width: 0;
  /* Lengths, ceilinged as a share of the field itself. A box cannot be narrower
     than the padding it holds, and folded this one is a fifth of the width of its
     own — left to lengths alone it would stand at eighty pixels inside a
     thirty-two pixel square and hang out of the bar, which on a narrow window is
     past the page's gutter and a horizontal scrollbar. The shares are each length
     over the field's floor, so both have reached their lengths by the narrowest
     the field ever opens to and every field you can read is inset the same. */
  padding: var(--space-2) min(var(--space-8), 29%) var(--space-2) min(var(--space-4), 8%);
  border: 1px solid var(--edge-strong);
  border-radius: var(--radius-tight);
  background: var(--surface-sunk);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--text-dossier);
  letter-spacing: var(--dossier-tracking-numeric);
  transition: border-color var(--duration-hover) var(--ease);
  /* The field's own furniture, replaced by the button below: the native one
     appears at a size and a colour of the browser's choosing, and cannot be told
     that Escape does the same thing. */
  -webkit-appearance: none;
  appearance: none;
}

/*
 * Folded, the field is `visibility: hidden` rather than merely narrow: a
 * zero-width input is still something Tab can land in, and a page with an
 * invisible stop in its tab order is a page the keyboard gets lost in.
 *
 * It goes at the end of the fold rather than the start of it — `step-end` over
 * the same duration — so what is folding away is visibly the field rather than
 * an empty box where the field was.
 */
.search[data-field="closed"] .search-input {
  visibility: hidden;
  transition:
    border-color var(--duration-hover) var(--ease),
    visibility var(--duration-reveal) step-end;
}

.search-input::-webkit-search-cancel-button {
  display: none;
}

.search-input::placeholder {
  color: var(--ink-faint);
}

.search-input:hover {
  border-color: var(--ink-faint);
}

/* The field is the one place in the page you type into, so it says so while you
   are in it — in the interaction role, like everything else you can act on. */
.search-input:focus {
  border-color: var(--interaction);
  outline: none;
}

.search-input:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/*
 * What the field folds into, and the whole of the search when nothing is being
 * searched for. It holds the form's height open under the field laid over it, so
 * the bar's height is the same whichever state this is in.
 *
 * Hidden rather than removed once the field is open: it is what the form is
 * measured by, and `visibility` takes it out of the tab order in the same breath.
 * The field covers the square it leaves behind.
 */
.search-toggle {
  display: grid;
  place-items: center;
  width: var(--search-square);
  height: var(--search-square);
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-tight);
  background: none;
  color: var(--ink-dim);
  cursor: pointer;
  transition:
    border-color var(--duration-hover) var(--ease),
    color var(--duration-hover) var(--ease);
}

.search[data-field="open"] .search-toggle {
  visibility: hidden;
}

.search-toggle:hover {
  border-color: var(--edge-strong);
  color: var(--ink);
}

/* Drawn by the page's one recipe for a glyph, further down; a little above the
   size a mark is set at, because this one is the whole of a control rather than a
   thing beside a title. */
.search-toggle > svg {
  width: 1.125rem;
  height: 1.125rem;
}

/*
 * Inside the field's right edge, over the padding held for it. Absent until
 * there is something to clear, so an empty field is just a field.
 */
.search-clear {
  position: absolute;
  top: 50%;
  right: var(--space-2);
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  transform: translateY(-50%);
  border: 1px solid transparent;
  border-radius: 50%;
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
  transition:
    border-color var(--duration-hover) var(--ease),
    color var(--duration-hover) var(--ease);
}

.search-clear:hover {
  border-color: var(--edge-strong);
  color: var(--ink);
}

.search-clear > svg {
  width: 0.875rem;
  height: 0.875rem;
}

/* ---- The season tabs --------------------------------------------------- */

/*
 * Six links across the top of the library: the whole series as one pool, then
 * the five seasons. Wrapping rather than scrolling, because a strip that has to
 * be scrolled to reach season 4 is the problem this replaces.
 *
 * The bar's bottom line is what the current tab's underline sits on, so the
 * strip reads as a row of tabs on a rule rather than as six separate boxes. Six
 * tabs need about 840px, which the page has at every width it is used at — but
 * the drawer takes a third of a narrow window, so wrapping is reachable and is
 * given a row gap: a wrapped row's tabs still underline themselves, and the row
 * below cannot be pulled into that underline by the negative margin under it.
 */
.season-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  /* The column it sits in is sized to the six tabs and capped there, so on any
     window wide enough the strip is one row and there is nothing to say here.
     Below that the column has less than the six need and the strip wraps within
     itself rather than pushing anything else off the bar — which is what this
     line is for: a flex container's automatic minimum is its own content, and
     that content is the whole unwrapped strip. */
  min-width: 0;
}

/*
 * One tab: its name, and the underline that says whether you are on it. There is
 * nothing else in it to lay out — no count, and nothing measuring how far into
 * the season a re-watch has got.
 */
.tab {
  display: block;
  /* Wide enough that six tabs of unequal names still read as one row of equal
     things rather than as six labels of six lengths. */
  min-width: 7.5rem;
  padding: var(--space-4) var(--space-1) var(--space-2);
  /* Transparent until this is the current tab, so nothing moves when it becomes
     one; pulled down over the strip's own rule so the two are one line. */
  margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  color: var(--ink-dim);
  font-family: var(--font-mono);
  font-size: var(--text-dossier);
  letter-spacing: var(--dossier-tracking);
  text-transform: uppercase;
  text-decoration: none;
  transition:
    border-color var(--duration-hover) var(--ease),
    color var(--duration-hover) var(--ease);
}

.tab:hover {
  color: var(--ink);
}

/*
 * The view you are in. The brand orange, because this is the page saying where
 * you are — see the role note in tokens.css. Nothing else in the strip is
 * coloured, so one glance finds it.
 */
.tab[aria-current="page"] {
  border-bottom-color: var(--brand);
  color: var(--ink);
}

/* ---- Narrowing and ordering the library ---------------------------------- */

/*
 * The band under the tabs: what is being held back, what order this is in, and
 * how much library that leaves. Directly above the grid, because it is the
 * heading of whatever the grid is now holding — a narrowed pool has no season
 * heading of its own, by design.
 *
 * No rule of its own under it: the tab strip's line is above and the first
 * season heading brings its own, and a third line eight pixels from the first
 * would read as a box rather than as a row of controls.
 */
.library-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-4) 0 var(--space-2);
}

/* Nothing in it until the library has been read, and an empty band of padding
   is a gap the page cannot explain. */
.library-controls:empty {
  display: none;
}

/*
 * Every stamped control in the page, wherever it is read: the library's two
 * controls, the hero's way into the series, and the drawer's two steps along a
 * season. One recipe, because they are one kind of thing — a short instruction in
 * a mono label, pressed in passing, and never the press. What none of them is, is
 * the play button, so they take the interaction role and never the brand.
 */
.filter-chip,
.controls-clear,
.about-toggle,
.drawer-step {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--edge-strong);
  border-radius: var(--radius-tight);
  background: rgb(var(--surface-sunk-channels) / 0.72);
  color: var(--ink-dim);
  font-family: var(--font-mono);
  /* Tracked like a numeral rather than like a field label: these are read as
     words, and 0.18em stops them being words. */
  font-size: var(--text-dossier-small);
  letter-spacing: var(--dossier-tracking-numeric);
  line-height: var(--leading-tight);
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition:
    border-color var(--duration-hover) var(--ease),
    color var(--duration-hover) var(--ease);
}

.filter-chip:hover,
.controls-clear:hover,
.about-toggle:hover,
.order-select:hover,
/* An edge of the season is plainly nowhere to go, so it does not light up. */
.drawer-step:enabled:hover {
  border-color: var(--interaction);
  color: var(--ink);
}

/* At the weight a card's marks are drawn at — one stroke for every icon in the
   page, whether it is a mark being read or a filter being pressed. Not at their
   size: this heart is set in a line beside two words, where a card's stands alone
   in a button and is drawn half again as large. */
.filter-chip > svg {
  width: 0.875rem;
  height: 0.875rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/*
 * On: the same filled heart in the same orange a favourited card carries, because
 * this is that mark asked as a question. The label is lifted to full ink rather
 * than coloured with it — two words in the brand would read as a link, and the
 * heart is the part that has to match.
 *
 * The edge stays in the interaction colour, which is what every pressed and
 * hovered control in this row wears: the chip being on is a state of a control,
 * where the heart inside it is the mark being quoted.
 */
.filter-chip[aria-pressed="true"] {
  border-color: var(--interaction);
  color: var(--ink);
}

.filter-chip[aria-pressed="true"] > svg {
  color: var(--brand-text);
  fill: currentColor;
}

/* The caret the browser's own control would have drawn, in the page's own ink —
   see the select below for why the native furniture is off. */
.order-field {
  position: relative;
  display: flex;
}

.order-field::after {
  content: "▾";
  position: absolute;
  top: 50%;
  right: var(--space-2);
  transform: translateY(-50%);
  color: var(--ink-faint);
  font-size: var(--text-dossier-small);
  pointer-events: none;
}

/*
 * One of three, which is what a native select is — it brings the whole keyboard
 * and pointer model with it rather than this page promising one it would then
 * have to build. Only the furniture is replaced: left alone, the control is drawn
 * at a size and a colour of the browser's choosing and would be the one light
 * rectangle on a page of concrete.
 */
.order-select {
  padding: var(--space-1) var(--space-6) var(--space-1) var(--space-3);
  border: 1px solid var(--edge-strong);
  border-radius: var(--radius-tight);
  background: rgb(var(--surface-sunk-channels) / 0.72);
  color: var(--ink-dim);
  font-family: var(--font-mono);
  font-size: var(--text-dossier-small);
  letter-spacing: var(--dossier-tracking-numeric);
  line-height: var(--leading-tight);
  text-transform: uppercase;
  cursor: pointer;
  transition:
    border-color var(--duration-hover) var(--ease),
    color var(--duration-hover) var(--ease);
  -webkit-appearance: none;
  appearance: none;
}

/* The menu itself is the operating system's, and it is not dark by default —
   the options say which surface they belong to so the list is legible. */
.order-select > option {
  background: var(--surface-raised);
  color: var(--ink);
}

/*
 * How much library this is, pushed to the far edge where a readout is looked
 * for. It takes the season heading's own voice — see `.season-meta` below, which
 * it shares its type and its dot with — because it is answering the same
 * question about a pool the arrangement left without a heading.
 */
.library-controls .selection-summary {
  margin-left: auto;
}

/* ---- Seasons ----------------------------------------------------------- */

.season {
  padding-top: var(--space-7);
}

.season-head {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-end;
  gap: var(--space-2);
  min-height: 9rem;
  margin-bottom: var(--space-5);
  padding: var(--space-6) var(--space-5) var(--space-4);
  overflow: hidden;
  border-bottom: 1px solid var(--edge);
  /* The page's own colour, so it changes nothing until the heading is pinned —
     at which point it is what the cards scroll behind rather than through. */
  background: var(--surface-page);
}

/*
 * In the pool the headings are the only thing keeping you oriented through 91
 * episodes, so each one pins to the top of the window until its own season has
 * scrolled past. Each `.season` section is the sticky container for its own
 * heading, so they replace one another rather than stacking up.
 *
 * Only in the pool: with one season on show there is nothing to lose your place
 * between, and a heading pinned over its own grid would be taking 9rem of the
 * window to say what the current tab already says.
 *
 * Pinned under the bar rather than at the top of the window, since the bar is
 * already there and is the one thing the page may not scroll in front of. The
 * bar measures itself and says how tall it came out — six tabs and a search
 * field wrap differently at every width, so the offset cannot be a constant.
 */
.library[data-view="all"] .season-head {
  position: sticky;
  top: var(--chrome-height);
  z-index: var(--layer-sticky);
}

/*
 * The season's own poster, blown up well past the heading and faded almost to
 * nothing. It is a watermark: present enough that each season looks like
 * itself, quiet enough that the title reads straight over it.
 *
 * The fade is a scrim in the page's own colour painted over the poster, rather
 * than a mask or a low opacity. That way the poster's edges are not merely
 * faint at the crop, they are covered outright — a straight edge showing
 * through at 9% is exactly what stops something reading as a watermark.
 *
 * The scrim's ellipse is measured in `rem` rather than in percentages of the
 * heading, because it has to close before the poster's own edges and the
 * poster is a fixed size while the heading is as wide as the window. Being an
 * ellipse, it closes on all four of them at once.
 */
.season-head::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    radial-gradient(
      /* Both radii close inside the heading's own 9rem, so the top and bottom
         of the crop are covered rather than merely faint. */
      20rem 6rem at 11rem 46%,
      rgb(var(--surface-page-channels) / 0.9) 0%,
      rgb(var(--surface-page-channels) / 0.93) 35%,
      rgb(var(--surface-page-channels) / 1) 78%
    ),
    var(--season-poster, none);
  background-repeat: no-repeat;
  background-position:
    0 0,
    left var(--space-5) top 36%;
  background-size:
    auto,
    auto 420%;
}

.season-name,
.results-name {
  position: relative;
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-tracking);
  line-height: var(--leading-tight);
}

/*
 * What the season holds, under its name in the dossier voice. Divided by a dot
 * the stylesheet draws rather than the markup carrying a separator, because a
 * season whose scrape measured no durations has no second value and must not be
 * left with a trailing dot.
 */
.season-meta,
.results-meta,
.selection-summary {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-3);
  margin: 0;
  color: var(--ink-dim);
  font-family: var(--font-mono);
  font-size: var(--text-dossier);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--dossier-tracking-numeric);
  text-transform: uppercase;
}

/* The one dot the page draws between two facts, wherever two facts sit in a
   row — so a field a scrape did not provide leaves no separator behind it. */
.season-meta > * + *::before,
.results-meta > * + *::before,
.selection-summary > * + *::before,
.card-match-field + .card-match-field::before {
  content: "·";
  margin-right: var(--space-3);
  color: var(--ink-faint);
}

/* ---- What a search found ------------------------------------------------ */

/*
 * The other kind of pool the grid area holds, headed the same way a season is —
 * one visual language for "here is a set of episodes and what it amounts to".
 * What it has instead of a poster is that there is no poster: a result set spans
 * the series and belongs to no one season, which is the fact the heading is
 * there to carry.
 */
.results-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
  padding: var(--space-7) 0 var(--space-4);
  border-bottom: 1px solid var(--edge);
}

/*
 * A search that found nothing, and a filter that kept nothing. Set as sentences
 * rather than as a stamped label: this is the page explaining itself, which is
 * the one voice in the library that is allowed to be prose.
 */
.results-empty,
.library-empty {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-width: 44rem;
  padding: var(--space-6) 0;
  color: var(--ink-dim);
  font-size: var(--text-body-small);
  line-height: var(--leading-body);
}

.results-empty > p,
.library-empty > p {
  margin: 0;
}

/*
 * Why a result came back: which season it is in, which field matched, and — for
 * the fields the card is not already showing — the part of that field that did.
 *
 * The season is first because it is the thing a result cannot be ambiguous
 * about: the code above says `02x11` and this says it in words, for the pool
 * where five seasons are mixed together.
 */
.card-match {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-3);
  margin: 0;
  padding-top: var(--space-1);
  border-top: 1px solid var(--edge);
  color: var(--ink-dim);
  font-size: var(--text-body-small);
  line-height: var(--leading-body);
}

.card-match-field {
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: var(--text-dossier-small);
  letter-spacing: var(--dossier-tracking);
  /* Set to the line beside it rather than to its own size, so the stamped field
     names sit on the same baseline as the evidence when both are on one line. */
  line-height: inherit;
  text-transform: uppercase;
  white-space: nowrap;
}

/*
 * The evidence, held to two lines. A synopsis match is already cut down to the
 * part that matched; this is what stops a long guest-cast name or an awkward cut
 * from pushing the grid's rows out of line with each other.
 */
.card-match-text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  flex-basis: 100%;
}

/*
 * The matched run itself. A wash of the interaction colour rather than the
 * highlighter yellow a browser draws by default: this is the page answering you,
 * which is the same voice the play status speaks in — and yellow would be a
 * fourth accent in a palette whose three roles are deliberately disjoint.
 */
mark {
  border-radius: 2px;
  padding: 0 0.15em;
  background: rgb(var(--interaction-channels) / 0.18);
  color: var(--ink);
}

/* In a card's own title the mark carries the title's weight, so a highlight
   cannot make the matched word read as a different size of type. */
.card-title mark {
  background: rgb(var(--interaction-channels) / 0.22);
}

/* ---- The episode grid -------------------------------------------------- */

/*
 * About five columns at a 1600px window, and one fewer at each step down as
 * the window narrows. Nothing about the count is hard-coded.
 */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--card-min-width), 1fr));
  gap: var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

/*
 * Deliberately not `overflow: hidden`. The only thing that overflows a card is
 * the thumbnail growing under the pointer, and .card-media clips that itself —
 * clipping at the card as well would cut the focus ring off anything the play
 * and detail tickets put inside one.
 */
.card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface-raised);
  /* The whole card opens the dossier, so the whole card says so. The controls
     inside it set their own cursor and are unaffected. */
  cursor: pointer;
  transition:
    transform var(--duration-hover) var(--ease),
    border-color var(--duration-hover) var(--ease),
    background-color var(--duration-hover) var(--ease),
    box-shadow var(--duration-hover) var(--ease);
}

.card:hover,
.card:focus-within {
  transform: translateY(calc(-1 * var(--lift)));
  border-color: var(--edge-strong);
  background: var(--surface-lifted);
  box-shadow: var(--shadow-lift);
}

.card-media {
  position: relative;
  aspect-ratio: var(--thumb-aspect);
  overflow: hidden;
  /* Rounds with the card, since the card no longer clips it. */
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
  background: var(--surface-sunk);
}

.card-thumb {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition:
    transform var(--duration-reveal) var(--ease),
    opacity var(--duration-reveal) var(--ease);
}

.card:hover .card-thumb,
.card:focus-within .card-thumb {
  transform: scale(1.06);
}

/* Stands in for a thumbnail the scan did not find, at whatever size the view
   asked for — a card's or the drawer's. */
.thumb-missing {
  background: repeating-linear-gradient(
    135deg,
    var(--surface-sunk) 0 10px,
    var(--surface-raised) 10px 20px
  );
}

/* Where Play will carry on from: a bar along the foot of the thumbnail, the
   card's and the drawer's, in the brand's orange because it is a promise about
   what pressing Play does. Over the picture rather than under it, so a card with
   a resume point is no taller than one without. Beneath the card's reveal, whose
   scrim deepens over it on hover rather than being cut by it. */
.resume-bar {
  position: absolute;
  inset: auto 0 0 0;
  height: 4px;
  background: rgb(var(--surface-sunk-channels) / 0.7);
  pointer-events: none;
}

.resume-bar-fill {
  height: 100%;
  background: var(--brand);
}

body[data-watch-state="none"] .resume-bar {
  display: none;
}

/* ---- What a card reveals on hover -------------------------------------- */

/*
 * The play button, in the corner of the thumbnail it has always occupied, and
 * nothing else. The rating, the air date and the two lines of synopsis that used
 * to stand in front of it are in the dossier, which is where they were all along.
 */
.card-reveal {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: var(--space-3);
  /* Dark enough to hold a button legibly over any of ninety-one pictures, sheer
     enough that the thumbnail is still the thing you recognise the episode by.
     Weighted to the bottom, which is the only part of it anything sits on. */
  background: linear-gradient(
    to top,
    rgb(var(--surface-sunk-channels) / 0.88) 0%,
    rgb(var(--surface-sunk-channels) / 0.55) 55%,
    rgb(var(--surface-sunk-channels) / 0.12) 100%
  );
  opacity: 0;
  transition: opacity var(--duration-reveal) var(--ease);
  /*
   * Deliberately still clickable at rest, rather than made inert until it is
   * visible. Nothing can reach a play button without the pointer being over the
   * card, which is what reveals it in the first place — so the guard would
   * protect against nothing, while costing a tap that arrives in the same
   * instant as the pointer. Losing a click on the primary action of the library
   * is the worse of the two.
   */
}

.card:hover .card-reveal,
.card:focus-within .card-reveal {
  opacity: 1;
}

/* ---- What the record puts on a card ------------------------------------ */

/*
 * Nothing is drawn on the picture. What the record says about an episode is set in
 * the card's footer with the code and the title — the count at the right of the
 * code row, the two toggles in the corner under it — because the thumbnail is the
 * one part of a card actually used to recognise an episode.
 */

/* One row of toggles, wherever they are read: a card's footer corner, or the
   drawer's own row beside the watch history. */
.mark-group {
  display: flex;
  gap: var(--space-1);
}

/* Someone with no watch state has no marks to make and no history to read, so
   neither the toggles nor the drawer's row that holds them is drawn. Outranks
   `.watch-detail` below by its selector, not by its place in the sheet. */
body[data-watch-state="none"] .mark-group,
body[data-watch-state="none"] .watch-detail {
  display: none;
}

.mark {
  display: grid;
  place-items: center;
  width: var(--mark-size);
  height: var(--mark-size);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
  opacity: 0;
  transition:
    opacity var(--duration-reveal) var(--ease),
    color var(--duration-hover) var(--ease);
}

/*
 * How an icon in the page is drawn — the marks here, the eye beside a count and
 * the drawer's own dismissal, and the same recipe again wherever else one appears,
 * all of them hand-authored in icon.js. An outline in the current colour at one
 * weight, so they sit at the same weight beside each other wherever they are read.
 * Whether a mark is *set* is a `fill` further down rather than a second glyph,
 * which is what lets one path carry both states.
 *
 * Only the size belongs to whatever holds the glyph.
 */
.mark > svg,
.card-watch-count > svg,
.drawer-close > svg,
.player-close > svg,
.search-toggle > svg,
.search-clear > svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/*
 * Larger than every other glyph in the page, deliberately. These two are the only
 * things on it a hand ever sets — everything else about an episode is read off the
 * media folder or counted by the library — so the mark that carries the whole of
 * the viewer's opinion is not also the smallest thing on the card.
 */
.mark > svg {
  width: 1.25rem;
  height: 1.25rem;
}

/*
 * A mark that has just been set springs past its size and settles back.
 *
 * It is the receipt for a round trip. Setting a mark is not drawn optimistically —
 * the record's whole value is that it is true — so there is a gap between the click
 * and the fill arriving, and on a card, where the marks are revealed by hover and
 * sit in a footer beside a title and a count, a fill alone is easy to miss. The pop
 * is fired when the library confirms the mark, which is why it means "stored" and
 * not "clicked"; watch.js decides when, and only ever on a mark being set.
 *
 * Drawn on the glyph rather than on the button, so the focus ring — which is the
 * button's, offset outside it — stays still while the mark inside it moves. The
 * reveal opacity and the colour transitions are the button's too, and are left
 * undisturbed by putting the transform one level in.
 *
 * The reveal duration and the page's one curve, borrowed rather than added to: the
 * overshoot is in the keyframes, so a second curve would buy nothing, and borrowing
 * a duration is what turns the pop off under the reduced-motion block already in
 * tokens.css. At its 1.35 peak the glyph is 27px against the 30px box a card gives
 * it, so the overshoot never leaves the button on either surface.
 */
@keyframes mark-pop {
  0% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.35);
  }
  70% {
    transform: scale(0.92);
  }
  100% {
    transform: scale(1);
  }
}

.mark[data-popping] > svg {
  animation: mark-pop var(--duration-reveal) var(--ease);
}

/*
 * At rest a card shows only the mark it actually carries; hovering reveals both
 * as outlines. A season nothing is marked in is therefore not a grid of empty
 * outlines, which is what keeps 24 cards scannable.
 */
.mark[aria-pressed="true"],
.card:hover .mark,
.card:focus-within .mark {
  opacity: 1;
}

/*
 * Set: filled, each in its assigned role. The heart takes the series' own orange
 * — the lifted tone, which is the one that survives being an icon on a dark
 * surface — and the thumbs down a muted neutral, so only one of the pair is
 * coloured and a favourite is found at a glance.
 */
.mark-favourite[aria-pressed="true"] {
  color: var(--brand-text);
}

.mark-disliked[aria-pressed="true"] {
  color: var(--ink-dim);
}

.mark-favourite[aria-pressed="true"] > svg,
.mark-disliked[aria-pressed="true"] > svg {
  fill: currentColor;
}

/*
 * Only an unmarked one brightens under the pointer. Orange already means "this is
 * a favourite" here, so letting hover repaint a mark that is already set would
 * take a filled heart out of the brand at exactly the moment it is being looked
 * at.
 */
.mark[aria-pressed="false"]:hover {
  color: var(--ink);
}

/* Held for the round trip, which is to a server on this machine. */
.mark:disabled {
  cursor: progress;
}

/* ---- Starting an episode ----------------------------------------------- */

/*
 * One action, so one rule for it: a square with rounded corners carrying a
 * triangle and no word. It keeps the series' orange, which besides identity is
 * what the brand is for — and it is the largest thing wearing it on any screen,
 * so the thing to press is the thing the eye lands on.
 *
 * There is nothing to read here, so nothing to squeeze: the button is a size
 * rather than a label, and it takes the size the surface holding it sets below.
 */
.play {
  display: grid;
  flex: none;
  place-items: center;
  width: var(--play-size);
  height: var(--play-size);
  padding: 0;
  border: 1px solid var(--brand-deep);
  border-radius: var(--radius-tight);
  background: var(--brand-deep);
  color: var(--on-brand);
  cursor: pointer;
  transition:
    background-color var(--duration-hover) var(--ease),
    border-color var(--duration-hover) var(--ease);

  /* Large on a card, which is the smallest surface it appears on and the one
     where it sits over a thumbnail rather than beside text. */
  --play-size: 2.75rem;
}

/* Large in the dossier, where the panel is the width of the drawer. */
.drawer-dossier .play {
  --play-size: 3rem;
}

/* Largest on the offer: the most useful thing on the page, so the easiest thing
   on it to hit. */
.up-next-body .play {
  --play-size: 4rem;
}

.play:hover {
  border-color: var(--brand);
  background: var(--brand);
}

/* The triangle, drawn as every other icon in the page is — the same 24×24 field
   at the same stroke weight — and filled, the way a set mark is. It scales with
   the button rather than sitting at one size in three sizes of square. */
.play > svg {
  width: 72%;
  height: 72%;
  fill: currentColor;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linejoin: round;
}

/* ---- The player --------------------------------------------------------- */

/*
 * The whole window, over everything: watching is the one thing going on. A
 * modal dialog is drawn in the top layer, above the chrome, the drawer and its
 * scrim, so it needs no place in the page's own stack of layers.
 */
.player {
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: #000;
  color: var(--ink);
}

.player[open] {
  display: flex;
  flex-direction: column;
}

.player::backdrop {
  background: #000;
}

/* The page behind does not scroll while an episode plays — and is left where it
   was, since hiding the overflow keeps the scroll position. */
body[data-player="open"] {
  overflow: hidden;
}

.player-chrome {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
}

.player-title {
  overflow: hidden;
  margin: 0;
  color: var(--ink-dim);
  font-family: var(--font-mono);
  font-size: var(--text-body-small);
  font-weight: 400;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The drawer's own way out, at the same far edge. */
.player-close {
  display: grid;
  flex: none;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  margin-left: auto;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 50%;
  background: none;
  color: var(--ink-dim);
  cursor: pointer;
  transition:
    border-color var(--duration-hover) var(--ease),
    color var(--duration-hover) var(--ease);
}

.player-close:hover {
  border-color: var(--edge-strong);
  color: var(--ink);
}

.player-close > svg {
  width: 1.25rem;
  height: 1.25rem;
}

/* The episode, as large as the window allows and never cropped. */
.player-stage {
  display: grid;
  flex: 1;
  min-height: 0;
  place-items: center;
}

.player-video {
  width: 100%;
  height: 100%;
  background: #000;
  object-fit: contain;
}

/* Said where the episode would have been, in the page's sentence voice. */
.player-message {
  margin: auto;
  max-width: min(90vw, 36rem);
  padding: var(--space-5);
  font-family: var(--font-mono);
  font-size: var(--text-body-small);
  font-weight: 600;
  line-height: var(--leading-body);
  text-align: center;
}

.player:not([data-state]) .player-message,
.player[data-state="failed"] .player-stage {
  display: none;
}

/* A dropped stream being reloaded: the picture stays up, and the word is said
   under it rather than over the place it is coming back to. */
.player[data-state="reconnecting"] .player-message {
  margin: 0 auto;
  padding: var(--space-3) var(--space-4);
  color: var(--ink-dim);
  font-weight: 400;
}

.player-sign-in {
  color: var(--interaction);
}

/* ---- What the page says when something went wrong ---------------------- */

/*
 * Fixed, so it is visible from wherever in 91 cards the control was pressed —
 * a message drawn beside the card could be off-screen.
 */
.play-status {
  position: fixed;
  inset: auto 0 var(--space-5);
  /* Above the drawer and its scrim: a press can come from either the grid or the
     dossier, and a failure has to be readable wherever it was made. */
  z-index: var(--layer-status);
  width: fit-content;
  max-width: min(90vw, 36rem);
  margin: 0 auto;
  padding: var(--space-3) var(--space-5);
  border: 1px solid var(--edge-strong);
  /*
   * The page's own voice sits in the interaction role, and stays there in every
   * tone. A message about what the library is doing right now is not the series,
   * not the play action and not a favourite, so borrowing the orange would blur
   * every one of the things it already means. A failure is told apart by weight
   * and by its words instead of by a colour of its own.
   */
  border-left: 3px solid var(--interaction);
  border-radius: var(--radius);
  background: var(--surface-lifted);
  box-shadow: var(--shadow-lift);
  color: var(--ink);
  font-family: var(--font-mono);
  /*
   * The one place in the page set as a sentence rather than as a stamped label:
   * these are whole messages, and uppercasing one would make it shouty.
   */
  font-size: var(--text-body-small);
  line-height: var(--leading-body);
  text-align: center;
}

/* A failure has to be noticed. */
.play-status[data-tone="failed"] {
  border-left-width: 6px;
  padding-left: calc(var(--space-5) - 3px);
  font-weight: 600;
}

/* ---- What a card says at rest ------------------------------------------ */

.card-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4) var(--space-4);
}

/* Centred rather than set on a baseline: the right of this row is a glyph beside a
   numeral now, and an icon has no baseline worth aligning a code to. */
.card-identity {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin: 0;
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: var(--text-dossier);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--dossier-tracking);
  text-transform: uppercase;
}

/* `01x01`, never `01X01` — the lowercase x is part of the canonical episode code. */
.card-code {
  color: var(--ink-dim);
  text-transform: none;
}

/*
 * How often this episode has come round, at the right of the code row — the
 * position and the faint ink the runtime held, which is the position already
 * looked at and a weight that does not compete with the title.
 *
 * Numerically tracked rather than at the row's own 0.18em: `×3` stops reading as a
 * number when it is tracked out like a field name.
 */
.card-watch-count {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  letter-spacing: var(--dossier-tracking-numeric);
  white-space: nowrap;
}

/* The size the Favourites chip draws its heart at, which is the page's one size
   for a glyph set beside stamped mono — a little above the type, because an
   outline eye at 11px closes up into a zero. Well under the mark size: this is a
   glyph in a line of text, not a button standing in one. */
.card-watch-count > svg {
  width: 0.875rem;
  height: 0.875rem;
}

/*
 * The title and the two marks, on one row: the marks are laid out beside the title
 * rather than positioned over it, so a two-line title can never run underneath
 * them, and the title keeps its own two lines of room whatever they need.
 */
.card-title-row {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  /* One size for both toggles, set here because this is the surface that holds
     them — the drawer sets its own. Well inside the two lines of room the title
     beside it keeps, so the marks can grow without stepping the card's height. */
  --mark-size: 1.875rem;
}

/* Down onto the title's last line: `flex-end` aligns the group with the bottom of
   that line's box, which is a descender's worth below the letters the marks are
   meant to sit beside. */
.card-title-row .mark-group {
  margin-bottom: -0.15em;
}

.card-title {
  flex: 1;
  min-width: 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  /* Two lines' worth of room whether the title needs it or not, so a row of
     cards keeps one baseline instead of stepping up and down. */
  min-height: calc(2em * var(--leading-tight));
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-card-title);
  /* A step below --display-weight: at 15px, 800 fills in and stops being read. */
  font-weight: 700;
  letter-spacing: var(--display-tracking);
  line-height: var(--leading-tight);
}

/*
 * The title is the link that opens the dossier, and is drawn as the title and
 * nothing else — no colour of its own and no underline. The card is what shows it
 * is pressable, by lifting; the link is there so the keyboard can reach it, so a
 * new tab and a bookmark work, and so an episode has a real address.
 */
.card-open {
  color: inherit;
  text-decoration: none;
}

/* ---- The detail drawer -------------------------------------------------- */

/*
 * The dossier for one episode, fixed to the right edge and full height at every
 * width.
 *
 * What changes with the window is whether the page makes room for it. Above
 * 1100px the body's own right padding is widened by exactly the drawer's width,
 * so the grid — which is `auto-fill`, and so has never known how many columns it
 * has — reflows to fewer of them and no card ends up behind the drawer. Below
 * that there is not room for both, and it becomes an overlay over a dimmed page.
 *
 * Closed, it is `visibility: hidden` rather than `display: none`. That way it can
 * slide instead of appearing, and nothing inside it is focusable or read aloud
 * while it is away — which is what makes it safe to leave the last episode's
 * dossier in the markup rather than tearing it down on every close.
 */
.drawer {
  position: fixed;
  inset: 0 0 0 auto;
  z-index: var(--layer-drawer);
  display: flex;
  flex-direction: column;
  width: var(--drawer-width);
  border-left: 1px solid var(--edge-strong);
  background: var(--surface-raised);
  box-shadow: var(--shadow-drawer);
  visibility: hidden;
  transform: translateX(100%);
  /* Visibility is a discrete property, so it steps rather than fades: held to the
     end of the slide out, and dropped at the start of the slide in by the rule
     below. */
  transition:
    transform var(--duration-reveal) var(--ease),
    visibility var(--duration-reveal) step-end;
}

body[data-drawer="open"] .drawer {
  visibility: visible;
  transform: none;
  transition:
    transform var(--duration-reveal) var(--ease),
    visibility 0s;
}

/*
 * The strip that survives every step: where in the season this episode is, the
 * way to either side of it, and the way out. Kept out of the scrolling dossier so
 * that stepping through a season never means scrolling back up to find the button
 * that got you there.
 */
.drawer-chrome {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--edge);
  background: var(--surface-lifted);
}

/* The position indicator, in the dossier voice — a stamped field, not a sentence. */
.drawer-place {
  margin: 0;
  color: var(--ink-dim);
  font-family: var(--font-mono);
  font-size: var(--text-dossier-small);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--dossier-tracking);
  text-transform: uppercase;
  white-space: nowrap;
}

/* An edge of the season: still there, still readable, plainly nowhere to go.
   Everything else about a step is the stamped-control recipe above the library
   controls, which these are one more of. */
.drawer-step:disabled {
  border-color: var(--edge);
  color: var(--ink-faint);
  cursor: default;
}

/* Pushed to the far edge, where a close button is looked for. */
.drawer-close {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  margin-left: auto;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 50%;
  background: none;
  color: var(--ink-dim);
  cursor: pointer;
  transition:
    border-color var(--duration-hover) var(--ease),
    color var(--duration-hover) var(--ease);
}

.drawer-close:hover {
  border-color: var(--edge-strong);
  color: var(--ink);
}

.drawer-close > svg {
  width: 1.125rem;
  height: 1.125rem;
}

/*
 * The dossier itself: the only thing that scrolls, and it contains its own
 * overscroll so reaching its end does not start scrolling 91 cards behind it.
 */
.drawer-dossier {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-5) var(--space-5) var(--space-8);
}

/*
 * The still, at the width of the drawer. It is the same 400×225 file the card
 * shows — the only one the media folder holds — so it is running a little past
 * its own resolution here. That is the trade the read-only guarantee asks for:
 * the alternative is generating a larger derivative beside the media.
 */
.drawer-media {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
}

.drawer-thumb {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--thumb-aspect);
  object-fit: cover;
  background: var(--surface-sunk);
}

.drawer-identity {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin: 0;
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: var(--text-dossier);
  letter-spacing: var(--dossier-tracking);
  text-transform: uppercase;
}

/* `02x11`, never `02X11` — the lowercase x is part of the canonical episode code. */
.drawer-code {
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--dossier-tracking-numeric);
  text-transform: none;
}

.drawer-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-tracking);
  line-height: var(--leading-tight);
}

/*
 * The numbers, in one row divided by dots the stylesheet draws rather than the
 * markup carrying separator characters — a field the scrape did not provide is
 * absent from the row, and an absent field must not leave a dot behind it.
 */
.drawer-facts {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
  margin: 0;
  color: var(--ink-dim);
  font-family: var(--font-mono);
  font-size: var(--text-dossier);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--dossier-tracking-numeric);
}

.drawer-facts > * + *::before {
  content: "·";
  margin-right: var(--space-3);
  color: var(--ink-faint);
}

.drawer-rating {
  color: var(--ink);
}

/*
 * What the record says about this episode, and the two marks that change it. The
 * marks are the card's own, larger but always visible: there is no hover to reveal
 * them here, and a panel opened deliberately about one episode is exactly where
 * making a mark should cost nothing.
 */
.watch-detail {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--edge);
  border-bottom: 1px solid var(--edge);
  --mark-size: 2.25rem;
}

.watch-detail .mark {
  opacity: 1;
}

.watch-history {
  margin: 0;
  color: var(--ink-dim);
  font-size: var(--text-body-small);
}

.drawer-synopsis {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  font-size: var(--text-body-small);
  line-height: var(--leading-body);
}

.drawer-synopsis > p {
  margin: 0;
}

.drawer-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.drawer-section-name,
.about-heading {
  margin: 0;
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: var(--text-dossier-small);
  font-weight: 400;
  letter-spacing: var(--dossier-tracking);
  text-transform: uppercase;
}

/*
 * The briefing-document layout: a stamped field name and its value beside it,
 * label column first. Two columns rather than a definition list stacked, because
 * these are short values being scanned down rather than read across.
 */
.dossier {
  display: grid;
  grid-template-columns: minmax(6.5rem, auto) 1fr;
  gap: var(--space-2) var(--space-4);
  margin: 0;
}

.dossier-label {
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: var(--text-dossier-small);
  letter-spacing: var(--dossier-tracking);
  line-height: var(--leading-body);
  text-transform: uppercase;
}

.dossier-value {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--text-dossier);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--dossier-tracking-numeric);
  line-height: var(--leading-body);
}

/*
 * The people. Chips rather than a comma-separated line because each one is a way
 * of moving around the library — clicking one searches for that person — and
 * because a guest cast of nine reads as nine things this way instead of as a
 * sentence.
 */
.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
}

/*
 * Stamped like the play actions' secondary buttons, and answering under the
 * pointer the same way: these are the one thing in the dossier that is pressable
 * rather than read, so they say so in the interaction role.
 */
.chip {
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--edge-strong);
  border-radius: var(--radius-tight);
  background: var(--surface-sunk);
  color: var(--ink-dim);
  font-size: var(--text-body-small);
  line-height: var(--leading-tight);
  text-decoration: none;
  cursor: pointer;
  transition:
    border-color var(--duration-hover) var(--ease),
    color var(--duration-hover) var(--ease);
}

.chip:hover {
  border-color: var(--interaction);
  color: var(--ink);
}

/*
 * The dim behind an overlaid drawer, and the way it closes by pointer — the grid
 * behind it is not what is being clicked. Declared unconditionally and switched
 * off above the breakpoint, rather than declared inside the narrow query, so that
 * no width can land between two queries and leave it live but unstyled.
 */
.drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--layer-scrim);
  background: var(--scrim);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--duration-reveal) var(--ease),
    visibility var(--duration-reveal) step-end;
}

body[data-drawer="open"] .drawer-scrim {
  opacity: 1;
  visibility: visible;
  transition:
    opacity var(--duration-reveal) var(--ease),
    visibility 0s;
}

/*
 * Above the breakpoint: the page keeps its own width and gives the drawer the
 * right edge. Nothing is covered, so nothing is dimmed and the grid stays as
 * clickable as it was — which is the whole reason this is a drawer and not a
 * modal.
 */
@media (min-width: 1100px) {
  /*
   * Deliberately not transitioned, while the drawer beside it slides. Animating
   * this padding would re-lay-out 91 cards on every frame of the slide — the grid
   * is `auto-fill`, so each frame is a fresh column count — and a grid that
   * stutters for 220ms is a worse thing to watch than one that reflows at once.
   */
  body[data-drawer="open"] {
    padding-right: calc(var(--page-gutter) + var(--drawer-width));
  }

  /* Centred on what is left of the page rather than on the window. */
  body[data-drawer="open"] .play-status {
    right: var(--drawer-width);
  }

  .drawer-scrim {
    display: none;
  }
}

/*
 * Below it: there is no width to give away, so the drawer covers the page and the
 * page is dimmed to say so.
 */
@media (max-width: 1099.98px) {
  .drawer {
    width: min(100%, 28rem);
  }

  /* The page behind a scrim does not scroll: it is not what is being read. */
  body[data-drawer="open"] {
    overflow: hidden;
  }
}
