/*
  MovieCMS — hand-written CSS, no build step, no framework (ADR 0001, ADR 0012).
  Budget: 12 KB gzipped. Every class here is load-bearing on at least one of
  the nine page types; nothing is speculative (YAGNI).

  One grid rule serves every poster list: .poster-grid. aspect-ratio holds the
  layout before an image decodes; explicit width/height on every <img> is the
  other half of that contract and lives in the templates, not here.
*/

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

:root {
  --bg: #fff;
  --fg: #14171a;
  --muted: #5b6570;
  --line: #e3e7ea;
  --line-strong: #c7cdd2;
  --accent: #0b6e3f;
  --accent-fg: #fff;
  --card-bg: #f7f8f9;
  --danger: #a4222a;
  --radius: 6px;
  --gap: 12px;
  /* A phone-shaped column at every width. 95% of this site's visitors are on
     a phone, so the desktop view is the same layout in the middle of a wider
     screen rather than a second design nobody sees. Everything below is built
     for this one column, which is why there is almost no media query left. */
  --maxw: 640px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14171a;
    --fg: #eef1f3;
    --muted: #9aa5ae;
    --line: #2a3138;
    --line-strong: #3a434b;
    --accent: #19935a;
    --accent-fg: #fff;
    --card-bg: #1b2024;
    --danger: #e0616a;
  }
}

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
}

img { max-width: 100%; display: block; }

a { color: inherit; }

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--accent);
  color: var(--accent-fg);
  padding: 10px 16px;
  z-index: 100;
}
.skip-link:focus { left: 8px; top: 8px; }

.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 16px; }

/* ---------------------------------------------------------------- header */

.site-header {
  background: var(--card-bg);
  border-bottom: 1px solid var(--line);
  padding: 10px 0;
}
.header-bar { display: flex; align-items: center; gap: 12px; }
.brand { font-size: 1.125rem; font-weight: 700; text-decoration: none; margin-right: auto; }

.header-actions { display: flex; gap: 4px; }

/* 44px is the target minimum, and an icon button is the one control where it
   is easy to lose: the glyph is 22px and the box has to be twice that. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  cursor: pointer;
}
.icon-btn svg { width: 22px; height: 22px; }
.icon-btn:hover, .icon-btn:focus-visible { background: rgba(127, 127, 127, .16); }

.search-form { display: flex; margin: 8px 0 0; position: relative; }
.search-input {
  flex: 1;
  min-width: 0;
  min-height: 42px;
  padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius) 0 0 var(--radius);
  font: inherit;
  background: var(--bg);
  color: var(--fg);
}
.search-submit {
  min-height: 42px;
  min-width: 44px;
  padding: 0 14px;
  border: 1px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--accent);
  color: var(--accent-fg);
  font: inherit;
  cursor: pointer;
}

/* Only collapsed once app.js has tagged <html class="js">. With scripts off
   the form is a plain, always-visible <form> that GETs /search/ — the icon
   that would reveal it would do nothing, so nothing is hidden. */
.js .search-form { display: none; }
.js .search-form.is-open { display: flex; }

.search-suggestions {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 0 0 var(--radius) var(--radius);
  z-index: 50;
  max-height: 60vh;
  overflow-y: auto;
  list-style: none;
  margin: 0;
  padding: 4px 0;
  display: none;
}
.search-suggestions.is-open { display: block; }
.search-suggestions li a { display: block; padding: 8px 12px; text-decoration: none; font-size: .9375rem; }
.search-suggestions li a:hover, .search-suggestions li a:focus { background: var(--card-bg); }
.search-suggestions .muted { color: var(--muted); font-size: .8125rem; }

/* The search box on /search/ itself, which is a page control rather than
   header chrome and so is never collapsed. */
.search-page { width: 100%; margin: 0 0 20px; }
.js .search-page { display: flex; }

/* ---------------------------------------------------------------- drawer */

/* Without .js this is an ordinary block of links under the header: static,
   visible, no transform, no scrim. Everything that makes it a drawer is
   below, behind .js. */
.drawer { background: var(--card-bg); border-bottom: 1px solid var(--line); }
.drawer-head { display: none; }
.drawer-menu { list-style: none; margin: 0; padding: 0; }
.drawer-menu a {
  display: block;
  min-height: 44px;
  line-height: 44px;
  padding: 0 16px;
  text-decoration: none;
  border-bottom: 1px solid var(--line);
}
.drawer-menu a:hover, .drawer-menu a:focus { color: var(--accent); }

