:root {
  --bg: #fbfbfa;
  --fg: #16161a;
  --muted: #6b7280;
  --line: #e3e3e0;
  --card: #ffffff;
  --slot: #f0f0ee;
  --accent: #1c7c4a;
  --negate: #b3261e;
  --radius: 6px;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #101012;
    --fg: #ececef;
    --muted: #8b8b95;
    --line: #26262b;
    --card: #17171a;
    --slot: #0b0b0d;
    --accent: #3ba76a;
    --negate: #e5605a;
  }
}

* { box-sizing: border-box; }

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

a { color: inherit; text-decoration: none; }
/* Gated to real pointers: on iOS/iPadOS, any `:hover` rule matching a link
   or its ancestor makes the first tap simulate hover (underlining here)
   instead of following it — a second tap is needed to actually navigate.
   `(hover: hover) and (pointer: fine)` only matches devices that can hover
   without tapping, so touch never sees the rule and the first tap just
   works. Every other `:hover` in this file is gated the same way. */
@media (hover: hover) and (pointer: fine) {
  a:hover { text-decoration: underline; }
}

/* ---------------------------------------------------------------- topbar */

.topbar {
  position: sticky;
  top: 0;
  z-index: 10;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}
/* Same max-width/centering as `main` below, so the brand/search/zoom track
   the content column on wide screens instead of the raw viewport edges. */
.topbar-inner {
  display: flex;
  align-items: center;
  gap: 20px;
  max-width: 1400px;
  margin: 0 auto;
  padding: 12px 24px;
}
.brand { font-weight: 650; letter-spacing: -0.01em; }
@media (hover: hover) and (pointer: fine) {
  .brand:hover { text-decoration: none; }
}
.search { flex: 1; max-width: 460px; }
.search input {
  width: 100%;
  padding: 7px 12px;
  font: inherit;
  color: inherit;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
/* Only iOS zooms the viewport on focus for an input under 16px (see
   `.facet-search`'s own copy of this fix) — everywhere else the input can
   stay at the inherited 15px body size instead of running a size larger
   than the text around it for a bug those platforms don't have. */
@supports (-webkit-touch-callout: none) {
  .search input { font-size: 16px; }
}
.search input:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}

/* The search box's suggestions (`/suggest`, driven from app.js): the facet
   pickers' floating card, but as wide as the box rather than its own widest
   option — names and titles run long, and a list that changed width with
   every keystroke would jump about under the reader's eye. On a phone the box
   is too narrow for that, so the media query below hangs it off the whole
   topbar instead. */
.search { position: relative; }
.suggest-list {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 20;
  margin: 4px 0 0;
  padding: 4px;
  max-height: min(480px, calc(100dvh - 80px));
  overflow-y: auto;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  list-style: none;
}
.suggest-list[hidden] { display: none; }
.suggest-head {
  padding: 8px 8px 2px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.suggest-head:first-child { padding-top: 4px; }
/* The Recent heading's Clear, at its right end: small and muted like the
   heading it sits in, but in the case a control is written in, not the
   heading's capitals — it's something to press, not another label. */
.suggest-head:has(.suggest-clear) { display: flex; align-items: baseline; }
.suggest-clear {
  margin-left: auto;
  padding: 0 2px;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: normal;
  text-transform: none;
  font-weight: 400;
  font-size: 12px;
  color: var(--muted);
  cursor: pointer;
}
.suggest-clear:hover, .suggest-clear:focus-visible { color: var(--accent); text-decoration: underline; }
.suggest-option {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 6px 8px;
  font-size: 14px;
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius);
}
.suggest-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A person's best-known title, under their name: what tells apart the
   several people a common name finds, which their years alone rarely do. */
.suggest-sub {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12px;
  color: var(--muted);
}
.suggest-option .meta { flex: none; font-size: 12px; color: var(--muted); }
.suggest-all {
  margin-top: 4px;
  border-top: 1px solid var(--line);
  border-radius: 0 0 var(--radius) var(--radius);
  font-size: 13px;
  color: var(--muted);
}
.suggest-option.active { background: color-mix(in srgb, var(--accent) 12%, transparent); }
@media (hover: hover) and (pointer: fine) {
  .suggest-option:hover {
    text-decoration: none;
    background: color-mix(in srgb, var(--accent) 12%, transparent);
  }
}
.suggest-empty { padding: 6px 8px; font-size: 13px; color: var(--muted); }

/* Only ever a mobile control — see the media query below, which is the sole
   place that turns it on. `[popovertarget]` buttons toggle their panel
   natively, with light-dismiss and Escape-to-close for free, so nothing
   here has to reimplement either — see `#mobile-panel` for how the panel
   itself is anchored to this trigger now that it renders in the top layer. */
.nav-menu-trigger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  user-select: none;
  anchor-name: --nav-menu-trigger;
}
/* Text presentation pinned with U+FE0E on both glyphs, same reasoning as the
   filters triangle: an emoji-font hamburger or X would read as a different
   kind of mark than the rest of the topbar's plain type. The open state is
   read off the popover itself via `:has()` — there is no longer a parent
   `<details>` whose own `[open]` attribute could carry it. */