.js .drawer {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 280px;
  max-width: 82vw;
  z-index: 60;
  background: var(--bg);
  border-right: 1px solid var(--line);
  border-bottom: 0;
  transform: translateX(-100%);
  transition: transform .2s ease;
  overflow-y: auto;
  /* The drawer is the one fixed element on the site, so it is the one that
     has to clear a phone's own chrome itself. */
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.js .drawer.is-open { transform: translateX(0); }
.js .drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 8px 8px 16px;
  border-bottom: 1px solid var(--line);
}
.drawer-title { font-weight: 700; }

.drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: 55;
  background: rgba(0, 0, 0, .45);
}
.drawer-scrim[hidden] { display: none; }

/* The page behind an open drawer must not scroll with it. */
body.has-drawer { overflow: hidden; }

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


/* ------------------------------------------------------------ section bar */

/* The heading IS the bar — see pages/home.blade.php for why it is an <h2>
   with a ground rather than a <div> wrapping one.

   Full-bleed inside the column: the negative margin cancels .wrap's 16px
   gutter exactly, so the strip runs edge to edge of the content column at
   every width without needing the 100vmax shadow trick .section--band used. */
.section-bar {
  margin: 28px -16px 12px;
  padding: 8px 16px;
  background: var(--accent);
  color: var(--accent-fg);
  font-size: .875rem;
  font-weight: 700;
  line-height: 1.3;
}
.section-bar:first-child { margin-top: 0; }

/* -------------------------------------------------------------- updates */

/* [category] [name] [quality], one row each. No poster, by design — see
   partials/update-list.blade.php. */
.updates { list-style: none; margin: 0; padding: 0; }
.update {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: inherit;
  /* 44px of touch target from 10px padding plus a 24px line, so the row is
     tappable without a min-height that would fight a two-line name. */
  min-height: 24px;
}
.update:hover .update-name, .update:focus .update-name { color: var(--accent); }

.update-cat {
  flex: none;
  font-size: .75rem;
  font-weight: 700;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: .02em;
}
.update-name { flex: 1; min-width: 0; font-size: .9375rem; }
/* The qualities group: one badge per available rendition. `flex: none` keeps
   it from being squeezed by a long title, and wrapping is allowed so a film
   with four renditions does not push the row sideways on a phone. */
.update-qualities { flex: none; display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; }
.update-quality {
  flex: none;
  font-size: .6875rem;
  font-weight: 700;
  color: var(--muted);
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  padding: 1px 5px;
  white-space: nowrap;
}

/* ------------------------------------------------- title action row */

/* Compact: the row is three secondary controls, so they take the height and
   the type size of a control rather than of a call to action. 36px is still
   above the 24px minimum target and reads as a row rather than three slabs.

   `flex: 0 1 auto` rather than `1 1 auto`: they size to their labels and sit
   together at the left instead of stretching to thirds of the column, which is
   what made three small actions look like three primary ones. */
.title-actions { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
.title-actions .btn {
  flex: 0 1 auto;
  min-height: 36px;
  padding: 0 12px;
  font-size: .8125rem;
  font-weight: 600;
}

/* The readonly field is the no-JavaScript fallback for the two buttons above
   it, so it is present in the markup and hidden only once scripts have
   proved they are running. */
.js .copy-link { display: none; }
/* Shown again when nothing could copy for us — see copyText() in app.js. On
   plain http:// there is no navigator.clipboard, and if execCommand is
   refused as well the only remaining way to get the link is to select it, so
   the field has to come back rather than leave a button that does nothing. */
.js .copy-link.is-revealed { display: flex; }

/* ---------------------------------------------------------------- main */

main { min-height: 50vh; padding: 20px 0 48px; }

.breadcrumbs {
  list-style: none;
  margin: 0 0 16px;
  padding: 0;
  font-size: .8125rem;
  color: var(--muted);
}
.breadcrumbs li { display: inline; }
.breadcrumbs li + li::before { content: " › "; }
.breadcrumbs a { text-decoration: none; }
.breadcrumbs a:hover { text-decoration: underline; }

h1 { font-size: 1.5rem; line-height: 1.3; margin: 0 0 8px; }
h2 { font-size: 1.125rem; margin: 28px 0 12px; }
/* h3 had no rule, so it inherited the UA's 1.17em and rendered *larger* than
   the h2 above it — which is why the FAQ was built out of <summary> instead. */
h3 { font-size: 1rem; margin: 20px 0 8px; }

/* Body copy at a readable measure. Named for what it is rather than borrowed
   from .title-synopsis, which the static pages were reusing. */
.prose { max-width: 70ch; }
.prose a { color: var(--accent); text-decoration: underline; }

/* A muted one-line note under a heading — not an empty state, and not card
   text, which is what .card-meta was being borrowed for. */
.section-note { font-size: .8125rem; color: var(--muted); margin: 0 0 12px; max-width: 70ch; }

.archive-intro { color: var(--muted); max-width: 70ch; margin: 0 0 20px; }
.archive-empty { color: var(--muted); padding: 40px 0; text-align: center; }

/* §15 PERF row: content-visibility skips layout/paint for whatever is not
   near the viewport yet. `contain-intrinsic-size` is not optional alongside
   it — the plain `auto` keyword before the estimate is what tells the
   browser to remember the section's real height once it has rendered it at
   least once, so the placeholder guess below only ever affects the very
   first paint and never turns into a scrollbar that jumps on scroll (a worse
   bug than the paint cost this exists to avoid). Every §05/§12 section below
   the hero and metadata table qualifies — Storyline, Screenshots, Cast,
   Related and the FAQ are all candidates, and this is deliberately one rule
   for all of them rather than one per page type.
   Left out on purpose: .poster-grid and .dl-table on their own — both sit
   directly in the initial viewport on at least one page type each (the
   archive/search/home listing itself, and the title page's own download
   table on a short viewport), where "below the fold" does not hold and an
   estimate could only ever be wrong in the direction that costs a jump. */
.section { margin-top: 32px; padding-top: 24px; border-top: 1px solid var(--line); content-visibility: auto; contain-intrinsic-size: auto 480px; }

/* ---------------------------------------------------------------- cast index */

/* One row per person, in the same language as the home page tiles: a bordered
   row, something square on the left, a label. A single column at every width —
   the row is a name and a number, so a second column would only make the line
   shorter, not the page denser. */
.cast-rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.cast-row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 64px;
  padding: 8px 14px;
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  text-decoration: none;
  color: var(--fg);
}
.cast-row:hover, .cast-row:focus { border-color: var(--accent); }

.cast-row-photo {
  flex: none;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--card-bg);
}
/* The no-photo case, which is most of the catalogue: a letter, not a request
   for an image we know is missing. */
.cast-row-photo--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  color: var(--muted);
  border: 1px solid var(--line);
}

.cast-row-name { font-weight: 600; }
/* Pushed to the far edge so the counts line up down the page and the eye can
   read the column without reading the names. */
.cast-row-count { margin-left: auto; font-size: .8125rem; color: var(--muted); white-space: nowrap; }

/* ---------------------------------------------------------------- category tiles */

/* Auto-fit rather than a fixed column count: categories are data, so the row
   has to survive an operator adding a fourth and a fifth without a media
   query per count. */
/* A folder, a name, a rule between them — one border per row rather than a
   box each, so the list reads as one thing.

   This used to be a phone-only treatment under a media query, with an
   auto-fit grid of bordered tiles above it. The column is 640px at every
   width now, so the phone treatment is simply the treatment, and the grid it
   overrode has no viewport left to appear at. */
.cat-tiles { list-style: none; display: block; margin: 0; padding: 0; }
.cat-tile {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 48px;
  padding: 12px 2px;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  text-decoration: none;
  color: var(--fg);
  font-weight: 600;
}
.cat-tiles > li:last-child .cat-tile { border-bottom: 0; }
.cat-tile:hover, .cat-tile:focus { color: var(--accent); }
.cat-tile-icon { flex: none; color: var(--accent); }

/* The facts table: every value that links is a real link, and says so in the
   site's own colour rather than in body text with an underline. */
.meta-table a { color: var(--accent); text-decoration: none; }
.meta-table a:hover, .meta-table a:focus { text-decoration: underline; }