.nav-menu-trigger::before { content: "\2630\FE0E"; font-size: 16px; color: var(--muted); }
.topbar-inner:has(#mobile-panel:popover-open) .nav-menu-trigger::before { content: "\2715\FE0E"; }

/* `#mobile-panel`'s floating-card look, plus the reset a `[popover]` element
   needs from its own UA stylesheet (which otherwise centres it with
   `inset: 0; margin: auto`) before its own `position-anchor`/`anchor()`
   rule, below, can place it against its trigger. Promoted to the top layer
   by the `popover` attribute itself, so — unlike the old `<details>`
   version — it needs no `z-index` or `position: relative` ancestor to float
   above the rest of the page.

   `display` is deliberately left out of this rule and given only to
   `:popover-open` below. The UA stylesheet already hides a closed popover
   with its own `display: none`, but that rule lives in the UA origin, which
   loses to any author declaration at the same importance regardless of
   specificity — a bare `display: flex` here, unconditional, would have beaten
   it and left the panel rendered open from first paint. */
.nav-menu-panel {
  position: fixed;
  inset: auto;
  margin: 0;
  flex-direction: column;
  gap: 12px;
  min-width: 140px;
  padding: 12px 16px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.nav-menu-panel:popover-open { display: flex; }
#mobile-panel {
  position-anchor: --nav-menu-trigger;
  top: anchor(--nav-menu-trigger bottom);
  right: anchor(--nav-menu-trigger right);
  margin-top: 8px;
}

/* CSS anchor positioning is ~88% supported as of mid-2026 — Chrome/Edge 125+,
   Firefox 131+, Safari 18.2+ — but is progressive enhancement, not a
   requirement: without it, `#mobile-panel`'s `position-anchor`/`anchor()`
   above are simply ignored and it falls back to `.nav-menu-panel`'s own
   `position: fixed`, landing here instead of floating under its trigger.
   Still reachable, still dismissible, just not anchored. */
@supports not (anchor-name: --unseen-anchor-probe) {
  .nav-menu-panel { top: 56px; right: 16px; }
}

/* Poster grid density, drawn like `.segmented`'s bar — a bordered enclosure
   round three things that switch a shared state rather than combine: smaller,
   reset, larger, all moving the same one figure. There is one trio here
   rather than one: it is duplicated into the mobile menu the same way
   browse, collections and sign-out are (see the media query below), because
   it lives in the topbar and the topbar sheds everything but the brand and
   search past 640px. */
.zoom {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
/* Stops it stretching full width in the mobile menu panel, a column flex
   context; `.topbar` is a row, where the default cross-axis centering is
   what's wanted instead. */
.nav-menu-panel .zoom { align-self: flex-start; }
.zoom-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  font: inherit;
  font-size: 15px;
  line-height: 1;
  color: var(--muted);
  background: none;
  border: 0;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .zoom-btn:hover { color: var(--fg); }
}
.zoom-btn + .zoom-btn { border-left: 1px solid var(--line); }
/* An end you are already at, same rule as the pager: dimmed in place rather
   than left clickable to nowhere. */
.zoom-btn:disabled { cursor: default; opacity: 0.32; }
.zoom-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

main { max-width: 1400px; margin: 0 auto; padding: 24px; }

/* The running commit, at the foot of every page but login. Set apart by
   whitespace alone; a rule would only underline the pager above it. */
.site-foot {
  max-width: 1400px;
  margin: 0 auto;
  padding: 8px 24px 32px;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* ---------------------------------------------------------------- login */

.login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}
.login-card {
  width: 260px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  text-align: center;
}
.login-card .brand { margin: 0 0 8px; font-size: 20px; }
.login-card input,
.login-card button {
  width: 100%;
  padding: 8px 12px;
  font: inherit;
  border-radius: var(--radius);
}
.login-card input {
  color: inherit;
  background: var(--card);
  border: 1px solid var(--line);
}
.login-card input:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}
.login-card button {
  color: var(--bg);
  background: var(--accent);
  border: none;
  cursor: pointer;
}
.login-error { margin: -4px 0 0; color: #c0392b; font-size: 13px; }

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

.page-head { margin-bottom: 28px; }
.page-head h1 { margin: 0 0 4px; font-size: 26px; letter-spacing: -0.02em; }
.sub { margin: 0; color: var(--muted); font-size: 14px; }

/* `/collections`' one action, under its header rather than beside it —
   `collection.html`'s own Delete sits in the same below-the-heading slot,
   left-aligned rather than pushed to a right edge nothing else on this
   page reserves for anything. Still the accent colour and not Delete's
   red outline, though: this button creates rather than destroys, so it
   keeps the visual weight `.new-collection-btn` always had, just not the
   position. */
.new-collection { margin-top: 14px; }
/* `.page-head`'s own 28px is the gap to a *section* (the grid, a stats
   table), twice `.new-collection`'s 14px gap to the text just above it —
   fine where the header's last child is a disclosure's plain-text summary
   (`.filters`, `.entity-panel`; unequal padding around a line of text
   goes unnoticed), wrong here where it's a solid button: unequal space
   around a visibly bounded box reads as unbalanced. Scoped to this one
   case rather than changed everywhere `.page-head` sets that 28px. */
.page-head:has(> .new-collection:last-child) { margin-bottom: 14px; }
.new-collection-btn {
  padding: 6px 14px;
  font: inherit;
  font-size: 13px;
  color: var(--bg);
  background: var(--accent);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
}

/* A collection's `<h1>` with a rename control beside it — see `app.js` for
   the click-to-reveal behaviour. `.entity-title` carries the 4px this page's
   plain `h1` would otherwise supply itself (`.page-head h1` above), now that
   the margin has to sit under a flex row rather than under the heading
   alone; `h1` and `.rename-form` both go margin-free so the row's own
   baseline alignment decides their spacing instead. */
.entity-title { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.entity-title h1 { margin: 0; }
/* `display` on `.edit-name` and `.rename-form` is scoped to `:not([hidden])`
   rather than given unconditionally, the same rule `.nav-menu-panel` above
   follows and for the same reason: `app.js` toggles each one's `hidden`
   attribute to swap the title for the rename form, and a bare `display`
   here is an author-stylesheet rule, which beats the UA stylesheet's own
   `[hidden] { display: none }` regardless of specificity — unconditional,
   it would have left both permanently visible no matter what `hidden` said. */
.edit-name:not([hidden]) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: center;
}
.edit-name {
  width: 22px;
  height: 22px;
  padding: 0;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
}
.edit-name svg { width: 14px; height: 14px; fill: currentColor; }
@media (hover: hover) and (pointer: fine) {
  .edit-name:hover { color: var(--fg); }
}
.edit-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Swapped for `.entity-title`'s static half by `app.js`, not sized down to a
   generic form field: the point is that the title itself became editable,
   so the input reads at the same size and weight the heading it replaced
   did. */
.rename-form:not([hidden]) { display: flex; align-items: center; gap: 8px; }
.rename-form { margin: 0; }
.rename-input {
  font: inherit;
  font-size: 26px;
  letter-spacing: -0.02em;
  color: inherit;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0 6px;
  min-width: 160px;
}
.rename-input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.rename-form button {
  padding: 4px 12px;
  font: inherit;
  font-size: 13px;
  color: var(--bg);
  background: var(--accent);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
}
.ext { margin: 10px 0 0; font-size: 13px; display: flex; flex-wrap: wrap; gap: 10px 14px; }
.ext a { color: var(--muted); white-space: nowrap; }
@media (hover: hover) and (pointer: fine) {
  .ext a:hover { color: var(--fg); }
}
.muted { color: var(--muted); }
/* `.ext`'s entries inside a table cell: the same links, right-aligned with
   the column. */
.source-badges { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px 10px; }
.source-badges a { color: var(--muted); white-space: nowrap; }
@media (hover: hover) and (pointer: fine) {
  .source-badges a:hover { color: var(--fg); }
}

/* One rule decides the shape of everything on these pages: a bordered pill is
   something you can press. A fact wears no border — it is text, set in the run
   of words the page reads in anyway.

   The two used to share `.tag`, which is how a genre badge on a title page and
   a resting filter chip on a person page came to be pixel-identical while only
   one of them did anything. Same mistake the sort row made, and the same fix
   (see `sort_chip`): a control wearing a fact's clothes cannot be told from the
   fact, and the reader only finds out by clicking one and watching nothing
   happen. It is the worse direction of the two errors, because the page teaches
   it once and the reader stops trying.

   The rule holds app-wide, which is the test of it: `.bound`, `.segmented` and
   the search box are the other bordered things here, and every one is a
   control. `.tag` was the only violation, so it is gone rather than restyled —
   what it marked is now either a chip or plain text. */
.chips { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.filter-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  padding: 1px 8px;
  font: inherit;
  font-size: 12px;
  color: var(--muted);
  background: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
a.filter-chip, a.sort-chip { text-decoration: none; }
@media (hover: hover) and (pointer: fine) {
  .filter-chip:hover { color: var(--fg); border-color: var(--muted); }
}
/* Unlit is the resting state — most chips are unlit most of the time, so it has
   to read as plain rather than as "switched off". Lit is the marked one,
   because it is the exception and it is an active constraint.
   `aria-current`, not `aria-pressed`: these navigate, and pressed belongs to a
   button. The only element here still carrying pressed is the poster, which
   really is one. */
.filter-chip[aria-current="true"] {
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
}
@media (hover: hover) and (pointer: fine) {
  .filter-chip[aria-current="true"]:hover { color: var(--bg); }
}
/* A chip at zero cannot be followed, so it is a <span> and says so rather
   than being a link that refuses. */
.filter-chip[aria-disabled="true"] {
  cursor: default;
  opacity: 0.32;
  border-style: dashed;
}
/* A facet's only value (`Chip.sole`: a single-role filmography's Role chip)
   is disabled for a different reason — it is the whole grid, not none of it
   — and it is there to be read, so it dims only enough to show it can't be
   clicked. */
.filter-chip[aria-disabled="true"][data-sole] { opacity: 0.6; }
.filter-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.chip-count { font-size: 10.5px; font-variant-numeric: tabular-nums; opacity: 0.7; }

/* Wraps because four segmented controls make this row long enough to overflow
   a phone, and the row is a set of independent controls rather than one
   sentence — it breaks between them cleanly. The row gap is tighter than the
   column gap so a wrapped row still reads as one band, and the column gap is
   wide enough that Seen, Bookmarked, Plex and sort read as four bars and not
   one long one.

   The filter panel leads and this row follows it now, so this gap is to
   `.filters` above rather than to the heading, and it matches the 14px
   `.filters` takes above itself in turn. */
.controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 20px;
  margin-top: 14px;
}

/* Sets sort apart from Seen, Bookmarked and Plex at the far edge of the line,
   the one control here that is not a filter drawn beside three that are.
   `flex-wrap` still breaks the row where it must — the auto margin only
   claims space on whichever line sort lands on — and the phone rule below
   resets it, since a stacked column has no "far edge" for it to claim. */
.control-sort { margin-left: auto; }
.sort-bars { display: inline-flex; align-items: center; gap: 8px; }

/* ------------------------------------------------------- the controls row */

/* A label and the bar it names — the shape of every item in the row: SEEN,
   PLEX, SORT. One class for the three because they are one thing, and the
   sort was only ever the sole occupant out here; a `.sort` wrapper holding a
   Plex control would be the name lying about what it wraps.

   Each is one control with segments, not several controls — because that is
   what each is, and because the filter chips in the panel are the other
   thing. The two read as different vocabularies rather than one style applied
   twice:

     gaps between pills   a set you add to and subtract from
     one bar, divided     one slot, n things that can occupy it

   Which means the shape carries the invariant. A row of separate boxes says
   "combine these however you like", and none of these combine — the sort has
   exactly one segment lit at all times, and Seen and Plex have at most one —
   so the enclosure is the honest drawing of them. Left as chips, the only way
   to learn a control was single-select was to click a second segment and
   watch the first go out. */
.control-group {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* A segmented control broken across two lines is a broken-looking control,
     where gapped chips would have wrapped without comment. `.controls` wraps
     around each of these as one item instead. */
  white-space: nowrap;
}
/* The panel's label treatment, minus the gutter — `.facet-label` sets a width
   and a top pad to line a column of labels up, and there is no column out here.
   What carries over is the part that means something: a naming word sits a step
   below the controls it names, the way the facet labels sit below their chips
   and bounds. This read 13px, which was a step *above* its own 12px segments —
   so the one word in the row you cannot operate was dressed heavier than the
   thing you can. */
.control-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.segmented {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* Segment fills are square; the enclosure supplies the rounded ends. */
  overflow: hidden;
}

/* Release, Seen and Plex switch rather than combine (see
   `FacetView.exclusive`), so they wear this enclosure instead of `.chips`'s
   gaps — the same "one slot, not a set you add to" shape sort uses, and for
   the same reason. It holds wherever they are drawn: Release inside the
   panel as a facet row, Seen and Plex out in the controls row through
   `mode_control`. What does not carry over is the arrow:
   that marks the one thing sort's lit segment can still do that these
   can't, a second click that leaves it lit. Here the lit segment behaves
   like every unlit one — a click always leaves it — so nothing here should
   look like it promises otherwise. The chips themselves stay `.filter-chip`,
   count badge and all; only the border, corners and disabled treatment
   change shape to fit a bar instead of a standalone pill. */
.segmented .filter-chip {
  border: 0;
  border-radius: 0;
}
.segmented .filter-chip + .filter-chip { border-left: 1px solid var(--line); }
/* No dashed border in a bar — a segment can't stand apart from its
   neighbours the way a lone disabled pill does, so it is dimmed in place. */
.segmented .filter-chip[aria-disabled="true"] { border-style: none; }
/* Inset, for the reason `.sort-chip:focus-visible` already is: the
   enclosure clips overflow, and an outset ring would be cut off on the
   first and last segment. */
.segmented .filter-chip:focus-visible { outline-offset: -2px; }

.sort-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  padding: 1px 8px;
  font: inherit;
  font-size: 12px;
  color: var(--muted);
  background: none;
  border: 0;
  cursor: pointer;
}
/* The dividers, and only between segments — the outer edges belong to the
   enclosure, and a border on each segment would double up at every seam. */