/* ---------------------------------------------------------------- chips */

.chips { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 20px; padding: 0; }

/* The A–Z row: 28 chips.
   On a PHONE it scrolls — wrapped it is four ragged lines, and 28 targets do
   not fit across 360px at a tappable size however they are squeezed. */
.chips--rail {
  flex-wrap: nowrap;
  overflow-x: auto;
  gap: 6px;
  /* Room for the scrollbar so the last chip is not sitting under it. */
  padding-bottom: 4px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
.chips--rail > li { flex: 0 0 auto; }

/* On a DESKTOP it wraps instead of scrolling. A scrollbar under a row with
   room to fit is a control the page did not need, and it hides the last
   letters behind a gesture.

   It used to share the width instead — `flex: 1 1 0` on every chip so the row
   ended flush with the column. That arithmetic does not work here and the
   screen showed it: the column is 640px at every width (§15), so 28 buckets
   with their gaps leave about 18px per chip against `min-height: 32px` and a
   999px radius. Every letter rendered as a tall thin oval. Shrinking the text
   to fit would have made 28 targets smaller than a fingertip to solve a
   problem that only exists in one row.

   So two tidy rows of equal squares. Each chip is as wide as it is tall, which
   is what makes an alphabet read as an alphabet rather than as 28 pills of
   different widths — `All` and `#` size to their own content and sit at the
   front. */
@media (min-width: 601px) {
  /* Three up, so six stills are 3x2 and nine are 3x3. An explicit count, not
     auto-fill: auto-fill fitted four 140px columns into the 608px content box
     and left a two-item orphan row, which is what made the strip look broken
     rather than dense. */
  .screenshot-strip { grid-template-columns: repeat(3, 1fr); }
  .chips--rail {
    flex-wrap: wrap;
    overflow-x: visible;
    padding-bottom: 0;
    gap: 6px;
  }
  .chips--rail .chip {
    min-width: 32px;
    justify-content: center;
    padding: 4px 8px;
  }
}
.chip {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 4px 12px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  text-decoration: none;
  font-size: .8125rem;
  color: var(--muted);
}
.chip:hover, .chip:focus { border-color: var(--accent); color: var(--fg); }
.chip.is-active { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }

/* ---------------------------------------------------------------- listing rows */

/* The class is still `poster-grid` and the partial is still poster-grid: it
   is the one rule every poster list on the site shares (§15), and that is the
   part worth keeping. What changed is the rule — one column of rows at every
   width, because the column is 640px everywhere now and a grid inside it is
   just a narrower grid.

   ImageSizes::POSTER_GRID is the matching half and has to move with this: the
   thumbnail is a fixed 64px box, so a `sizes` still describing a 190px grid
   column would fetch the 420w candidate for it. */
.poster-grid { list-style: none; margin: 0; padding: 0; }

/* The rail — related titles, a filmography — is the same row. It used to be a
   denser grid for the bytes; a 64px thumbnail is already below where that
   mattered, so there is nothing left for the variant to do and it stays only
   because callers pass it and it still selects the eager/priority behaviour. */
.poster-grid--rail { list-style: none; margin: 0; padding: 0; }

.card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: inherit;
}
.card:hover .card-title, .card:focus .card-title { color: var(--accent); }

.card-media {
  position: relative;
  flex: none;
  width: 64px;
  aspect-ratio: 2 / 3;
  overflow: hidden;
  border-radius: 4px;
  background: var(--card-bg);
}
.card-media img { width: 100%; height: 100%; object-fit: cover; }

/* Under the title in a row, not over the poster: a 64px thumbnail has no room
   for a legible chip on top of it, and an unreadable badge is worse than the
   text it duplicates. */
.card-badges { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 4px; }
.badge {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--line-strong);
  font-size: .6875rem;
  font-weight: 700;
  padding: 1px 5px;
  border-radius: 4px;
  line-height: 1.4;
}

.card-body { flex: 1; min-width: 0; padding: 0; }
.card-title { font-size: .9375rem; font-weight: 600; margin: 0 0 2px; line-height: 1.3; }
.card-meta { font-size: .75rem; color: var(--muted); margin: 0; }

/* ---------------------------------------------------------------- pagination */

.pagination {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  list-style: none;
  margin: 28px 0 0;
  padding: 0;
  align-items: center;
}
.pagination a, .pagination .current, .pagination .dots {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  text-decoration: none;
  font-size: .875rem;
}
.pagination a:hover, .pagination a:focus { border-color: var(--accent); }
.pagination .current { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.pagination .dots { border-color: transparent; color: var(--muted); }

/* ---------------------------------------------------------------- title page */

.title-hero, .cast-hero { display: flex; gap: 24px; margin-bottom: 8px; flex-wrap: wrap; }
.title-poster, .cast-portrait {
  aspect-ratio: 2 / 3;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--card-bg);
  flex-shrink: 0;
}
.title-poster img, .cast-portrait img { width: 100%; height: 100%; object-fit: cover; }
/* Sized apart on purpose: a person portrait is not a title poster, and while
   the person page borrowed .title-poster it could not be changed without
   moving the title page too. */
.title-poster { width: 200px; }
.cast-portrait { width: 180px; }
.title-info, .cast-facts { flex: 1; min-width: 240px; }
.title-facts { list-style: none; margin: 8px 0 16px; padding: 0; color: var(--muted); font-size: .9375rem; }
.title-facts li { display: inline; }
.title-facts li + li::before { content: " · "; }
.title-synopsis { max-width: 70ch; }
.title-synopsis a { color: var(--accent); text-decoration: underline; }

/* §05's compact credits block, beside the poster. These three rows used to
   sit in the metadata table below; they are the page's densest internal
   linking and §05 puts them in the hero. They are not in both places — a
   duplicated name is a second link to the same person from one page. */
.credit-rows { margin: 0 0 16px; font-size: .9375rem; }
.credit-row { display: flex; gap: 8px; margin-bottom: 4px; }
.credit-label { color: var(--muted); flex: 0 0 72px; }
.credit-rows a { color: var(--accent); }

/* §05's quick download bar. A primary call to action, so it is not a chip
   row: one button per rendition, its size underneath. */

.meta-table { width: 100%; border-collapse: collapse; margin: 8px 0 20px; font-size: .9375rem; }
.meta-table th, .meta-table td { text-align: left; padding: 6px 10px 6px 0; border-bottom: 1px solid var(--line); vertical-align: top; }
/* ...except the last row. Its border-bottom and the following `.section`
   border-top are both 1px of --line, separated by the table's 20px bottom
   margin and the section's 32px top margin — so the page drew two parallel
   rules with 52px of nothing between them, which reads as two empty table
   rows rather than as one divider. The section owns the divider; the table
   stops at its content. */
.meta-table tr:last-child th, .meta-table tr:last-child td { border-bottom: 0; }
/* An auto-layout table grows past width:100% rather than wrapping when a cell
   holds one long unbroken token — an original title, a long tag. On a 360px
   viewport that is a horizontally scrolling page, which §15 forbids. */
.meta-table td { overflow-wrap: anywhere; }
.meta-table th { width: 160px; color: var(--muted); font-weight: 600; }

/* ---------------------------------------------------------------- download table */

/* ── TWO LINES PER RENDITION, AT EVERY WIDTH ──────────────────────────────
   This was a five-column table that stacked to cards under 600px, and the
   card was label-above-value four times over plus a full-width button: about
   ten stacked blocks per rendition, so a film with four renditions filled
   several screens with the same four words repeated.

   The column is 640px everywhere now, so the wide table had no viewport left
   to appear at and the card is simply the layout. Compacted to:

       720p 1280x720                      [ Download 720p ]
       0.3 MB   MP4 - H.264

   The labels are gone from the face of it because the values say what they
   are: a size ends in MB, a codec is a codec, a language is a language. They
   have NOT gone from the accessibility tree — <thead> is still clipped rather
   than display:none, and the explicit roles in the markup are what keep cell
   N associated with column header N once these boxes stop being table boxes.
   See partials/download-table.blade.php; that association is the reason the
   labels were safe to drop visually. */

.dl-table { width: 100%; margin: 10px 0; }
.dl-table, .dl-table thead, .dl-table tbody, .dl-table th, .dl-table td, .dl-table tr { display: block; }

/* Clipped, not display:none. These boxes have stopped being table boxes, so
   the roles in the markup are what keeps the table a table — and display:none
   on the header row would delete the column names from the accessibility tree
   along with the sr-only name of the action column. */