.sort-chip + .sort-chip { border-left: 1px solid var(--line); }
@media (hover: hover) and (pointer: fine) {
  .sort-chip:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 6%, transparent); }
}
.sort-chip[aria-current="true"] {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
/* Inset, because the enclosure clips overflow and an outset ring would be cut
   off on the first and last segments — the two most likely to be tabbed to. */
.sort-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The width is held whether or not there is an arrow in it. It was keeping the
   row from shifting under the cursor; inside an enclosure it also keeps the
   dividers still, which is what the eye tracks the control by. */
.sort-arrow {
  display: inline-block;
  min-width: 0.7em;
  font-size: 10px;
}

/* -------------------------------------------------------------- filters */

/* This panel now leads the row of controls beneath it, so the gap is to the
   heading above rather than to anything of `.controls`' — see the comment
   there for the matching 14px on the other side. */
.filters { margin-top: 14px; }
/* Shut, the panel is a single summary line, and the 14px above and below it
   (see `.controls`) already reads as the boundary. Open, it is a page's worth
   of chips and bound fields ending right where the controls row begins with
   nothing but that same margin between them — so this hairline draws the
   edge the margin alone stopped marking once there was a form's worth of
   content pressed up against it. */
.filters[open] {
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
/* Wraps, because the summary stopped being two short words. It now carries the
   names of everything set inside the panel, and on a narrow screen it needs to
   break somewhere. A flex row breaks between whole items — triangle, 'Advanced
   filters', the summary text, 'clear' — each landing on its own line whether
   or not it fits several to a line, which is where an iPhone SE saw them.
   Plain inline flow instead, so the triangle through 'clear' reads as one
   running sentence and wraps at whatever word happens to run out of room,
   the same way it already read as a single line on wider screens.
   `fit-content` still keeps the click target off the full width on a wide
   screen; `max-width` is what stops it demanding more than there is. */
.filters > summary {
  display: block;
  width: fit-content;
  max-width: 100%;
  padding: 3px 0;
  font-size: 13px;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  list-style: none;
}
.filters > summary::-webkit-details-marker { display: none; }
.filter-count:empty { display: none; }
/* U+25B6, the full-size triangle, and not U+25B8 — its 'small' sibling, which
   is a codepoint drawn deliberately under its em box. At 10px on a 13px line
   that one was shrunk twice over, and came out the size and colour of the
   middots the summary sets between the filters it names. That is the wrong mark
   to lose: this one is the control.

   8px, and the drop is the point rather than a retreat from it. The shape is
   carrying the distinction now, so the glyph needs less type size than the
   understated one did to read as unmistakably a triangle — small and clearly
   pointing beats larger and merely round.

   If it ever renders as a coloured play button, a font's emoji glyph has won
   the fallback — "▶\FE0E" pins the text presentation. */
.filters > summary::before {
  content: "▶";
  display: inline-block;
  position: relative;
  top: -1px;
  font-size: 8px;
  margin-right: 6px;
  vertical-align: middle;
  transition: transform 0.12s ease;
}
.filters[open] > summary::before { transform: rotate(90deg); }

/* `apply` and `clear` in one voice because they are one kind of thing:
   momentary, with nothing to show for themselves afterwards. `clear` empties
   the whole panel, so it stays singular up in the summary; `apply` now sits
   once per row instead — see `.apply-bound` — but reads identically, because
   a row's figure is no less "the whole panel, deferred" for living next to
   just one axis of it.

   Which is why neither takes a chip's pill. Dressed as a chip it was the
   unlit chip exactly — same 12px, same grey, same hairline, same 999px — and
   that is the app-wide error the chips themselves are drawn to avoid (see
   `.filter-chip`). A chip carries state: it lights, it stays lit, and it
   applies the instant it is clicked. This one can never light, and the
   promise of instant effect is the one thing the typed bounds cannot keep.
   Underlined text is the shape with no state in it.

   Quiet on purpose, too. Enter in any field submits the form on desktop,
   so `apply` is mainly what a touch keyboard has no equivalent of, and
   stays out of the way until there is something for it to do — only the
   row most recently typed in ever shows it (see `app.js`), since it is the
   same submit from any row and one visible at a time is the button, not four
   copies of it. */
.apply-bound {
  display: none;
  padding: 0;
  font: inherit;
  font-size: 12px;
  color: var(--muted);
  background: none;
  border: 0;
  text-decoration: underline;
  cursor: pointer;
}
.facet.dirty .apply-bound { display: inline; }
@media (hover: hover) and (pointer: fine) {
  .apply-bound:hover,
  .clear-filters:hover { color: var(--fg); }
}
.apply-bound:focus-visible,
.clear-filters:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.clear-filters {
  margin-left: 6px;
  padding: 0;
  font: inherit;
  font-size: 12px;
  color: var(--muted);
  background: none;
  border: 0;
  text-decoration: underline;
  cursor: pointer;
}

.facets {
  /* The label gutter used to be a hand-tuned pixel width, re-tuned (and
     re-broken) every time a longer label showed up — Language, then
     Collection. A grid measures it instead: each `.facet` hands its label
     and its one content sibling straight to the grid as `display: contents`,
     so the first column is `max-content` off whatever labels actually exist
     today, not a guess about them. Nothing here has to be touched again for
     a longer label to line up. */
  --facet-gap: 8px;
  display: grid;
  grid-template-columns: max-content 1fr;
  align-items: baseline;
  /* A grid item stretches to fill its track by default — flex items don't,
     so nothing here needed this said out loud until the row stopped being a
     flex row. Release's segmented pair is the one that shows it: without
     this its border ran the full second column instead of hugging its two
     chips. */
  justify-items: start;
  row-gap: 10px;
  column-gap: var(--facet-gap);
  margin-top: 12px;
}
.facet { display: contents; }
/* The row's one content sibling, whatever it is (`.facet-chips`, `.chips`,
   `.segmented`, `.facet-body`) — picked off its position after the label
   rather than a shared class, so a bound row's wrapper needs no class of
   its own to shrink correctly in the grid's second column. */
.facet-label + * { min-width: 0; }
/* Whichever facets are long enough to earn a filter box (see `facet_row`'s
   `searchable`): it sits above the chips rather than beside the label, so it
   reads as part of the row it narrows rather than as a second control
   competing with the label for the same baseline. Selected off the DOM
   parent, not the rendered one — `.facet` generates no box of its own to
   inherit alignment from now that it's `display: contents`. */
.facet-searchable > * { align-self: start; }
/* Genre, Country, Language and Collection — the rows whose length nothing can
   predict (see `facet_row`'s comment in macros.html). `.facet-picker` just
   anchors the listbox (`position: relative`); the search box and its tags
   share one wrapping row, `.facet-tags`, with the box leading it so tags
   only ever wrap in after it (see `facet_row`'s comment on why that's the
   one order that never moves the box). */
.facet-picker { position: relative; display: block; min-width: 0; }
.facet-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* The or/and and any/primary switches (`facet_row`'s comment in
   macros.html): a two-segment bar shaped like Release's own — `.segmented`,
   not `.chips` — because both are "one slot, not a set". A small negative
   margin keeps it from reading as a third tag-sized pill sitting a touch
   taller than its neighbours; `.segmented`'s border adds height
   `.filter-chip` alone doesn't carry.

   Where it stops resembling Release: Release can sit truly off — clicking
   the lit chip a second time clears it to no constraint at all — so solid
   fill earns its keep there as "an active constraint, the exception"
   (`.filter-chip[aria-current]`'s own rule). Or/and and any/primary have no
   such off switch; one side is always lit, the same shape Sort's row is in
   for the same reason (a grid is always in some order). Dressed in
   `.filter-chip`'s solid fill regardless, "or" reads as one more lit filter
   sitting beside Comedy and Horror rather than as the row's resting default
   — so the lit segment borrows Sort's quieter tint instead, the treatment
   already reserved for "you are here" rather than "this is constraining
   you". Shape stays `.filter-chip`'s (the bar, the dividers); only the lit
   colour changes, and it wins on specificity alone — no hover variant
   needed, the same as `.sort-chip[aria-current]` needs none. */
.facet-op { margin: -1px 0; }
.facet-op .filter-chip[aria-current="true"] {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
/* The active state is a `<span>`, not a link — clicking it would ask for the
   mode it is already in, so it carries no cursor promising otherwise, the
   same rule a disabled chip follows for the same reason. */
.facet-op .filter-chip:not(a) { cursor: default; }
/* A plain tag — every facet but the four negatable ones — is still a lit
   `.filter-chip` and nothing more, one element, one link. */
.chip-remove { font-size: 13px; line-height: 1; opacity: 0.85; }
/* `.facet-tag` (`facet_tag` in macros.html) wears the same `.filter-chip`
   pill but is a `<span>` holding two links rather than one `<a>`: the body
   flips included/excluded, `×` removes outright, and an anchor cannot nest
   inside another. Neither inner link keeps a browser's default underline —
   `.filter-chip` never wore one, and a link appearing here should not start
   looking like one now that it is real. */
.chip-body, .chip-remove {
  display: inline-flex;
  align-items: baseline;
  color: inherit;
  text-decoration: none;
}
.chip-body { gap: 5px; }
/* The global `a:hover` rule (top of this file) underlines every plain link
   on hover and outranks a bare `.chip-body`/`.chip-remove` by specificity —
   tag plus pseudo-class beats one class alone — so the override has to
   repeat `:hover` to match its weight rather than just restating
   `text-decoration: none` at rest. Both rules are gated the same way. */
@media (hover: hover) and (pointer: fine) {
  .chip-body:hover, .chip-remove:hover { text-decoration: none; }
  .facet-tag:hover .chip-remove { opacity: 1; }
}
.chip-body:focus-visible, .chip-remove:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* Excluded rather than included, on the four negatable facets
   (`GridQuery.toggled`'s two-state loop, included <-> excluded — see
   `GridQuery.removed` for the separate action that actually drops a tag).
   The tag keeps the same pill and the same lit weight — it is no less a
   selection — but wears `--negate` instead of `--accent`. Grouping and
   `but not` (`facet_row`'s comment in macros.html) are what actually say
   which list a tag is in now; the colour is the same click that changes the
   state already changing what it looks like, a second, redundant cue rather
   than the only one. */
.facet-tag[data-negated="true"] {
  color: var(--bg);
  background: var(--negate);
  border-color: var(--negate);
}
/* The word joining the two groups — plain text, not a chip, since it labels
   the split rather than acting on anything. Sized and coloured like
   `.facet-label`'s own weight class rather than the chips either side of
   it, and `nowrap` so it can't break across "but" and "not" if the row
   wraps right where it sits. */
.facet-not {
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}
.facet-search {
  width: 180px;
  max-width: 100%;
  /* Matches `.filter-chip`'s own padding, not just its font-size: the two
     sit in the same left-aligned column, and `.facet-label`'s padding-top
     is already tuned to that chip's 2px inset (1px border + 1px padding).
     A different inset here would need a second tuned constant instead of
     reusing the one that already exists. */
  padding: 1px 8px;
  font: inherit;
  font-size: 12px;
  color: inherit;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.facet-search:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
/* iOS Safari zooms the whole viewport on focus for any input under 16px —
   the same reason `.search input` above is pinned to 16px. Scoped to iOS
   through a feature only its WebKit build reports, rather than bumped
   everywhere, so this keeps matching `.filter-chip`'s 12px on desktop
   Safari and Linux, where the zoom never happens. */
@supports (-webkit-touch-callout: none) {
  .facet-search { font-size: 16px; }
  /* iOS reads the input's own font-size to decide whether to zoom, not the
     placeholder's — `::placeholder` takes a size of its own, so the box can
     stay at 16px there while the greyed-out hint keeps matching the 12px
     chip row it sits above. */
  .facet-search::placeholder { font-size: 12px; }
}
/* Floats over the row rather than pushing its height around every time it
   opens: two hundred-odd Country options would otherwise shove everything
   below Country down the page on every focus. Same border and shadow as
   `.nav-menu-panel`, but anchored the plain way — `position: absolute`
   against this element's own positioned ancestor — since this list lives
   inline in the page rather than in the top layer, and has no trigger
   sitting elsewhere for `.nav-menu-panel`'s `anchor()` rules to target.

   `width: max-content` so Genre's twenty-odd short labels don't pay for
   Language's long ones — each list is only as wide as its own widest
   option. The floor is the search box's own width, not a guessed number:
   a list with nothing wider than "Comedy" still reads as one control with
   the box beneath it rather than a dropdown narrower than what opened it. */
.facet-listbox {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  margin: 4px 0 0;
  padding: 4px;
  width: max-content;
  min-width: 180px;
  max-width: min(320px, 100%);
  max-height: 260px;
  overflow-y: auto;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  list-style: none;
}
.facet-listbox[hidden] { display: none; }
.facet-listbox li[hidden] { display: none; }
.facet-option {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 5px 8px;
  font-size: 12px;
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius);
}
/* The bookmarks page's picked collection (`.collection-pick` in app.js): a
   selected tag like browse's, but its body is plain text rather than a link
   and its `×` a button rather than a link, so both need `<a>`'s look back.
   `.filter-chip` and `.facet-picker` each set a `display`, which outranks
   the `hidden` attribute's own `display: none`, so the swap between the tag
   and the search box restates it. */
.collection-pick-tag { cursor: default; }
.collection-pick-tag[hidden],
.collection-pick .facet-picker[hidden] { display: none; }
button.chip-remove {
  padding: 0;
  font: inherit;
  font-size: 13px;
  background: none;
  border: 0;
  cursor: pointer;
}
/* A button rather than a link where picking an option selects it instead of
   navigating — the bookmarks page's Collection picker. Stripped back to
   look exactly like the `<a>` every other picker's options are. */
button.facet-option {
  width: 100%;
  font-family: inherit;
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
}
a.facet-option.active,
button.facet-option.active { background: color-mix(in srgb, var(--accent) 12%, transparent); }
@media (hover: hover) and (pointer: fine) {
  a.facet-option:hover,
  button.facet-option:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
}
/* Same rule `.filter-chip[aria-disabled]` follows for a chip driven to zero:
   dimmed and inert rather than a link that refuses. No dashed border here —
   a listbox row can't stand apart from its neighbours the way a lone pill
   can, so it's dimmed in place instead, the way a segment is. */
span.facet-option { opacity: 0.32; }
.facet-listbox-empty { padding: 5px 8px; font-size: 12px; color: var(--muted); }
.facet-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  padding-top: 2px;
}
/* A bound row's content: the fields and the apply button that follows them,
   wrapped together so the row still has exactly one thing beside its label —
   what lets `.facet` stay a plain `display: contents` pair in the grid
   without a bound row needing a grid-column of its own. */
.facet-body { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: var(--facet-gap); }
/* The numeric facets. Deliberately quiet — the words around the boxes are the
   same muted grey as a resting chip, so the row of them reads as one panel with
   two kinds of control in it rather than as a form bolted under the chips. What
   marks a bound as set is the same thing that marks a chip as lit: ink where
   there was none. */
.bounds {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  /* Matches `.bound-field`'s own gap: a facet with two fields (Year, IMDb)
     reads as one flowing line — word, box, word, box — and a wider gap here
     than inside a field would put more air after a box than before it. */
  gap: 4px 5px;
}
.bound-field {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  font-size: 12px;
  color: var(--muted);
  cursor: text;
}
.bound-word { user-select: none; }

.bound {
  width: 5.5em;
  padding: 1px 6px;
  font: inherit;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.bound:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
/* The placeholder is the extent of the data, which is exactly what an empty
   field behaves as — so it has to sit a clear step back from a typed figure, or
   the field reads as already set. */
.bound::placeholder { color: var(--muted); opacity: 0.55; }
/* Spinners off. At 12px they are wider than the two digits beside them and they
   invite dragging at a precision nobody wants on a filmography's worth of
   years. Nothing is lost: the arrow keys still step the value, which is the
   affordance a keyboard was using anyway. */
.bound::-webkit-inner-spin-button,
.bound::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.bound { -moz-appearance: textfield; appearance: textfield; }

/* ------------------------------------------------------------------- grid */

/* `--poster-size` is set inline, on the root, only once a reader has actually
   zoomed (see the head script in `base.html` and the click handler in
   `app.js`) — until then it is unset here and the literal fallback below is
   the whole of the rule, exactly as it was before zoom existed. That is why
   the fallback still needs to differ between this and the mobile override
   further down: a reader who has never touched the zoom control gets the
   same two breakpoint defaults as always, not one figure fighting the other
   at the fallback. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--poster-size, 132px), 1fr));
  gap: 18px 14px;
}

.card { position: relative; margin: 0; min-width: 0; }

.poster {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 2 / 3;
  padding: 0;
  overflow: hidden;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease,
              background-color 0.2s ease-out;
}
@media (hover: hover) and (pointer: fine) {
  .poster:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgb(0 0 0 / 0.16); }
}
.poster:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.poster img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: opacity 0.14s ease, filter 0.14s ease;
}
/* A poster that is still arriving would otherwise paint top-down in strips as
   its bytes come in, which on a page of uncached posters is a wall of half
   images. So it stays hidden, leaving the card empty, until its
   onload handler marks it loaded, and then fades in whole.

   Visibility and an animation rather than opacity and a transition, because
   opacity is already the seen state's: a keyframe with only a `from` ends on
   whatever opacity the cascade gives the image, 1 or the seen dimming alike,
   and a hidden rule on visibility has nothing to outrank.

   While it waits, the card is a shade darker than the page rather than the
   white of a card, which stood out from it as a grid of empty tiles. A card
   whose poster turns out not to exist goes back to white for its fallback. */
.poster img:not(.loaded) { visibility: hidden; }
.card:not(.no-poster) .poster:has(> img:not(.loaded)) { background: var(--slot); }
.poster img.loaded { animation: poster-in 0.2s ease-out; }
@keyframes poster-in { from { opacity: 0; } }

/* Text fallback, revealed by the img onerror handler. */
.fallback {
  display: none;
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  padding: 10px;
  font-size: 12px;
  line-height: 1.35;
  text-align: center;
  color: var(--muted);
}
.no-poster img { display: none; }
.no-poster .fallback { display: flex; }

/* Seen state: dim the poster so unwatched films are what catches the eye, and
   tick it so seen is legible on its own. Dimming is only relative — a muted
   poster reads as dimmed, and a grey text fallback barely changes — so the
   tick, which no artwork can wash out, is what actually marks it. */
.card.seen .poster img { opacity: 0.38; filter: grayscale(0.55); }
.card.seen .fallback { opacity: 0.4; }

.check {
  position: absolute;
  top: 6px;
  right: 6px;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  background: var(--accent);
  border-radius: 50%;
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 0.14s ease, transform 0.14s ease;
}
.card.seen .check { opacity: 1; transform: scale(1); }
.check svg { width: 15px; height: 15px; fill: none; stroke: #fff; stroke-width: 2.6; }

/* Collection membership, in the caption's bottom line beside the rating.
   Inherits the caption's colour: `--accent` is what says seen everywhere else,
   so a green icon here read as a second tick. The icon is an inline box and not
   a flex item, so it sits on the text's own baseline — centred in a flex row it
   rode high, since the digits' visual middle is below the line box's. */
.in-collections svg {
  width: 1.05em;
  height: 1.05em;
  margin-right: 0.15em;
  vertical-align: -0.17em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Bookmark toggle. Opposite corner from `.check`, and a control rather than
   a badge: it answers to its own click, so unlike `.check` — which only ever
   appears once already true — it has to be discoverable before it is ever
   clicked. Hidden at rest rather than merely dim: a wall of posters showing
   a corner icon on every single one read as noise, not an invitation, so it
   surfaces only on hover (or focus, for a keyboard reader who cannot hover
   at all) and otherwise stays out of the way. Once lit it stops being a
   thing to discover and becomes a fact about the title, so `.bookmarked`
   below keeps it on screen unconditionally. */
.bookmark {
  position: absolute;
  top: 6px;
  left: 6px;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  background: rgb(0 0 0 / 0.5);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.14s ease, background-color 0.14s ease, transform 0.14s ease;
}
@media (hover: hover) and (pointer: fine) {
  .card:hover .bookmark { opacity: 1; }
}
.bookmark:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 2px; }
.bookmark svg { width: 14px; height: 14px; fill: none; stroke: #fff; stroke-width: 1.8; }
.card.bookmarked .bookmark {
  background: var(--accent);
  opacity: 1;
}
.card.bookmarked .bookmark svg { fill: #fff; stroke: #fff; }

/* A collection's select mode (`collection.html`'s Selection row, `app.js`).
   While selecting, the card is one target that only ever selects, so the
   bookmark toggle goes, and the seen tick goes with it: the corner it holds
   is where a picked card's badge sits, and two ticks meaning two different
   things on one card is the clash worth avoiding. The dimming stays to say
   seen for the length of the mode. A picked card wears `--negate` — the
   colour an excluded facet tag already wears, and what picking it here is
   for — as a ring and a minus badge: nothing green and nothing ticked, so it
   can never be read as seen. Drawn on the poster itself rather than as
   markup, so no card on any other page carries an element only this one mode
   ever shows. */
.selecting .grid .bookmark,
.selecting .grid .check { display: none; }
.selecting .grid .card.selected .poster {
  outline: 3px solid var(--negate);
  outline-offset: 2px;
}
.selecting .grid .card.selected .poster::after {
  content: "";
  position: absolute;
  top: 6px;
  right: 6px;
  width: 24px;
  height: 24px;
  background: var(--negate) center / 15px no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 12h12' fill='none' stroke='white' stroke-width='2.6' stroke-linecap='round'/%3E%3C/svg%3E");
  border-radius: 50%;
}

figcaption {
  margin-top: 7px;
  font-size: 12.5px;
  line-height: 1.35;
}
/* One line, clipped with an ellipsis, so the year and rating lines sit at the
   same height on every card in a row however long the title runs. */
figcaption a {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* `min-height` holds the second line's space open on titles that have no rating
   to put there. Nothing is printed — an unrated film is one nobody has voted on,
   and a dash would dress that absence up as a recorded fact — but the row would
   otherwise sit ragged, since the obscure tail this page exists to surface is
   exactly where ratings are missing. */
figcaption .meta {
  display: block;
  min-height: 1lh;
  color: var(--muted);
  font-size: 11.5px;
}

/* ★ once a rating has the votes to settle its decimal, ☆ while it has not (see
   RATING_SETTLED_MIN_VOTES). The pair must differ in fill and in nothing else,
   which takes two guards: U+2605 is in the emoji set and U+2606 is not, so a
   platform left to itself may render the filled star as a colour glyph and the
   hollow one as text — differing in hue and width instead of fill, which is the
   signal gone. And U+2606 is missing from more faces than U+2605, so the two can
   fall back differently and shift the line under them; a fixed advance keeps the
   caption rows level, which matters most in the thin-record tail this page
   exists to surface, where the hollow star is the common case. */
.star {
  font-variant-emoji: text;
  display: inline-block;
  min-width: 1em;
  text-align: center;
}

/* Nothing hides client-side any more. 'unseen only' and 'on Plex only' were a
   class on <body> and the facets marked their cards `filtered-out`; both are
   query state now, so a grid holds exactly the cards that cleared the filters
   and there is nothing on the page left to conceal. */

/* ----------------------------------------------------------------- detail */

.detail { display: flex; gap: 28px; align-items: flex-start; flex-wrap: wrap; }
.detail-poster { width: 190px; flex: none; }
.detail-body { flex: 1; min-width: 260px; }
.detail-body h1 { margin: 0; font-size: 24px; letter-spacing: -0.02em; }
/* The original title belongs to the title, not to the run of facts under it:
   tight to the h1, italic and a size up from `.sub` so it reads as a name the
   film also goes by rather than the first item of metadata. The gap goes below
   it instead, where the group actually breaks. */
.detail-body .orig { margin: 1px 0 0; font-size: 15px; font-style: italic; color: var(--muted); }
.detail-body .sub { margin-top: 9px; }
/* Muted and `.sub`-sized rather than full body text: at full contrast this
   read as a shouted interruption between two runs of quiet meta lines
   (`.sub`, `.ext`) above and the credits below, not a synopsis in its place
   — the same problem `.orig` solves for the title by staying muted rather
   than competing with the `h1` next to it. */
.detail-body .plot { margin: 14px 0 0; color: var(--muted); font-size: 14px; line-height: 1.5; }
/* A named attribute of the title and its values. The credits are three of
   these; genre is the fourth and last, and it sits here rather than in a strip
   of badges because a bordered pill is a control that acts on the page under it
   (see `.filter-chip`) and these leave the page, exactly as the credit links in
   the rows above do. Plain muted text was the other option and it would have
   read as a second `.sub` line — muted under muted, two runs of facts with
   nothing to tell them apart. This column already had a shape for a named
   attribute, so genre takes it. */
.detail-row { margin-top: 18px; }
.detail-row h3 {
  margin: 0 0 3px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.detail-row p { margin: 0; }
.as { color: var(--muted); font-size: 13px; }

/* ---------------------------------------------------------------- results */

/* Search's two name-plus-meta lists — people and collections — are one
   shape, so one ruleset. `.collections` here is a free name again now that
   the stats table below has its own (`.stats-table`); before that rename
   this list rendered off the table's rules by accident, the same class
   colliding with two unrelated meanings. */
/* A grid and not `columns`: columns fill down the first before the second,
   so a ranked list read down rather than across, and every "More people"
   reflowed the whole list between the columns. Rows fill across, so the best
   match leads top-left and more only ever adds rows underneath. At most three
   across, at least 220px each — what `columns: 3 220px` gave. */
.people, .collections {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(max(220px, calc((100% - 48px) / 3)), 1fr));
  column-gap: 24px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.people li, .collections li { padding: 3px 0; }
.people .meta, .collections .meta { color: var(--muted); font-size: 12.5px; }
/* A person's best-known title on a line of its own, as in the search box's
   suggestions (`.suggest-sub`): inline, it wrapped wherever a name and title
   happened to run out of column. */
.people .known-for { display: block; color: var(--muted); font-size: 12.5px; }

/* The collections list: a name and three measurements — how far a table's
   single plain header row can go before it needs no grouping to stay
   legible. Named `.stats-table` and not `.collections`, which
   `search.html`'s unrelated `<ul>` of collection search results already
   uses. `.table-scroll` and not a wrapping table: a table that reflowed its
   own cells to fit a narrow window would read worse than one that just
   scrolls sideways under a fixed header row, even at four columns.

   This used to be an eleven-column table shared with three now-removed
   genre/country/language index pages, with a booktabs-style two-row grouped
   header (Titles/Seen/Rating/Votes/Attributes) to keep that many columns
   legible — see git history for that version if this one ever grows back
   toward it. */
.table-scroll { overflow-x: auto; }
.stats-table {
  /* Left to its content, a table this narrow reads as a stray scrap of text
     stranded at the top of an otherwise empty desktop-width page — spanning
     the container is what makes the columns read as a considered layout
     instead. Below the width they need, `.table-scroll` takes over and this
     stops mattering: the table keeps its content width and scrolls. */
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
  white-space: nowrap;
  /* A rule marking where the table *ends*, before the pager — nothing else
     does. Still `--line` rather than a harder black — every rule on this
     site is that same muted gray. */
  border-bottom: 2px solid var(--line);
}
.stats-table th, .stats-table td {
  padding: 7px 14px 7px 0;
  text-align: right;
  font-variant-numeric: tabular-nums;
  vertical-align: middle;
}
/* The name column is the one word among the numbers, so it reads like one:
   left-aligned, and plain text rather than tabular figures. Numbers stay
   right-aligned so the digits themselves line up down the column — the
   point of `tabular-nums` — which a name column of ragged lengths has no
   such column of digits to keep aligned in the first place. */
.stats-table th:first-child, .stats-table td:first-child {
  text-align: left;
  font-variant-numeric: normal;
}
/* Every figure muted, the name alone at full contrast — `.people .meta`'s
   rule, and the one this table itself drew before it was a table: the name
   is the fact, everything else is commentary on it. */
.stats-table td:not(:first-child) { color: var(--muted); }
.stats-table thead th { border-bottom: 2px solid var(--line); }
.stats-table tbody tr + tr { border-top: 1px solid var(--line); }
/* A header's sort control needs no enclosure of its own here — `.sort-chip`
   already reads as plain muted text with an arrow outside `.segmented`'s
   bar. */
.stats-table th .sort-chip { padding: 1px 4px; }

/* "Manage collection" on a collection's own page — `.filters`' shape (a
   `<details>` summary that is a whole control, a bordered panel under it
   once open, the same triangle marking which state it's in, the same
   margin model: `margin-top` here, no `margin-bottom` anywhere, open or
   shut, because the *next* element's own `margin-top` is what draws the
   gap below — see `.controls`' comment for why doubling up on both sides
   of that boundary was never right) without `.filters`' own
   `filter-count`/`clear-filters`, which have nothing to say about a form. */
.entity-panel { margin-top: 14px; }
.entity-panel > summary {
  display: block;
  width: fit-content;
  padding: 3px 0;
  font-size: 13px;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  list-style: none;
}
.entity-panel > summary::-webkit-details-marker { display: none; }
/* The same triangle `.filters > summary::before` draws — one glyph for
   "this disclosure is open or shut" app-wide, not a second one invented
   for this panel. */
.entity-panel > summary::before {
  content: "▶";
  display: inline-block;
  position: relative;
  top: -1px;
  font-size: 8px;
  margin-right: 6px;
  vertical-align: middle;
  color: var(--muted);
  transition: transform 0.12s ease;
}
.entity-panel[open] > summary::before { transform: rotate(90deg); }
.entity-panel[open] {
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
.entity-panel-body { margin-top: 12px; }

/* Refresh/Add: a label and its row share the same two column edges across
   every row — `.facets`' own grid shape (`max-content 1fr`, items left
   rather than stretched to fill their track) reused directly rather than
   re-derived, so this panel reads as the same kind of thing as Advanced
   filters above it, not a different visual language. The row itself
   (`.source-row`) is what `.facet-body` is for a bound field: one inline
   group holding the control and its own action together, so "Add"/"Refresh"
   sits beside the field it acts on rather than off in a column of its own.
   Each row is still its own `<form>` so it posts independently — `display:
   contents` lets a form's own children join the grid directly instead of
   the form drawing a box the grid would have to fit around.

   `.facets`' own `baseline`, reused for the same reason: a differently
   sized label ("Titles" at 11px) and row content (`.sub` at 14px, its
   buttons at 12px) only line up if the grid aligns their actual text
   baselines rather than their box tops, which drift apart by a font's own
   half-leading the moment sizes differ. Titles' own `<textarea>` is the
   one exception — it spans several lines, and a textarea's baseline is its
   bottom edge, not its first line, so baseline-aligning that row would
   pull its label down to the textarea's *bottom*. `.facet-searchable`
   makes the identical exception against `.facets`' own baseline, for the
   identical reason. */
.source-forms {
  display: grid;
  grid-template-columns: max-content 1fr;
  align-items: baseline;
  justify-items: start;
  row-gap: 10px;
  column-gap: 8px;
}
/* `.source-line` is the same for a row that can't be a `<form>` of its own —
   the bookmarks page's Collection picker, whose buttons submit a form
   outside it (see `bookmarks.html`). */
.source-forms form, .source-forms .source-line { display: contents; }
.source-forms form:has(textarea) > * { align-self: start; }
.source-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  padding-top: 2px;
  text-decoration: none;
  /* An attached link's row uses this for its badge ("IMDb", "IMDb₂") now,
     not its fetched name — this app's own short, fixed vocabulary the
     same as "List"/"Titles" are, so a `max-content` column sizing
     to whichever is longest (`.facets` does the same for "Genre" next to
     "Year") no longer risks the one thing it used to guard against: a
     link's own unbounded, someone-else's-list-name label blowing the
     shared column width out. */
}
/* An attached list's fetched name, leading its row: dressed as every other
   `↗` link is (`.ext a`), since it leaves for another site just as they do.
   `.source-row`'s baseline alignment keeps it on one line with the larger
   `.sub` status after it. Someone else's list name has no length limit, so
   it breaks anywhere rather than widening the row. */
.source-name { font-size: 13px; color: var(--muted); overflow-wrap: anywhere; }
@media (hover: hover) and (pointer: fine) {
  .source-name:hover { color: var(--fg); }
}
/* `.facet-label + *`'s own rule, reapplied: a grid item stretches to fill
   its track's content size by default, which would otherwise let a long
   placeholder or a wide `.sub` line blow the second column out past where
   the grid meant it to sit. */
.source-label + * { min-width: 0; }
.source-row {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
}
/* Titles' own row: keep its button pinned to the textarea's top rather
   than its baseline (its bottom edge), the same exception the grid above
   makes for this one row. */
.source-row:has(textarea) { align-items: flex-start; }
.source-forms input[type="url"],
.source-forms textarea {
  width: 320px;
  max-width: 100%;
  padding: 1px 6px;
  font: inherit;
  font-size: 12px;
  color: var(--fg);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  resize: vertical;
}
/* Not its own padding any more — the 4px top/bottom it used to get pushed
   its placeholder down further than List's own placeholder sits, two
   fields in the same grid reading at a different offset for no reason
   either name gives. `min-height` is what Titles actually needs of its
   own: `rows="3"` only draws its *default* height, and the `resize:
   vertical` it shares with List above lets a drag take it below that,
   past even List's own single line — a floor at exactly the three lines
   `rows` already promises, in the same unit (`em`, off this rule's own
   12px) a resize can only ever add to, never take under. */
.source-forms textarea { min-height: calc(3 * 1.5em + 4px); }
.source-forms input:focus,
.source-forms textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}
.source-forms .sub { margin: 0; padding-top: 1px; }
/* The Add/Refresh action itself: `.apply-bound`'s own shape, reused rather
   than a bordered button — this panel's whole point is to read as the same
   kind of thing Advanced filters is, and that panel has almost no real
   buttons in it, just links. */
.source-apply {
  padding: 0;
  font: inherit;
  font-size: 12px;
  color: var(--muted);
  background: none;
  border: 0;
  text-decoration: underline;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .source-apply:hover { color: var(--fg); }
}
.source-apply:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Link/Titles only (`.source-input-row`): nothing to add until something's
   typed, so — `.apply-bound`'s own rule — the link stays hidden until then.
   Refresh has no field to gate on (there's nothing to type before it means
   something), so its own `.source-apply` is simply always there, `.clear-filters`' rule rather than `.apply-bound`'s. */
.source-input-row .source-apply { display: none; }
.source-input-row.dirty .source-apply { display: inline; }

/* Delete: no row, no label, no description — the button's own text already
   says what it does, so nothing here explains it further, the same reason
   `.new-collection-btn` on `/collections` needs no label beside it either.
   Still inside "Manage collection", just not inside `.source-forms`'s
   grid — a small gap above is all that sets it apart from Add/Refresh, not
   a rule or a box. Left-aligned like everything else on the page, not
   pushed to the right: that's the one slot this app already reserves for
   a primary/expected-next action (`.control-sort`'s own `margin-left:
   auto`), which is exactly what Delete is not — the older convention for a
   lone destructive control is to keep it out of that slot, not put it
   there in a different color. */
.manage-delete { margin-top: 14px; }
/* The bookmarks page's two whole-list buttons, New collection and Clear
   bookmarks, on one line in the slot `.manage-delete` holds alone on a
   collection's page. */
.manage-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 14px;
}
.delete-trigger {
  padding: 6px 14px;
  font: inherit;
  font-size: 13px;
  color: #c0392b;
  background: none;
  border: 1px solid #c0392b;
  border-radius: var(--radius);
  cursor: pointer;
}
/* `#delete-confirm`'s own floating-card look, the same reset `#mobile-panel`
   above needs from the `[popover]` UA stylesheet — except here `inset: 0` and
   `margin: auto` are kept rather than overridden, so the card centres itself
   in the viewport instead of anchoring to its trigger: a destructive
   confirmation reads as an interruption of the page, not a menu hanging off
   the button that opened it. `display` stays withheld until `:popover-open`
   for the same reason as `#mobile-panel`'s own copy of this rule: given
   unconditionally, it would outrank the UA stylesheet's closed-state
   `display: none` and leave the card rendered open from first paint. */
.confirm-popover {
  position: fixed;
  inset: 0;
  margin: auto;
  flex-direction: column;
  gap: 12px;
  width: max-content;
  height: max-content;
  max-width: min(300px, calc(100vw - 32px));
  padding: 14px 16px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.confirm-popover:popover-open { display: flex; }
/* Every `[popover]` gets this pseudo-element for free while open — including
   `#mobile-panel` above, which leaves it untouched (transparent) because a
   menu shouldn't dim the page behind it. A destructive confirmation is the
   one control on this page worth that extra weight, so only this popover
   gives it a fill. */
.confirm-popover::backdrop { background: rgba(0, 0, 0, 0.35); }
.confirm-popover p { margin: 0; font-size: 13px; }
.confirm-actions { display: flex; justify-content: flex-end; gap: 10px; }
.confirm-actions button {
  padding: 6px 12px;
  font: inherit;
  font-size: 13px;
  border-radius: var(--radius);
  cursor: pointer;
}
.confirm-cancel { color: var(--muted); background: none; border: 1px solid var(--line); }
.confirm-delete { color: var(--bg); background: #c0392b; border: 1px solid #c0392b; }

.form-error { margin: 4px 0 0; color: #c0392b; font-size: 13px; }

/* List/Titles' own per-field messages — `grid-column: 2` rather than a
   third `.source-forms` column: each `<p>` is its own row's only cell
   (`.source-forms form { display: contents }` puts it straight into the
   parent grid), landing directly under the field rather than under its
   label. Below the mobile breakpoint `.source-forms form` switches to a
   plain flex column instead, where `grid-column` does nothing and the
   `<p>` simply falls in place after the field it already follows in the
   markup. */
.field-error, .field-notice { grid-column: 2; margin: 0; font-size: 12px; }
.field-error { color: #c0392b; }
.field-notice { color: var(--muted); }
.source-forms input[aria-invalid="true"],
.source-forms textarea[aria-invalid="true"] { border-color: #c0392b; }

.empty { color: var(--muted); }

/* Below this width, four groups (Seen, Bookmarked, Plex, sort) no longer fit
   one row, but `.controls`' plain `flex-wrap` doesn't know that in advance —
   it packs as many as the viewport allows and breaks wherever the next one
   stops fitting, which lands in a different place on every device (two per
   line on a phone, three on a tablet) rather than at a rule anyone chose.
   One group per line fixes that, but a group is still a flex row of
   `.control-label` beside `.segmented` with nothing to line the labels up —
   SEEN, BOOKMARKED and PLEX aren't the same width, so each bar started
   wherever its own label happened to end. `.facets` solved exactly this with
   a grid (`max-content 1fr`), so this reaches for the same fix rather than
   inventing a second one: `.controls` becomes a grid and each
   `.control-group` goes `display: contents`, the way `.facet` already does,
   so its label and bar fall straight into the grid's two columns.

   `--label-col` is why `.controls` and `.facets` share one number instead
   of each measuring `max-content` off its own labels alone: two `max-content`
   grids size independently, so Seen's bar and Collection's bar would start a
   couple pixels apart depending on which section's labels ran longer.
   `subgrid` would share the measurement itself rather than a guessed number
   for it, and was tried twice — plainly and gated behind `.filters[open]` —
   but broke the page on iPad both times, for reasons that weren't visible
   without a browser on that device to inspect. A fixed width is the
   fallback: reintroduces exactly the fragility `.facets`' own grid comment
   describes (re-tune this if a label longer than BOOKMARKED/COLLECTION
   shows up later), but it doesn't touch `<details>` at all, so it doesn't
   share whatever broke last time.

   Below 640px the phone rule takes over instead — see there for why that
   one stacks label above bar rather than reaching for this same column. */
@media (min-width: 641px) and (max-width: 900px) {
  :root { --label-col: 88px; }
  .controls {
    display: grid;
    grid-template-columns: var(--label-col) 1fr;
    align-items: baseline;
    justify-items: start;
    row-gap: 10px;
    column-gap: 8px;
  }
  .control-group { display: contents; }
  .facets { grid-template-columns: var(--label-col) 1fr; }
}

@media (max-width: 640px) {
  main { padding: 16px; }
  /* Browse, collections and sign-out move into #mobile-panel below this
     width, so the search box — still the only elastic thing in the row — is
     left splitting the phone with just the brand and the menu button, not
     with three more links first. That is what keeps `the seventh seal` from
     coming back truncated on the results page. */
  .nav-wide { display: none; }
  .nav-menu-trigger { display: inline-flex; }
  .topbar-inner { gap: 12px; padding: 12px 16px; position: relative; }
  /* The box has only what the brand and menu button leave it, so the
     suggestions span the topbar's own gutters instead of the box. */
  .search { position: static; }
  .suggest-list { top: calc(100% - 8px); left: 16px; right: 16px; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(var(--poster-size, 104px), 1fr)); gap: 14px 10px; }
  /* The label column is what gives out at this width — a gutter beside chips
     that need every pixel. It goes for every facet, inside the disclosure and
     above it, because they are one list: the grid collapses to a single
     column and each row goes back to being its own block, label above
     content, rather than a `display: contents` pair of grid cells. */
  .facets { display: flex; flex-direction: column; gap: 10px; }
  .facet { display: flex; flex-direction: column; gap: 6px; }
  .facet-label { padding-top: 0; }
  /* Manage collection's own grid, stacked the same way and for the same
     reason: a `max-content` label column beside List/Titles' input fields
     leaves those fields squeezed into whatever the column happens to give
     up, which on an iPhone SE is narrower than their fixed 320px design
     width — `max-width: 100%` on them can't save it either, since it
     resolves against `.source-row`'s own shrink-to-fit width, not the
     viewport, and a row sized around a 320px child never shrinks below it.
     Stacking removes the column, and stretches `.source-row` to the full,
     definite width `100%` on the fields can then actually cap against —
     `align-items` has to come off the grid's own `baseline` for that
     stretch to happen at all, the same reason `.controls` restates it
     next to `flex-direction` rather than leaving the row rule's value to
     carry over unchanged. */
  .source-forms { display: flex; flex-direction: column; align-items: stretch; gap: 10px; }
  .source-forms form,
  .source-forms .source-line { display: flex; flex-direction: column; gap: 6px; }
  .source-label { padding-top: 0; }
  .source-forms input[type="url"],
  .source-forms textarea { width: 100%; }
  /* Stacked, there's no shared baseline left to pull Titles' own label
     down to its textarea's bottom — the exception that guarded against it
     above only means something in the grid, and left in place here it
     would stop `.source-row` stretching to the full width its own `100%`
     field just above needs. */
  .source-forms form:has(textarea) > * { align-self: stretch; }
  /* The rename input, the same trap in miniature: no `width` of its own,
     only a 160px floor meant for a short name, so on a wide screen it
     just sits at whatever its default browser size happens to be — never
     measured against `.entity-title`'s actual width because a flex item's
     automatic minimum size defaults to its content size, not zero, which
     is what lets it overflow past Save rather than shrink for it. `flex`
     lets it take whatever `.entity-title` has left beside Save instead of
     its own default; `min-width: 0` is what actually permits that,
     overriding the automatic minimum the flex spec otherwise gives a
     text input. Scoped to this width rather than restated everywhere,
     the 160px floor stays what decides the input's size above it. Safari
     needs the same override one level up, on `.rename-form` itself — its
     own automatic minimum, as a nested flex item of `.entity-title`,
     isn't properly inherited down from the input's `min-width: 0` there,
     so left unset it holds the form at its unshrunk content width
     regardless of what the input alone is willing to give up. */
  .rename-form { min-width: 0; }
  .rename-input { flex: 1 1 auto; min-width: 0; }
  /* The controls row stacks with them. Not for the gutter — it never had one,
     it has an 8px gap — but because the labels now wear the facets' clothes,
     and a word that looks like TYPE and GENRE while sitting somewhere they
     don't reads as a distinction the control isn't making. It buys the width
     back too: five segments and a label are more than one line an iPhone SE
     can hold. `align-items` has to come off centre or the label floats over
     the middle of the bar; the nowrap stays, so the bars themselves still
     never break. */
  /* The tablet breakpoint above lines labels up in a `max-content` column
     because there's room left beside it for sort's segmented bar; down here
     there isn't — sort needs the row's full width to hold its segments
     without wrapping, which is what column above already knew. `flex-direction:
     column` puts each `.control-group` on its own row the plain way, by
     stretching it to the container's full width, rather than reaching for
     the grid a wider row can afford. */
  .controls { flex-direction: column; align-items: stretch; }
  .control-group { flex-direction: column; align-items: flex-start; gap: 6px; }
  /* A stacked column has no far edge for sort to claim — the auto margin
     that sets it apart from the filters on a wide line would instead just
     shove it off `align-items: flex-start` here. The bookmarks page's two
     sort bars stack in that column, each in its own enclosure. */
  .control-sort { margin-left: 0; }
  .sort-bars { flex-direction: column; align-items: flex-start; gap: 6px; }
}

/* A filtered grid that came back with nothing. Every constraint is lit above,
   so this states what happened and does not offer advice about it. */
.empty { margin: 24px 0; color: var(--muted); font-size: 13px; }

/* The pager. Centred under the grid rather than ranged left with everything
   else on the page, because it is not part of the column of controls above —
   it is the foot of a wall of posters, and the wall is symmetrical.
   `justify-content` and not a fixed order of widths: the two ends drop to plain
   text when you reach them, and the row must not shuffle as they do. */
.pager {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 6px 18px;
  margin: 28px 0 8px;
  font-size: 13px;
  color: var(--muted);
}
.pager a, .pager > span:not(.pager-at) {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  line-height: 1;
}
/* Each viewBox is cropped to its chevron's ink, so the gap to the label is the
   same for every icon; the nudge is the text's optical centre sitting a touch
   below the middle of its line box. */
.pager svg { display: block; flex: none; height: 0.6em; transform: translateY(0.05em); }
@media (hover: hover) and (pointer: fine) {
  .pager a:hover { color: var(--fg); }
}
.pager a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* An end you are already at. Dimmed rather than removed, so the row keeps its
   shape and the step you are still able to take does not slide sideways under
   the cursor on the way to the last page. */
.pager > span[aria-hidden="true"] { opacity: 0.35; }

/* Search's "More" tile (`search.html`): one control in all three sections,
   drawn as the cells beside it are drawn. Among posters it is a poster's slot,
   dashed, like the other things on this app that are not a result
   themselves, with the count leading as the thing weighed before clicking;
   among the people and collections it is a line of text like the names
   around it (`.people .more-tile` below), since a box there stood out from a
   list of plain names as nothing else on it does. */
.more-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  box-sizing: border-box;
  height: 100%;
  min-height: 44px;
  padding: 6px;
  font-size: 13px;
  color: var(--muted);
  text-decoration: none;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
}
.more-count { font-size: 17px; color: var(--fg); font-variant-numeric: tabular-nums; }
@media (hover: hover) and (pointer: fine) {
  .more-tile:hover { text-decoration: none; border-color: var(--muted); color: var(--fg); }
}
.more-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A poster's slot is its 2:3 box, not the card's full height — the caption
   under a poster is not part of the grid's shape, so the tile aligns with the
   posters beside it and leaves the caption row empty. */
.grid .more-tile { height: auto; aspect-ratio: 2 / 3; }
.people .more-tile, .collections .more-tile {
  display: inline;
  padding: 0;
  font-size: inherit;
  color: inherit;
  border: 0;
}
.people .more-count, .collections .more-count { font-size: inherit; }
@media (hover: hover) and (pointer: fine) {
  .people .more-tile:hover, .collections .more-tile:hover { text-decoration: underline; }
}
.grid > [hidden], .people > [hidden], .collections > [hidden] { display: none; }
/* Where you are, which is a statement and not a control — the one thing on
   the row that is not a link, and so the one without a hover. */
.pager-at { font-variant-numeric: tabular-nums; }

.nav-link { font-size: 13px; color: var(--muted); text-decoration: none; }
@media (hover: hover) and (pointer: fine) {
  .nav-link:hover { color: var(--fg); }
}
.signout { margin-left: auto; }