.dl-table thead {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
}

.dl-table tbody { display: flex; flex-direction: column; gap: 8px; }

/* Explicit placement rather than flow: the action has to sit on the first
   line and span both, and the three meta cells have to share the second. DOM
   order is untouched, so the reading order and the header association are
   what they were. */
.dl-table tr {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  column-gap: 10px;
  row-gap: 2px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.dl-table td { border: 0; padding: 0; min-width: 0; }

.dl-table td:nth-child(1) { grid-area: 1 / 1 / 2 / 3; }   /* quality + dimensions */
.dl-table td:nth-child(2) { grid-area: 2 / 1 / 3 / 2; }   /* size */
.dl-table td:nth-child(3) { grid-area: 2 / 2 / 3 / 3; }   /* format */
.dl-table td:nth-child(4) { grid-area: 1 / 3 / 3 / 4; }   /* action, both rows */

.dl-table td:nth-child(2),
.dl-table td:nth-child(3) { font-size: .75rem; color: var(--muted); }

/* A hairline between the three meta values, so they read as three facts and
   not one run-on string. Cheaper than a separator element per cell, and it
   disappears on its own when a cell is empty. */
.dl-table td:nth-child(3)::before { content: '\00b7'; margin-right: 8px; color: var(--line-strong); }

.dl-quality { font-weight: 700; }
.dl-quality .card-meta { display: inline; margin-left: 6px; font-weight: 400; }

.dl-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 14px;
  background: var(--accent);
  color: var(--accent-fg);
  border-radius: var(--radius);
  text-decoration: none;
  font-weight: 600;
  font-size: .8125rem;
  white-space: nowrap;
}
.dl-action:hover, .dl-action:focus { filter: brightness(1.08); }
.dl-unavailable {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--muted);
  font-size: .75rem;
  text-align: right;
}
.dl-empty { color: var(--muted); padding: 16px 0; }
/* A note about a table that has rows in it — .dl-empty is the empty state and
   reads as one. */
.dl-note { color: var(--muted); font-size: .8125rem; margin: 8px 0 0; max-width: 70ch; }

@media (max-width: 600px) {
  /* One step down across the page, and the spacing with it. A phone shows a
     third of the width, so the same type and the same gaps mean a third of
     the content per screen — the page reads as sparse rather than as roomy. */
  body { font-size: .9375rem; }
  h1 { font-size: 1.375rem; }
  h2 { font-size: 1.125rem; }
  h3 { font-size: 1rem; }
  .wrap { padding: 0 12px; }
  .section { margin-top: 24px; padding-top: 18px; }
  .archive-intro { font-size: .875rem; margin-bottom: 12px; }
  .chips { gap: 6px; margin-bottom: 14px; }
  .meta-table th, .meta-table td { padding: 6px 8px; font-size: .875rem; }

  /* The poster centres on a phone and stays left on a wider screen.
     .title-hero is a wrapping flex row: above 600px the poster and the facts
     sit side by side and left is correct. Below it they stack, and a fixed
     200px box left-aligned under a full-width heading reads as a mistake --
     it is the largest thing on the page and the only one not aligned to
     something. `margin-inline: auto` centres the box without touching the
     flex direction, so the facts beneath it keep their left edge. */
  .title-poster, .cast-portrait { margin-inline: auto; }
  .copy-link { max-width: 100%; margin: 10px 0; }
  .site-header { padding: 8px 0; }
  .site-footer { padding: 18px 0; }

  /* A phone is a narrow column: the name is the content, so it takes the
     reading size and everything around it steps down rather than competing. */
  .cast-row { min-height: 56px; gap: 10px; padding: 6px 10px; }
  .cast-row-photo { width: 40px; height: 40px; }
  .cast-row-name { font-size: .875rem; }
  .cast-row-count { font-size: .6875rem; }
  .cat-tile { font-size: .9375rem; }
}

/* Under ~380px the four-track grid runs out of room, so the action drops to
   its own full-width line rather than squeezing the values beside it. */
@media (max-width: 380px) {
  .dl-table tr { grid-template-columns: auto 1fr; }
  .dl-table td:nth-child(1) { grid-area: 1 / 1 / 2 / 3; }
  .dl-table td:nth-child(4) { grid-area: 3 / 1 / 4 / 3; margin-top: 6px; }
  .dl-table .dl-action { width: 100%; }
}

/* ---------------------------------------------------------------- screenshots */

.screenshot-strip {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
  list-style: none;
  margin: 12px 0;
  padding: 0;
}
.shot-thumb { display: block; aspect-ratio: 16 / 9; border-radius: var(--radius); overflow: hidden; background: var(--card-bg); }
.shot-thumb img { width: 100%; height: 100%; object-fit: cover; }

.lightbox {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.9);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: 24px;
}
.lightbox.is-open { display: flex; }
.lightbox img { max-width: 100%; max-height: 90vh; margin: 0 auto; }
.lightbox-close {
  position: absolute;
  top: 16px;
  right: 16px;
  min-height: 44px;
  min-width: 44px;
  border: 0;
  border-radius: var(--radius);
  background: rgba(255,255,255,.15);
  color: #fff;
  font-size: 1.25rem;
}

/* ---------------------------------------------------------------- faq */

/* <details>/<summary>, not a JS accordion: it opens with scripting off and the
   answers are in the DOM for a crawler either way (§15, and the FAQPage node
   on the title page depends on the answer text being real). */
.faq { margin-top: 4px; }
.faq-item { border-bottom: 1px solid var(--line); }
.faq-q { padding: 12px 0; font-weight: 600; font-size: .9375rem; cursor: pointer; }
.faq-q::marker { color: var(--muted); }
.faq-a { margin: 0 0 14px; color: var(--muted); max-width: 70ch; }

/* ---------------------------------------------------------------- cast */

.cast-strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 12px;
  list-style: none;
  margin: 12px 0;
  padding: 0;
}
.cast-person { text-align: center; text-decoration: none; color: inherit; display: block; }
.cast-photo { width: 100%; aspect-ratio: 2 / 3; border-radius: var(--radius); overflow: hidden; background: var(--card-bg); margin-bottom: 6px; }
.cast-photo img { width: 100%; height: 100%; object-fit: cover; }
.cast-name { font-size: .8125rem; font-weight: 600; }
.cast-role { font-size: .75rem; color: var(--muted); }

/* --------------------------------------------------------------- episode still */

/* 16:9, because an episode still is a frame and not a poster. Until this
   existed the episode page had nowhere to put `episodes.still_key` and reused
   the series poster instead. */
.episode-still { width: 320px; max-width: 100%; aspect-ratio: 16 / 9; border-radius: var(--radius); overflow: hidden; background: var(--card-bg); flex-shrink: 0; }
.episode-still img { width: 100%; height: 100%; object-fit: cover; }

/* ---------------------------------------------------------------- copy-link */

/* Narrower: the field holds a URL nobody reads — the button beside it is the
   control. It was taking half the column to display text that exists only to
   be copied. */
.copy-link { display: flex; gap: 6px; max-width: 320px; margin: 12px 0; }
.copy-link input {
  flex: 1;
  /* A flex item that is an <input> takes its automatic minimum size from the
     `size` attribute, not from the box it is in, so without this the field
     refuses to shrink and the row overflows the 328px content box a 360px
     viewport gives. */
  min-width: 0;
  min-height: 40px;
  padding: 0 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  font: inherit;
  font-size: .8125rem;
  background: var(--bg);
  color: var(--fg);
}
.copy-link-btn {
  min-height: 44px;
  padding: 0 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--card-bg);
  color: inherit;
  font: inherit;
}
.copy-link-btn:hover, .copy-link-btn:focus { border-color: var(--accent); }
.copy-link-btn.is-copied { border-color: var(--accent); color: var(--accent); }

/* ---------------------------------------------------------------- forms */

.form { max-width: 560px; }
.form-row { margin-bottom: 16px; }
.form-label { display: block; font-weight: 600; margin-bottom: 4px; font-size: .9375rem; }
.form-hint { color: var(--muted); font-size: .8125rem; margin-top: 4px; }
.form-error { color: var(--danger); font-size: .8125rem; margin-top: 4px; }
.field {
  width: 100%;
  min-height: 44px;
  padding: 8px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  font: inherit;
  background: var(--bg);
  color: var(--fg);
}
textarea.field { min-height: 120px; }
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 20px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-fg);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
}
.btn:hover, .btn:focus { filter: brightness(1.08); }
.btn-secondary { background: transparent; color: var(--fg); border-color: var(--line-strong); }

/* A filled neutral, not an outline and not the accent.
   ── WHY NOT GREEN ──
   The accent is the download, and a production page already carries one green
   button per rendition. Painting Trailer, Copy and Share green too would put
   seven equally loud buttons on the page and leave nothing saying which one
   the visitor came for. Solid, quiet and small says "also available" without
   competing.

   color-mix against --fg rather than a pair of hard-coded hexes, so the fill
   follows the theme into dark mode instead of needing a second rule that
   somebody forgets to change. */
.btn-quiet {
  background: color-mix(in srgb, var(--fg) 8%, var(--bg));
  border-color: transparent;
  color: var(--fg);
}
.btn-quiet:hover, .btn-quiet:focus {
  filter: none;
  background: color-mix(in srgb, var(--fg) 14%, var(--bg));
}

.btn-icon { width: 15px; height: 15px; flex: none; margin-right: 6px; }

/* The receipt after a redirect — including the takedown reference number,
   which is the single most important string on that page and was rendering as
   small muted hint text. */
.form-status {
  border-left: 3px solid var(--accent);
  background: var(--card-bg);
  padding: 12px 14px;
  margin: 0 0 16px;
  border-radius: 0 var(--radius) var(--radius) 0;
}
.form-status + .form-status { margin-top: -8px; }

/* The summary above a bounced form. .form-error is sized for one line under a
   single field; a list of every failure inherited 13px red text and a browser
   default indent. */
.form-errors {
  border-left: 3px solid var(--danger);
  background: var(--card-bg);
  padding: 12px 14px;
  margin: 0 0 16px;
  border-radius: 0 var(--radius) var(--radius) 0;
}
.form-errors p { margin: 0 0 6px; color: var(--danger); font-weight: 600; }
.form-errors ul { margin: 0; padding-left: 20px; font-size: .875rem; }

/* A checkbox and its sentence. Inside .form-label the whole consent sentence
   rendered bold with the box floating at its start. */
.form-check { display: flex; gap: 8px; align-items: flex-start; font-weight: 400; font-size: .9375rem; }
.form-check input { flex-shrink: 0; width: 18px; height: 18px; margin-top: 3px; }

/* The honeypot wrapper. display:none and never .sr-only — .sr-only is visible
   to screen readers, and a honeypot wearing it traps exactly the visitors who
   can least afford to have a takedown notice silently refused. */
.hp-field { display: none; }

.vote-list { list-style: none; margin: 0; padding: 0; }
/* The per-row POST form in a vote row, which otherwise relied on a <form>
   happening to be a block-level child of a flex container. */
.vote-form { margin: 0; }
.vote-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--line); }
.vote-count { font-weight: 700; color: var(--accent); min-width: 44px; text-align: center; }

/* ---------------------------------------------------------------- notices / errors */

.notice-page { max-width: 640px; }
.error-code { font-size: .875rem; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.related-rail { margin-top: 12px; }

/* ---------------------------------------------------------------- footer */

.site-footer { border-top: 1px solid var(--line); margin-top: 48px; padding: 24px 0; color: var(--muted); font-size: .875rem; }
.footer-links { list-style: none; display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0 0 12px; padding: 0; }
.footer-links a { text-decoration: none; }
.footer-links a:hover { text-decoration: underline; }

/* ---------------------------------------------------------------- announcement strip (§03) */

/* Dismissed with a checkbox and :has(), because §15 allows four JS
   behaviours and a line of text is not the fifth. The input is visually
   hidden but still focusable, so the strip closes from the keyboard. */
.announce { display: flex; align-items: center; gap: 12px; margin: 0 0 16px; padding: 10px 14px; border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--card-bg); }
.announce:has(.announce-dismiss:checked) { display: none; }
.announce-dismiss { position: absolute; width: 1px; height: 1px; opacity: 0; }
.announce-text { margin: 0; flex: 1 1 auto; }
.announce-close { cursor: pointer; flex: 0 0 auto; font-size: 1.25rem; line-height: 1; padding: 0 4px; color: var(--muted); }
.announce-dismiss:focus-visible ~ .announce-close { outline: 2px solid var(--accent); outline-offset: 2px; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}
