/* ===========================================================================
   THEORIA — the catalog sheet
   ---------------------------------------------------------------------------
   One design system for the whole site. Plain white paper, one lime green.

   The idea: Theoria is a film *archive* with a measuring instrument pointed
   at it — not a cinema. So the vocabulary is contact sheets, catalog cards,
   accession numbers and shelf labels, set on white with hairline rules.

   LIME IS THE MEASUREMENT MARK. It appears only where something has been
   measured: a bar, a keyed frame, a rating, the page you're on. It is never
   decoration, never a gradient, never a glow. Spending it anywhere else is
   what makes it stop meaning anything.

   No framework, no build step: custom properties + grid/flex.
   Light by default, with a dark counterpart under prefers-color-scheme. The
   dark mode is ONLY a token swap — because every component below reads its
   colours from tokens, no component rule is duplicated per mode.

   Contract with page stylesheets (home.css, analytics.css):
   this file owns every token and every shared component. A page stylesheet
   may ADD a component of its own; it may never restyle one defined here.
   That rule is what keeps section margins from cancelling each other out.
   =========================================================================== */

:root {
  /* --- Color ---------------------------------------------------------------
     Contrast ratios verified against --paper #FFFFFF:
       --ink 19.7:1 · --ink-muted 7.6:1 · --ink-faint 4.8:1 · --lime-text 5.0:1
       --lime-mark 3.1:1 (clears the 3:1 non-text mark floor)
       --ink on --lime 13.1:1
     --lime itself is 1.5:1 on paper: a FILL, never a mark on its own and
     never text. Where it fills a meter bar the number is printed in the same
     cell — that printed value is the required relief channel. */
  --paper: #ffffff;
  --sheet: #f7f7f5;
  --sheet-2: #efeeea;
  --rule: #e3e2dd;
  --rule-strong: #c9c7c0;

  --ink: #0b0b0b;
  --ink-muted: #57534e;
  --ink-faint: #78716c;

  --lime: #a3e635;
  --lime-wash: #f2fbe0;
  --lime-text: #4d7c0f;
  --lime-mark: #65a30d;
  --lime-deep: #365314;
  --lime-hover: #93d128;
  /* Lime's fill is equally bright in both themes, so text printed on it must
     stay fixed rather than swap with --ink — in dark mode --ink turns near-
     white, which is illegible on lime. Never redefined under [data-theme]. */
  --ink-on-lime: #0b0b0b;
  /* Form errors only -- the one place a reader must act before going on.
     6.2:1 on --sheet. */
  --danger: #b91c1c;
  /* Collection-action icon fills: one colour per collection so a selected
     Like, Watch later and Top read apart at a glance, not just "on". True
     hues in both themes, no outline -- a deliberate call: lime and yellow
     are ~1.3:1 on the light pill (10:1 on the dark one), under the 3:1 mark
     floor, and the deeper steps that cleared it read as olive and bronze.
     The swapped label ("In your Top") carries the state in words, so the
     colour is reinforcement, not the only signal. */
  --mark-like: #ef4444;
  --mark-watch-later: #a3e635;
  --mark-top: #facc15;
  /* Chart area fill, read by analytics.js. */
  --chart-wash: rgba(163, 230, 53, 0.18);

  /* Studio logo assets are TMDB PNGs drawn for a light backing — many are
     dark wordmarks on transparent ground. Unlike every other surface color,
     this plate must NOT swap with the theme, or a dark logo goes illegible
     the moment the site switches to dark mode. Fixed, like --ink-on-lime. */
  --logo-plate: #f4f3ee;
  --logo-ink: #57534e;

  /* Sequential ramp for ranked data. PASSES all four ordinal checks
     (monotone L, adjacent ΔL ≥ .06, light-end ≥ 2:1, single hue). */
  --rank-1: #7cb518;
  --rank-2: #558e0b;
  --rank-3: #365314;

  /* --- Type ----------------------------------------------------------------
     Archivo carries two roles through its width axis: expanded for display,
     condensed for labels. That single family doing two opposite jobs is the
     typographic signature. */
  --font-display: "Archivo", "Helvetica Neue", Arial, sans-serif;
  --font-label: "Archivo", "Helvetica Neue", Arial, sans-serif;
  --font-body: "Instrument Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "Spline Sans Mono", ui-monospace, "SF Mono", Menlo, monospace;
  /* The personnel face: Work Sans, used for what a person did on a film — a
     cast member's character, and every crew job from directing to camera.
     A humanist grotesque, warmer and rounder than the Instrument Sans body
     and off the mono axis entirely, so a credit reads as a name rather than
     a field value. */
  --font-role: "Work Sans", "Helvetica Neue", Arial, sans-serif;

  --text-3xs: 0.6875rem;
  --text-2xs: 0.75rem;
  --text-xs: 0.8125rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-md: 1.125rem;
  --text-lg: 1.375rem;
  --text-xl: 1.75rem;
  --text-2xl: 2.5rem;
  --text-display: clamp(3rem, 7vw, 5.5rem);

  /* --- Space --------------------------------------------------------------- */
  --space-1: 0.25rem;
  --space-2: 0.375rem;
  --space-3: 0.5rem;
  --space-4: 0.75rem;
  --space-5: 1rem;
  --space-6: 1.5rem;
  --space-7: 2rem;
  --space-8: 2.75rem;
  --space-9: 4rem;
  --space-10: 6rem;

  /* --- Form ---------------------------------------------------------------- */
  --radius: 2px;          /* sheets and plates, not pills */
  --radius-pill: 999px;   /* chips, buttons, and pagination controls */
  --hairline: 1px solid var(--rule);
  --frame: 260px;         /* max content width of the page shell gutter calc */
  --measure: 1240px;
  /* The one shadow in the system, for the few surfaces that sit visibly
     above the page rather than flush on it (the auth card, Task 95 UI
     pass, and the account popover). Everywhere else a --hairline border already does the job of
     separating a sheet from the page, so this stays reserved rather than
     becoming a general-purpose "card" treatment. */
  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.06), 0 12px 32px -8px rgba(0, 0, 0, 0.12);

  --z-header: 100;
  --z-assistant: 150;
  --z-skip: 200;

  --ease: cubic-bezier(0.2, 0, 0.1, 1);
  --dur-fast: 120ms;
  --dur: 220ms;
  --dur-slow: 420ms;

  /* Overshoots past its target before settling -- distinct from --ease
     (which never leaves 0..1) because the icon-fill bloom below wants that
     overshoot to read as the glyph swelling under a thumb, not just easing
     into place. */
  --ease-bloom: cubic-bezier(0.16, 1, 0.3, 1);

  /* Search magnifier (Material Symbols "search"), carried as a mask source so
     it can be tinted per theme and per state. Used by .field:has(search). */
  --icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960' fill='%23000'%3E%3Cpath d='M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z'/%3E%3C/svg%3E");

  /* The <select> chevron. Every engine draws its own menulist arrow in its own
     place — Safari insets it in a rounded plate, Firefox and Chrome each pick a
     different glyph — so the control is normalised with appearance:none and this
     is drawn back on. Unlike --icon-search it is a background-image, not a mask:
     a mask needs an ::after, and the only selector that could carry one here is
     .field:has(select), which would leave Firefox <121 and Safari <15.4 with a
     select that has no arrow at all. A background-image can't be tinted, so the
     colour is baked in and the token is re-declared per theme below — the same
     shape every other colour token in this file already has. */
  --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960' fill='%2378716c'%3E%3Cpath d='M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z'/%3E%3C/svg%3E");

  color-scheme: light;
}

/* --- Dark counterpart -------------------------------------------------------
   Every token is re-picked against the dark surface rather than flipped — an
   inverted light palette gets the lightness relationships wrong. Ratios
   verified against --paper #09090b (the cinema redesign's darkened theatre,
   a hair cooler and deeper than the old #12120f):
     --ink 17.6:1 · --ink-muted 8.3:1 · --ink-faint 5.6:1 · --lime-text 15.2:1
     --lime-mark 13.2:1 · --ink on --lime 13.1:1 (buttons keep ink labels)

   Dark is now the site's default (base.html's head script); light remains
   one click away on the header toggle.

   The lime *swaps role* between modes. On white it was too light to serve as
   a mark, so marks stepped DOWN the ramp to a darker green. On ink it is the
   brightest thing available, so the mark and link steps move UP instead.
   Only the tokens change — no component below this block is redefined.

   Applied two ways, and the values live in ONE place (--dark-*) so the two
   can never drift apart:
     [data-theme="dark"]  — the user picked dark from the header toggle
     prefers-color-scheme — no explicit pick yet, so follow the OS
   The :not([data-theme]) guard is what lets an explicit "light" choice win
   over a dark OS setting. */
:root {
  --dark-paper: #09090b;
  --dark-sheet: #121215;
  --dark-sheet-2: #1b1b1f;
  --dark-rule: #232327;
  --dark-rule-strong: #36363c;
  --dark-ink: #f2f1ed;
  --dark-ink-muted: #a9a7a1;
  --dark-ink-faint: #8a8882;
  /* --lime-wash sits deliberately near the surface: on white it is a pale
     tint of the page, so on ink it must be a barely-lifted tint of the ink.
     Matching the light value's saturation produces olive slabs instead of a
     whisper behind the number. */
  --dark-lime-wash: #18200d;
  --dark-lime-text: #bef264;
  --dark-lime-mark: #a3e635;
  --dark-lime-deep: #d9f99d;
  --dark-lime-hover: #bef264;
  --dark-danger: #f87171;
  --dark-chart-wash: rgba(163, 230, 53, 0.10);
  --dark-rank-1: #bef264;
  --dark-rank-2: #a3e635;
  --dark-rank-3: #7cb518;
  /* A black shadow barely registers against an already-near-black surface,
     so the dark counterpart leans on more opacity rather than more blur to
     stay legible as a shadow at all. */
  --dark-shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3), 0 16px 40px -8px rgba(0, 0, 0, 0.5);
  --dark-icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960' fill='%238a867e'%3E%3Cpath d='M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z'/%3E%3C/svg%3E");
}

[data-theme="dark"] {
  --paper: var(--dark-paper);
  --sheet: var(--dark-sheet);
  --sheet-2: var(--dark-sheet-2);
  --rule: var(--dark-rule);
  --rule-strong: var(--dark-rule-strong);
  --ink: var(--dark-ink);
  --ink-muted: var(--dark-ink-muted);
  --ink-faint: var(--dark-ink-faint);
  --lime-wash: var(--dark-lime-wash);
  --lime-text: var(--dark-lime-text);
  --lime-mark: var(--dark-lime-mark);
  --lime-deep: var(--dark-lime-deep);
  --lime-hover: var(--dark-lime-hover);
  --danger: var(--dark-danger);
  --chart-wash: var(--dark-chart-wash);
  --rank-1: var(--dark-rank-1);
  --rank-2: var(--dark-rank-2);
  --rank-3: var(--dark-rank-3);
  --icon-chevron: var(--dark-icon-chevron);
  --shadow-card: var(--dark-shadow-card);

  color-scheme: dark;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --paper: var(--dark-paper);
    --sheet: var(--dark-sheet);
    --sheet-2: var(--dark-sheet-2);
    --rule: var(--dark-rule);
    --rule-strong: var(--dark-rule-strong);
    --ink: var(--dark-ink);
    --ink-muted: var(--dark-ink-muted);
    --ink-faint: var(--dark-ink-faint);
    --lime-wash: var(--dark-lime-wash);
    --lime-text: var(--dark-lime-text);
    --lime-mark: var(--dark-lime-mark);
    --lime-deep: var(--dark-lime-deep);
    --lime-hover: var(--dark-lime-hover);
    --danger: var(--dark-danger);
    --chart-wash: var(--dark-chart-wash);
    --rank-1: var(--dark-rank-1);
    --rank-2: var(--dark-rank-2);
    --rank-3: var(--dark-rank-3);
    --icon-chevron: var(--dark-icon-chevron);
    --shadow-card: var(--dark-shadow-card);

    color-scheme: dark;
  }
}

/* --- Reset ---------------------------------------------------------------- */

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

html {
  /* iOS inflates text in landscape unless this is pinned. */
  -webkit-text-size-adjust: 100%;

  /* .site-header is sticky at top:0, so anything scrolled to by an anchor —
     the "Skip to content" link, a section id, the scrollIntoView() that
     initPagedSection() does when you page the cast list — lands underneath it.
     Reserving the header's own min-height plus a little air fixes all three at
     once, in every engine, without a scroll-offset hack per call site. */
  scroll-padding-top: calc(60px + var(--space-5));

  /* iOS/Android paint an opaque grey rectangle over any tapped link or button,
     which on a poster card covers the artwork. A touch device has no hover to
     fall back on, so the feedback is kept rather than zeroed out — recoloured
     to the site's own wash instead of removed. */
  -webkit-tap-highlight-color: rgba(163, 230, 53, 0.24);
}

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

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

a {
  color: var(--lime-text);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* One focus treatment for the whole site. */
:where(a, button, input, select, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--lime-mark);
  outline-offset: 2px;
  border-radius: var(--radius);
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 700;
}

p {
  margin: 0;
}

/* --- Type roles ------------------------------------------------------------ */

.display {
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: 118%;
  line-height: 1.02;
  letter-spacing: -0.015em;
  text-transform: uppercase;
}

/* The shelf label: condensed, spaced, small. Used for record-row labels,
   table headers, stat labels and chips on the site. */
.label {
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-faint);
}

/* Accession numbers, surrogate keys, IDs, and every numeric cell. The
   subject of this project is a star schema with real primary keys — the
   mono face is honest about that rather than hiding it. */
.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

.muted { color: var(--ink-muted); }
.faint { color: var(--ink-faint); }

/* Available to screen readers, removed from the visual page. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --- Page shell ------------------------------------------------------------ */

.skip-link {
  position: absolute;
  left: var(--space-4);
  top: calc(-1 * var(--space-10));
  z-index: var(--z-skip);
  background: var(--ink);
  color: var(--paper);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius);
  font-size: var(--text-sm);
  font-weight: 600;
  transition: top var(--dur) var(--ease);
}

.skip-link:focus {
  top: var(--space-4);
  text-decoration: none;
}

.container {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--space-6);
}

main.container {
  padding-block: var(--space-8) var(--space-10);
}

/* --- Header ---------------------------------------------------------------- */

/* The header floats: a frosted bar inset from the page edges rather than a
   full-width band, so a film's backdrop can run behind it on the home and
   film pages. The <header> itself is transparent and click-through; only
   the bar takes pointer events. */
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  padding-top: calc(env(safe-area-inset-top, 0px) + var(--space-4));
  pointer-events: none;
}

/* Full-bleed pages (home, film) start their hero at the very top, under the
   bar, so the header leaves the flow there. */
.page-home .site-header,
.page-film .site-header {
  position: fixed;
  left: 0;
  right: 0;
}

.site-header .nav {
  /* A variable so the language switcher, which is a direct child of the row
     rather than one of the links, can offset itself back to the links' own
     spacing (see .lang-switch). */
  --nav-gap: var(--space-4);
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--nav-gap);
  min-height: 58px;
  flex-wrap: wrap;
  width: calc(100% - 2 * var(--space-5));
  max-width: 1180px;
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-6);
  border: 1px solid var(--rule);
  border-radius: 29px;
  background: color-mix(in srgb, var(--paper) 58%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  backdrop-filter: blur(18px) saturate(1.4);
  transition: background var(--dur-slow) var(--ease),
              box-shadow var(--dur-slow) var(--ease),
              border-color var(--dur-slow) var(--ease);
}

/* Set by cinema.js while the reader scrolls down: the whole header slides up
   out of view and returns on the first scroll up. The extra 48px clears the
   bar's own drop shadow, which would otherwise leave a smudge at the top edge.
   `transform` only exists in this state (and while it eases back), so the
   header never becomes a containing block for fixed descendants at rest. */
.site-header {
  transition: transform var(--dur-slow) var(--ease);
}

.site-header.is-hidden {
  transform: translateY(calc(-100% - 48px));
}

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

/* Set by cinema.js once the page has scrolled: the bar firms up so links
   stay legible over busy posters. */
.site-header.is-scrolled .nav {
  background: color-mix(in srgb, var(--paper) 84%, transparent);
  border-color: var(--rule-strong);
  box-shadow: 0 18px 40px -24px rgb(0 0 0 / 0.6);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: 118%;
  font-size: var(--text-md);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
}

.brand:hover {
  text-decoration: none;
  color: var(--ink);
}

/* The logomark is an inline <svg> in base.html rather than a ::before, so it
   scales with the type and can invert on dark ground. Sized to the wordmark's
   cap height, not its em box, so the Θ and the T optically match. */
.brand .mark {
  width: 0.92em;
  height: 1.16em;
  flex: none;
  overflow: visible;
}

.nav-links {
  display: flex;
  gap: var(--space-1);
  flex-wrap: wrap;
  margin-left: auto;
}

.nav-links a {
  position: relative;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink-muted);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-pill);
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.nav-links a:hover {
  color: var(--ink);
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  text-decoration: none;
}

/* Active page: a lime point under the label. Same "this one is measured"
   logic as the keyed frames on the contact sheet. */
.nav-links a[aria-current="page"] {
  color: var(--ink);
  font-weight: 600;
}

.nav-links a[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 2px;
  width: 4px;
  height: 4px;
  margin-left: -2px;
  border-radius: 50%;
  background: var(--lime);
  box-shadow: 0 0 10px var(--lime);
}

/* --- Theme toggle & nav toggle ----------------------------------------------
   Two quiet icon buttons in the header, same shape. Each pair of glyphs is
   stacked and swapped by opacity so neither control changes size when it's
   pressed. */

.theme-toggle,
.nav-toggle {
  display: inline-grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  background: none;
  border: 1px solid var(--rule-strong);
  border-radius: 50%;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}

.theme-toggle:hover,
.nav-toggle:hover {
  color: var(--ink);
  border-color: var(--lime-mark);
  background: none;
}

/* Additive to the shared shape/size above: both icon buttons are filled,
   not outlined -- same "mass instead of a hairline" treatment as the
   collection actions on a movie page. The hamburger also gets an active
   state the theme switch has no equivalent of: solid lime while the menu
   it controls is open, same "on" language as the account trigger's expanded
   state and a collection action's is-selected. */
.theme-toggle,
.nav-toggle {
  border: none;
  background: var(--sheet-2);
  color: var(--ink);
}

.theme-toggle:hover,
.nav-toggle:hover {
  background: var(--lime-wash);
  color: var(--lime-deep);
}

.nav-toggle[aria-expanded="true"] {
  background: var(--lime);
  color: var(--ink-on-lime);
}

.nav-toggle[aria-expanded="true"]:hover {
  background: var(--lime-hover);
  color: var(--ink-on-lime);
}

.theme-toggle svg,
.nav-toggle svg {
  grid-area: 1 / 1;
  width: 16px;
  height: 16px;
  transition: opacity var(--dur-fast) var(--ease);
}

/* Show the glyph for the mode you'd switch TO, which is the conventional
   affordance: a moon means "go dark". */
.theme-toggle .icon-dark { opacity: 1; }
.theme-toggle .icon-light { opacity: 0; }

[data-theme="dark"] .theme-toggle .icon-dark { opacity: 0; }
[data-theme="dark"] .theme-toggle .icon-light { opacity: 1; }

/* --- Language switcher -----------------------------------------------------
   A pill that shows the current language (EN) and opens a short list of all
   three, each in its own language -- there are only three, so a reader who
   can't read the current one can still find theirs by name. The trigger is the
   same lime-outlined chip as Sign in (`.signin-btn--header`) at the 34px
   height of the icon buttons beside it: lime outline and lime text at rest,
   lime wash with deeper text on hover and while open. The list is the
   account popover's sheet -- rounded, lifted, eased in -- right-aligned under
   the trigger, since the switcher sits at the far end of the header.

   Built on a native <details>, so it opens, closes and works with no JS; each
   option is a submit button of the one form. theoria.js only adds closing on
   an outside click, Escape and focus leaving. */

.lang-switch {
  flex: none;
  /* Sits the width of the links' own gap from the Sign in chip, not the
     wider gap between the nav's top-level items. */
  margin: 0 0 0 calc(var(--space-6) - var(--nav-gap));
}

.lang-menu {
  position: relative;
}

.lang-menu__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 34px;
  padding: 0 var(--space-3) 0 var(--space-4);
  border: 1px solid var(--lime-mark);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--lime-text);
  font-family: var(--font-label);
  font-size: var(--text-3xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  list-style: none; /* drops the native disclosure triangle */
  user-select: none;
  transition: background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.lang-menu__trigger::-webkit-details-marker {
  display: none;
}

.lang-menu__trigger:hover,
.lang-menu[open] > .lang-menu__trigger {
  background: var(--lime-wash);
  color: var(--lime-deep);
}

.lang-menu__chevron {
  width: 0.7rem;
  height: 0.7rem;
  flex: none;
  transition: transform var(--dur-fast) var(--ease);
}

.lang-menu[open] .lang-menu__chevron {
  transform: rotate(180deg);
}

.lang-menu__panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  z-index: 30;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 11rem;
  padding: var(--space-2);
  background: var(--paper);
  border: var(--hairline);
  border-radius: 0.875rem;
  box-shadow: var(--shadow-card);
  transform-origin: top right;
  animation: lang-menu-in var(--dur) var(--ease);
}

@keyframes lang-menu-in {
  from {
    opacity: 0;
    transform: translateY(-4px) scale(0.98);
  }
}

/* One row per language: its code in a small chip, then its own name. The
   global button{} treatment (lime fill, uppercase, pill, and a button:hover
   that outranks a bare class) is shed here the way .menu-option sheds it. */
.lang-switch__opt {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  background: none;
  border: none;
  border-radius: 0.5rem;
  color: var(--ink);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 400;
  font-stretch: normal;
  letter-spacing: normal;
  text-align: left;
  text-transform: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.lang-switch__opt:hover,
.lang-switch__opt:focus-visible {
  background: var(--sheet-2);
  color: var(--ink);
  outline-offset: -2px;
}

.lang-switch__code {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  min-width: 2rem;
  height: 1.5rem;
  border-radius: var(--radius-pill);
  background: var(--sheet-2);
  font-family: var(--font-label);
  font-size: var(--text-3xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  transition: background var(--dur-fast) var(--ease);
}

.lang-switch__opt:hover .lang-switch__code,
.lang-switch__opt:focus-visible .lang-switch__code {
  background: var(--paper);
}

/* The current language: lime chip and bold lime-text name, the filter menu's
   selected-row treatment. Not clickable -- it is already the page's language. */
.lang-switch__opt[aria-current="true"] {
  color: var(--lime-text);
  font-weight: 600;
  cursor: default;
}

.lang-switch__opt[aria-current="true"] .lang-switch__code {
  background: var(--lime);
  color: var(--ink-on-lime);
}

@media (prefers-reduced-motion: reduce) {
  .lang-menu__panel {
    animation: none;
  }
  .lang-menu__chevron {
    transition: none;
  }
}

/* Touch rows get a ~44px target, like .menu-option's coarse-pointer bump. */
@media (pointer: coarse) {
  .lang-switch__opt {
    padding-block: var(--space-3);
  }
}

/* Hamburger by default; hidden entirely above the breakpoint and, without
   JS, hidden below it too — see the has-js gate further down, which is the
   only place this button is ever made visible. */
.nav-toggle {
  display: none;
}

.nav-toggle .icon-menu { opacity: 1; }
.nav-toggle .icon-close { opacity: 0; }

.nav-toggle[aria-expanded="true"] .icon-menu { opacity: 0; }
.nav-toggle[aria-expanded="true"] .icon-close { opacity: 1; }

/* --- Personal collections --------------------------------------------------- */

/* The account page ("/me/") has its own list now — see static/css/account.css
   — but a movie/series detail page's Like/Watch later/Top pills
   (`.collection-action`) still use this pill shape: a filled pill, not an
   outline one -- mass instead of a hairline. Icons are static SVGs, masked
   the same way the nav's account icons are, so the outline glyph tints with
   the button's own text colour at rest and hover -- no separate colour to
   keep in sync. */
.collection-action {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: var(--space-2);
  min-height: 2rem;
  padding: var(--space-2) var(--space-3);
  border: none;
  border-radius: var(--radius-pill);
  background: var(--sheet-2);
  color: var(--ink);
  font-family: var(--font-label);
  font-weight: 700;
  font-size: var(--text-3xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.collection-action:hover {
  background: var(--lime-wash);
  color: var(--lime-deep);
}

/* Selected pill stays the resting sheet-2/ink fill -- unlike a filter chip,
   this is a personal mark on the title, not a mode switch, so the pill
   itself doesn't need to shout. The icon carries the "on" state instead
   (below) -- it blooms in its collection's colour -- and the label swaps to
   the past tense, so the state reads in words too, not only in hue. */

.collection-action__icon {
  position: relative;
  width: 0.85rem;
  height: 0.85rem;
  flex: none;
}

/* Two stacked masks, same glyph pair: ::before is the outline in the
   button's own text colour; ::after is the filled glyph in the collection's
   colour, clipped to nothing at rest and painted over the outline, which
   drops out once selected so the colour shows with no edge around it. Each modifier
   below only points --icon-outline/--icon-fill at its own pair of SVGs. */
.collection-action__icon::before,
.collection-action__icon::after {
  content: "";
  position: absolute;
  inset: 0;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  -webkit-mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: contain;
}

.collection-action__icon::before {
  -webkit-mask-image: var(--icon-outline);
  mask-image: var(--icon-outline);
  background: currentColor;
}

.collection-action__icon::after {
  -webkit-mask-image: var(--icon-fill);
  mask-image: var(--icon-fill);
  background: var(--icon-fill-colour);
  clip-path: circle(0% at 50% 55%);
  transition: clip-path 320ms var(--ease-bloom);
}

/* Both labels are in the markup; the state picks which one shows. The
   button's aria-label stays the plain action name, since aria-pressed
   already tells a screen reader it's on -- "Liked, pressed" would say it
   twice. */
.collection-action.is-selected .collection-action__label--off,
.collection-action:not(.is-selected) .collection-action__label--on {
  display: none;
}

.collection-action--like .collection-action__icon {
  --icon-outline: url("../img/icons/thumb-up.3126c01f9d71.svg");
  --icon-fill: url("../img/icons/thumb-up-fill.c8c32714b28a.svg");
  --icon-fill-colour: var(--mark-like);
}

.collection-action--watch-later .collection-action__icon {
  --icon-outline: url("../img/icons/bookmark.9475db9608ea.svg");
  --icon-fill: url("../img/icons/bookmark-fill.97eb38ab2c7f.svg");
  --icon-fill-colour: var(--mark-watch-later);
}

.collection-action--top .collection-action__icon {
  --icon-outline: url("../img/icons/star.1740b75378a3.svg");
  --icon-fill: url("../img/icons/star-fill.d164b1190aeb.svg");
  --icon-fill-colour: var(--mark-top);
}

/* Bloom: the fill opens from the glyph's centre while the icon swells
   slightly -- the swell is what makes it feel responsive under a thumb.
   Selecting: the outline waits for the bloom to cover it (260ms, just short
   of the 320ms clip) before dropping out. Deselecting: the outline is back
   at once while the fill drains. A transition runs with the rules of the
   state being entered, so the delay lives on the selected rule only. */
.collection-action.is-selected .collection-action__icon::after {
  clip-path: circle(75% at 50% 55%);
}

.collection-action.is-selected .collection-action__icon::before {
  opacity: 0;
  transition: opacity 0ms linear 260ms;
}

@keyframes collection-action-swell {
  0% { transform: scale(1); }
  45% { transform: scale(1.22); }
  100% { transform: scale(1); }
}

/* JS adds this class only after a click toggles the pill on (stripping it
   first, so a repeat click replays) -- so the swell plays on the action
   just clicked, not on every pill that renders already selected. */
.collection-action.just-toggled-on .collection-action__icon {
  animation: collection-action-swell 360ms var(--ease-bloom);
}

@media (prefers-reduced-motion: reduce) {
  /* Matches the selected rule's specificity, or its delay would survive. */
  .collection-action .collection-action__icon::after,
  .collection-action.is-selected .collection-action__icon::before {
    transition: none;
  }
  .collection-action.just-toggled-on .collection-action__icon {
    animation: none;
  }
}

.collection-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  border-top: var(--hairline);
}

.collection-actions form {
  margin: 0;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .theme-toggle .icon-dark { opacity: 0; }
  :root:not([data-theme]) .theme-toggle .icon-light { opacity: 1; }
}

/* --- Footer ---------------------------------------------------------------- */

.site-footer {
  position: relative;
  border-top: var(--hairline);
  margin-top: var(--space-9);
  padding-block: var(--space-9) var(--space-7);
  color: var(--ink-faint);
  font-size: var(--text-xs);
}

/* The wordmark set huge and hollow above the footer line: the last thing on
   every page, like the title card after the credits. */
.footer-mark {
  font-family: var(--font-display);
  font-weight: 900;
  font-stretch: 125%;
  font-size: clamp(4rem, calc(19 * var(--vw, 1vw)), 17rem);
  line-height: 0.8;
  letter-spacing: -0.04em;
  text-transform: uppercase;
  text-align: center;
  color: transparent;
  -webkit-text-stroke: 1px var(--rule-strong);
  white-space: nowrap;
  overflow: hidden;
  user-select: none;
  padding-bottom: var(--space-7);
}

.site-footer .footer-row {
  display: flex;
  gap: var(--space-5);
  justify-content: space-between;
  flex-wrap: wrap;
}

/* --- The sheet header ------------------------------------------------------
   Every page opens with this band: the title in expanded display, an
   optional sub-line, then a hairline. It is the single device that makes
   ten different pages read as one site. */

.sheet-head {
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: var(--space-5);
}

.sheet-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: 118%;
  font-size: var(--text-2xl);
  line-height: 1.04;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  text-wrap: balance;
}

.sheet-sub {
  margin-top: var(--space-3);
  color: var(--ink-muted);
  font-size: var(--text-md);
  max-width: 62ch;
}

/* --- Sections --------------------------------------------------------------
   ONE owner for vertical rhythm. Sections never set their own top margin —
   that is how the old stylesheet ended up with paddings cancelling out. */

.sheet-section + .sheet-section,
.sheet-head + .sheet-section,
.stats + .sheet-section {
  margin-top: var(--space-9);
}

.section-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-3);
  border-bottom: var(--hairline);
}

.section-head h2 {
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.section-head .section-note {
  margin-left: auto;
  font-size: var(--text-2xs);
  color: var(--ink-faint);
  font-family: var(--font-mono);
}

/* --- Stat row --------------------------------------------------------------
   Ruled cells rather than boxed tiles — a catalog record, not a dashboard. */

.stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
}

.stat {
  padding: var(--space-5) 0 0;
}

.stat .stat-value {
  position: relative;
  display: block;
  padding-top: var(--space-3);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--text-lg);
  line-height: 1.3;
  min-height: 1.3em; /* an em-dash placeholder occupies the same height */
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* The tick above each figure is the same "this one is measured" mark as the
   active nav rule and the meter fills — a caliper mark, not a divider. */
.stat .stat-value::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 22px;
  height: 3px;
  background: var(--lime);
}

.stat .stat-label {
  display: block;
  margin-top: var(--space-3);
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-3xs);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-faint);
}

/* --- Poster / person cards -------------------------------------------------
   Frames on a sheet: hairline border, no shadow, no radius to speak of. */

.poster-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: var(--space-5);
}

.poster-grid.is-tight {
  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
  gap: var(--space-4);
}

/* Landscape cards (studio logos) need more width per card than a 2:3
   portrait poster does, or a wide wordmark logo has almost no room to
   render at a legible size. */
.poster-grid.is-wide {
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: var(--space-6);
}

.poster-card a {
  display: block;
  color: var(--ink);
}

.poster-card a:hover { text-decoration: none; }

.poster-card .poster,
.poster-card .headshot,
.poster-card .logo,
.poster-card .placeholder {
  aspect-ratio: 2 / 3;
  width: 100%;
  object-fit: cover;
  background: var(--sheet);
  border: var(--hairline);
  transition: border-color var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}

.poster-card .placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--rule-strong);
  font-size: var(--text-xl);
}

/* Person fallback: a generic profile silhouette, not an empty box — reads
   as a deliberate stand-in for a missing TMDB photo rather than a broken
   image. */
.poster-card .placeholder-person,
.person-plate .placeholder-person,
.credit-list .placeholder-person {
  background: linear-gradient(160deg, var(--sheet) 0%, var(--sheet-2) 100%);
}

.placeholder-person .person-icon {
  width: 42%;
  height: 42%;
  color: var(--rule-strong);
}

/* Studio logos are landscape/transparent wordmarks, not portrait photos —
   cropping one with object-fit: cover (the default above) would cut letters
   off, so both the real logo and its fallback get a shorter, padded, contain
   frame instead. Coverage is ~46% (Task 59's measured figure), so the
   fallback is an initial-letter monogram rather than a hand-drawn icon.

   Both share ONE flat --logo-plate background (not the sheet gradient the
   person placeholder uses) so a card with a logo and a card without one read
   as the same family of tile at a glance — the previous version had the real
   logo on --paper (white, but flips dark with the theme) beside a gradient
   placeholder, two different surfaces for what should look like one kind of
   card. --logo-plate is fixed rather than theme-tokened for the same reason:
   TMDB's logo assets assume a light backing regardless of site theme. */
.poster-card .logo,
.poster-card .placeholder-studio {
  aspect-ratio: 2 / 1;
  background: var(--logo-plate);
}

.poster-card .logo {
  object-fit: contain;
  object-position: center;
  padding: var(--space-5);
}

.placeholder-studio .studio-initial {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-2xl);
  color: var(--logo-ink);
}

.poster-card a:hover .poster,
.poster-card a:hover .headshot,
.poster-card a:hover .logo,
.poster-card a:hover .placeholder,
.poster-card a:focus-visible .poster,
.poster-card a:focus-visible .headshot,
.poster-card a:focus-visible .logo {
  border-color: var(--lime-mark);
  transform: translateY(-3px);
}

.poster-card .card-body {
  padding-top: var(--space-3);
}

.poster-card .card-title {
  /* Clamp at two lines everywhere, so a stray long name can't shove a card's
     layout around — but this is only a max: the title takes just the height
     it needs. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.3;
}

/* Only film cards reserve the second title line whether the title fills it or
   not, so the sub-line and rating badge below sit at the same height across a
   row even when some titles wrapped and some didn't. Person and studio names
   are nearly always one line, so reserving it there just opens a gap above
   the sub-line. */
.poster-card.is-film .card-title {
  min-height: calc(2 * 1.3em);
}

/* The numeric sub-line: a release year, or a "Pop 8.4" fallback. Mono suits a
   bare figure. Prose sub-lines (a character/role) use .card-role instead. */
.poster-card .card-sub {
  display: block;
  margin-top: var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  /* Medium (not bold) + one step up from --ink-faint: the sub-line was
     easy to miss against the title above it. */
  font-weight: 500;
  color: var(--ink-muted);
}

/* A performance's role/character. Personnel face (Work Sans), same size and
   weight footprint as the mono sub it replaces. See --font-role. */
.poster-card .card-role {
  display: block;
  margin-top: var(--space-1);
  font-family: var(--font-role);
  font-size: var(--text-2xs);
  line-height: 1.3;
  /* Same medium weight + --ink-muted as .card-sub, so a card's sub-line reads
     at one strength across every page whether it carries a figure or prose. */
  font-weight: 500;
  color: var(--ink-muted);
}

/* A filmography's job_display ranges from one word ("Thanks") to a joined
   string of every job a person held on that film — the credits data has
   rows up to 9 jobs deep. Without a reserved, clamped height here, the rating
   badge under each card lands at a different row depending on how many lines
   that card's role text happened to wrap to, and the grid reads as uneven
   rather than as a sheet of frames sharing one plan. Clamped at 3 lines: wide
   enough to show a common 4-job stack ("Director / Screenplay / Story /
   Producer") in full, and a graceful cutoff for the rare deeper one — the
   same trade-off .card-title already makes for a long name. Scoped to
   .is-film, same as .card-title's own min-height above: a rating badge only
   ever follows a film card, so only a film card needs this alignment. */
.poster-card.is-film .card-role {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  min-height: calc(3 * 1.3em);
}

/* --- Rating badge (IMDb) ----------------------------------------------------
   Added component, Task 68 — a text "IMDb" chip plus the figure and its
   "/ 10" scale, and on the movie detail page, the vote count and an outbound
   link. Compact (poster cards) vs. full (movie detail) is entirely which
   optional params _rating_badge.html was given, not a class switch, so there
   is one .rating-badge rather than two. IMDb yellow (#F5C518) is confined to
   the chip's fill — nothing else here recolours toward it, since that yellow
   must never be mistaken for this site's own lime measurement mark, which
   stays reserved for meter bars, keyed posters, and the active nav link. */

.rating-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--ink);
  text-decoration: none;
}

.poster-card .rating-badge {
  margin-top: var(--space-2);
}

a.rating-badge:hover .rating-badge-figure,
a.rating-badge:focus-visible .rating-badge-figure {
  color: var(--lime-text);
}

/* The mark is CSS text, not an image: "IMDb" in the brand yellow (#F5C518) on a
   dark chip, the way the real logo reads. The SVG wordmark went illegible at
   the small sizes this badge sits in, and a black-on-yellow chip stays crisp at
   any size. The yellow is confined to this chip's fill — nothing here tints
   toward it — so it can't be mistaken for the site's lime measurement mark. */
.rating-badge-mark {
  flex-shrink: 0;
  padding: 2px 4px;
  border-radius: 3px;
  background: #f5c518;
  color: #000;
  font-family: var(--font-label);
  font-weight: 700;
  font-stretch: 78%;
  font-size: var(--text-3xs);
  line-height: 1;
  letter-spacing: 0.02em;
}

.rating-badge-figure {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: var(--text-sm);
}

.rating-badge-votes {
  margin-left: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--ink-faint);
}

/* The detail page's badge stands in for a whole record-row dd. The figure
   matches the other rows' values (--text-sm, inherited); only the chip is
   stepped up so the "IMDb" mark stays legible. record-row--rating centres the
   12px label against the taller badge — without it the label top-aligns and
   the row looks off. */
.record-row--rating {
  align-items: center;
}

.record-row .rating-badge-mark {
  padding: 3px 5px;
  font-size: var(--text-2xs);
}

/* --- Chips ----------------------------------------------------------------- */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.chip {
  font-family: var(--font-label);
  font-weight: 700;
  font-stretch: 78%;
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink);
  background: var(--sheet-2);
  border: none;
  border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-4);
  transition: background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.chip:hover {
  background: var(--lime-wash);
  color: var(--lime-deep);
  text-decoration: none;
}

/* --- Tables ---------------------------------------------------------------- */

.table-wrap {
  overflow-x: auto;
  /* Without this, swiping a wide analytics table sideways on iOS Safari or
     Chrome for Android runs off the end of the scroller and triggers the
     browser's back-navigation gesture instead — the reader loses the page. */
  overscroll-behavior-x: contain;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

thead th {
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-3xs);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-faint);
  text-align: left;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--rule-strong);
  white-space: nowrap;
  background: var(--paper);
}

tbody td {
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--hairline);
  vertical-align: middle;
}

tbody tr:hover { background: var(--sheet); }

/* Row-header cells are <th scope="row"> for structure, but should read as
   body text, not as a column label. */
tbody th[scope="row"] {
  font-weight: 600;
  font-size: var(--text-sm);
  text-align: left;
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--hairline);
}

/* Spline Sans Mono is loaded at 400/500 only. Asking for 600 makes the
   browser synthesize the bold, which smears the glyphs — so mono cells cap
   at the heaviest weight actually available. */
tbody th[scope="row"].mono,
.mono {
  font-weight: 500;
}

tbody th.genre-cell a { color: var(--ink); }
tbody th.genre-cell a:hover { color: var(--lime-text); }

th.num, td.num {
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
  white-space: nowrap;
}

/* --- Meter cells -----------------------------------------------------------
   A lime bar drawn behind the number, scaled to the column max. The bar is
   below the 3:1 mark floor on purpose — the value is printed on top of it in
   the same cell, which is the documented relief channel. Never use a bare
   lime bar without its number. Driven by initMeters() in theoria.js.

   The bar grows leftward from its right-aligned number, so the LEFT edge is
   the one that moves with the value — that edge carries the lime rule, and
   the fill behind it stays a pale wash. (Marking the right edge would put
   every bar's rule in the same place, encoding nothing.)

   The cell is width-limited rather than the fill: capping .meter-fill would
   flatten the top of the range, making 34 and 15 land in the same place. */

td[data-meter] {
  position: relative;
  isolation: isolate;
  padding-right: var(--space-5);
}

/* Two-column ranked sheets (genres, revenue-by-year) give the bar real room,
   so the range is legible. In a dense multi-column analytics table the bar
   is a sparkline beside its number and takes whatever the column allows. */
.table-2col td[data-meter] {
  width: 42%;
}

/* The track: reserves the right-hand strip for the number so a full-width
   bar can never run under its own digits. The fill's percentage width is
   then relative to THIS box, not the cell, so 100% lands exactly here. */
td[data-meter] .meter-track {
  position: absolute;
  inset: 4px 3.25rem 4px 0;
  z-index: -1;
  pointer-events: none;
}

td[data-meter] .meter-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: 0;
  background: var(--lime-wash);
  border-left: 2px solid var(--lime);
  transition: width var(--dur-slow) var(--ease);
}

/* --- Toolbar & controls ----------------------------------------------------- */

.toolbar {
  display: flex;
  gap: var(--space-5);
  align-items: center;
  justify-content: flex-start;
  flex-wrap: wrap;
  margin-bottom: var(--space-6);
}

.toolbar form {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  align-items: center;
}

.field {
  display: flex;
  align-items: center;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  background: var(--paper);
  transition: border-color var(--dur-fast) var(--ease);
}

.field:focus-within {
  border-color: var(--lime-mark);
}

/* A magnifier sits at the head of every search field. It is a masked shape,
   not an <img>, so its colour tracks the theme (and turns lime while the
   field is focused, matching the border). Placed on the .field wrapper via
   :has() so no search template has to carry the markup, and never on the
   <select> fields that share the .field shell. */
.field:has(input[type="search"])::before {
  content: "";
  flex: none;
  width: 1rem;
  height: 1rem;
  margin-left: var(--space-4);
  background-color: var(--ink-faint);
  -webkit-mask: var(--icon-search) center / contain no-repeat;
  mask: var(--icon-search) center / contain no-repeat;
}

.field:has(input[type="search"]:focus)::before {
  background-color: var(--lime-mark);
}

input[type="text"],
input[type="search"],
input[type="email"] {
  /* Safari styles input[type=search] as a native rounded field and refuses
     several of the declarations below until the native appearance is dropped;
     the reset is what makes the control render identically in all four
     engines rather than only in Chrome and Firefox. Extending a shared
     control (rather than styling input[type=email] on its own in a page
     stylesheet) is theoria.css's job -- see the auth pages' design notes. */
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: none;
  color: var(--ink);
  font-family: inherit;
  font-size: var(--text-sm);
  padding: var(--space-3) var(--space-5);
  min-width: 200px;
}

/* Safari draws its own magnifier and a grey clear-cross inside a search field.
   The magnifier duplicates the one .field already places, and the cross sits
   where the design puts nothing — both are engine chrome the other browsers
   never show, so they go. */
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button {
  -webkit-appearance: none;
  appearance: none;
}

/* Placeholders inherit neither colour nor opacity consistently: Firefox applies
   its own opacity on top of the colour, so the same hex reads lighter there. */
::placeholder {
  color: var(--ink-faint);
  opacity: 1;
}

/* The icon already supplies the left inset for a search field. */
.field:has(input[type="search"]) input[type="search"] {
  padding-left: var(--space-3);
}

input[type="text"]:focus,
input[type="search"]:focus,
input[type="email"]:focus {
  outline: none;
}

/* A <select> lives inside the same .field wrapper as the search input, so it
   needs to shed its own border and pick up the shared font/colour instead —
   otherwise it renders as unstyled system chrome next to a designed one. */
.field select {
  /* Left to itself a <select> is the least portable control on the page:
     Safari keeps a rounded inset plate and ignores the transparent background,
     Firefox and Chrome each draw a different arrow in a different spot, and iOS
     centres the label. appearance:none flattens all of that, and --icon-chevron
     draws the one arrow back — so the field looks the same everywhere instead
     of only where it was last checked. */
  -webkit-appearance: none;
  appearance: none;
  border: none;
  /* Longhands, not the background shorthand: the shorthand would have to carry
     a var() inside its position/size slash form, and anything unresolvable
     there fails the whole declaration at computed-value time — which would take
     the chevron and the transparent ground down together. Split out, each piece
     fails on its own or not at all. */
  background-color: transparent;
  background-image: var(--icon-chevron);
  background-repeat: no-repeat;
  background-position: right var(--space-4) center;
  background-size: 0.7rem;
  color: var(--ink);
  font-family: inherit;
  font-size: var(--text-sm);
  padding: var(--space-3) var(--space-4);
  /* Clear the chevron the background now draws. */
  padding-right: calc(var(--space-4) + 1.1rem);
  cursor: pointer;
}

.field select:focus {
  outline: none;
}

/* Filter menu — the JS-built replacement for a long <select> in a toolbar
   (see initFilterMenu() in theoria.js). The native <select> picker opens a
   full-screen OS list on a phone; this keeps the choices to a short scroll
   inside the page. Additive: no rule above is touched, and with JS off the
   real <select> is what renders. Every value is a theme token, so the panel
   follows light/dark with no dark-mode block of its own. */
.menu {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
}

/* The trigger has to shed the global button{} treatment (lime fill, uppercase,
   pill) and instead read exactly like the .field select it stands in for. */
.menu-trigger {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  background: transparent;
  background-image: var(--icon-chevron);
  background-repeat: no-repeat;
  background-position: right var(--space-4) center;
  background-size: 0.7rem;
  border: none;
  /* Pill, not square: the trigger fills the .field shell, so its tap-highlight
     and focus background should follow the same rounded edge. */
  border-radius: var(--radius-pill);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 400;
  font-stretch: normal;
  letter-spacing: normal;
  text-transform: none;
  padding: var(--space-3) var(--space-4);
  padding-right: calc(var(--space-4) + 1.1rem);
  cursor: pointer;
}

.menu-label {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.menu-panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: 30;
  width: max-content;
  min-width: 100%;
  max-width: min(80vw, 18rem);
  max-height: min(60vh, 20rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  padding: var(--space-1);
}

.menu-option {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  text-align: left;
  white-space: nowrap;
  background: none;
  border: none;
  border-radius: 0;
  color: var(--ink);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 400;
  font-stretch: normal;
  letter-spacing: normal;
  text-transform: none;
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}

/* A dot slot on every row so the label baseline doesn't shift when one is
   marked; only the current choice fills it. */
.menu-option::before {
  content: "";
  flex: none;
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  background: transparent;
}

.menu-option:hover,
.menu-option:focus-visible {
  background: var(--sheet);
  outline-offset: -2px;
}

.menu-option[aria-selected="true"] {
  color: var(--lime-text);
  font-weight: 600;
}

.menu-option[aria-selected="true"]::before {
  background: var(--lime-mark);
}

/* The signed-in header menu (Task 95) reuses .menu/.menu-trigger/.menu-panel
   wholesale -- built for the filter dropdown, but already generic enough to
   host an account menu with only these additions. Two of .menu's own defaults
   are toolbar-specific and have to be overridden rather than inherited: its
   flex:1 1 auto (meant to fill a .field row next to a search input) would
   otherwise stretch this item across the nav bar, and vertically fill the
   mobile menu's open column since flex-grow tracks whichever axis
   flex-direction currently points along. */
.account-menu {
  flex: none;
}

/* Filled, not outlined -- same "mass instead of a hairline" treatment the
   collection actions on a movie page use: icon (the disc) + label in one
   solid pill, bold ink at rest, lime on hover and while the panel it opens
   is expanded or you're already on the profile page it links to. The disc
   itself stays a constant paper circle -- an avatar has to read the same
   whichever state the pill around it is in, so only the pill's fill and the
   username swap colour. */
.account-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  background-image: none; /* no chevron -- this isn't a <select> stand-in */
  background-color: var(--sheet-2);
  padding-left: var(--space-2);
  padding-right: var(--space-4);
  font-weight: 700;
  color: var(--ink);
  transition: background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.account-trigger:hover {
  background-color: var(--lime-wash);
  color: var(--lime-deep);
}

.account-trigger[aria-expanded="true"],
.account-trigger[aria-current="page"] {
  background-color: var(--lime);
  color: var(--ink-on-lime);
}

.account-disc {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-label);
  font-size: var(--text-2xs);
  font-weight: 700;
}

/* Signed-out visitors get a lime-tinted outline chip instead of a plain
   text link, so "Sign in" doesn't read as just another item alongside
   Movies/TV Shows/People. A solid lime-filled pill (the first pass here)
   read as too heavy a "cover" next to the plain nav links -- this instead
   borrows the site's own quieter "lime means selectable/special" language:
   the outline-plus-wash-on-hover .collection-action already uses elsewhere
   (border var(--lime-mark), fill var(--lime-wash), text var(--lime-deep)
   on interaction) and the lime-text/lime-mark pairing .menu-option's
   selected row uses for label + marker. No fill at rest, so it sits at the
   same visual weight as its siblings and stands out by colour, not mass.
   The icon is docs/icons/account_circle, copied into static/img/icons/ and
   masked the same way auth.css's field icons are.
   `.nav-links a` (color/padding-block, above) is one class + one type
   selector, which outranks a bare `.signin-btn` class rule -- the extra
   `.nav-links a.signin-btn` specificity below is what makes the lime color
   win instead of the muted nav-link grey. */
.signin-btn {
  display: inline-flex;
  align-items: center;
  align-self: center;
  gap: var(--space-2);
  margin-left: var(--space-2);
  padding-inline: var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.nav-links a.signin-btn,
.signin-btn--header {
  color: var(--lime-text);
  font-weight: 600;
  border-color: var(--lime-mark);
  background: transparent;
  padding-block: var(--space-2);
}

.nav-links a.signin-btn:hover,
.signin-btn--header:hover {
  color: var(--lime-deep);
  border-color: var(--lime-mark);
  background: var(--lime-wash);
  text-decoration: none;
}

/* Only shown at phone widths -- see the mobile nav block. */
.signin-btn--header {
  display: none;
  font-size: var(--text-sm);
  white-space: nowrap;
}

/* The signed-in twin of .signin-btn--header, same reasoning: hidden here,
   shown only in the mobile nav block below. Trimmed to the icon-buttons'
   34px row height the way that chip is.

   Unlike that fixed-text chip, a username is arbitrary length, and it turns
   out flex-shrink alone can't be trusted to keep it off a second line: with
   .nav's own flex-wrap, the browser decides what fits on row one using each
   item's *un-shrunk* hypothetical size, then wraps whatever doesn't fit
   (here, the theme toggle) to row two -- shrinking only resolves items that
   already made the cut. A max-width, by contrast, is a real ceiling on that
   hypothetical size (CSS Flexbox §9.2), so it's what actually keeps row one
   from overflowing in the first place -- confirmed against a live phone
   width sweep (320-680px) with a 10-character username, the length that
   first broke this. Sized to leave room for a short name, not a long one,
   which still ellipsizes rather than reading in full -- the disc alone
   (flex:none, never shrinks) is what stays reliably tappable and
   identifiable at every width. */
.account-menu--header {
  display: none;
}

.account-menu--header .account-trigger {
  height: 34px;
  padding-block: 0;
  padding-right: var(--space-3);
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.account-menu--header .menu-label {
  max-width: 4.5rem;
}

.signin-icon {
  width: 0.85rem;
  height: 0.85rem;
  flex: none;
  background-color: var(--lime-mark);
  -webkit-mask-image: url("../img/icons/account-circle.9b185a999925.png");
  -webkit-mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-image: url("../img/icons/account-circle.9b185a999925.png");
  mask-mode: alpha;
  mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;
  transition: background-color var(--dur-fast) var(--ease);
}

.nav-links a.signin-btn:hover .signin-icon,
.signin-btn--header:hover .signin-icon {
  background-color: var(--lime-deep);
}

/* A touch target needs more room than the mouse-row chip above gives it --
   same reasoning as .menu-trigger/.menu-option's own coarse-pointer bump
   further down. */
@media (pointer: coarse) {
  .nav-links a.signin-btn {
    padding-block: var(--space-3);
    padding-inline: var(--space-5);
  }

  .signin-icon {
    width: 1rem;
    height: 1rem;
  }
}

.account-panel form {
  margin: 0;
}

/* A floating popover rather than the filter menu's squared list: rounded,
   lifted off the page, centred under the username that opens it (`translate`,
   not `transform`, so the entrance animation's transform can't undo the
   centring), and eased in when it opens. */
.account-panel {
  left: 50%;
  right: auto;
  translate: -50% 0;
  min-width: 12.5rem;
  padding: var(--space-2);
  background: var(--paper);
  border: var(--hairline);
  border-radius: 0.875rem;
  box-shadow: var(--shadow-card);
  transform-origin: top center;
}

.account-panel:not([hidden]) {
  animation: account-panel-in var(--dur) var(--ease);
}

@keyframes account-panel-in {
  from {
    opacity: 0;
    transform: translateY(-4px) scale(0.98);
  }
}

@media (prefers-reduced-motion: reduce) {
  .account-panel:not([hidden]) {
    animation: none;
  }
}

.account-panel .menu-option {
  width: 100%;
  font: inherit;
  border-radius: 0.5rem;
  padding: var(--space-3) var(--space-4);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.account-panel .menu-option:hover,
.account-panel .menu-option:focus-visible {
  background: var(--sheet-2);
}

/* Each row leads with its icon (docs/icons, masked so it tints with the row's
   text) in the slot the filter menu uses for its selection dot. */
.account-panel .account-option::before {
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 0;
  background-color: currentColor;
  -webkit-mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-mode: alpha;
  mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;
}

.account-panel .account-option--profile::before {
  -webkit-mask-image: url("../img/icons/account-circle.9b185a999925.png");
  mask-image: url("../img/icons/account-circle.9b185a999925.png");
}

/* Red for the one row that ends the session, with a faint red wash on hover
   instead of the neutral one. */
.account-panel .account-option--signout {
  color: var(--danger);
  margin-top: var(--space-1);
}

.account-panel .account-option--signout:hover,
.account-panel .account-option--signout:focus-visible {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 10%, transparent);
}

.account-panel .account-option--signout::before {
  -webkit-mask-image: url("../img/icons/logout.fa676d6ff062.png");
  mask-image: url("../img/icons/logout.fa676d6ff062.png");
}

/* Sign-out confirmation. Same rounded, lifted sheet as the account popover,
   led by the account hero's measuring tick drawn in --danger: the one place
   that tick marks an ending rather than a figure. Padding lives on the inner
   wrapper so a click on the dialog element itself is always the backdrop. */
.signout-dialog {
  width: min(26rem, calc(100% - 2rem));
  max-width: none;
  padding: 0;
  margin: auto;
  background: var(--paper);
  color: var(--ink);
  border: var(--hairline);
  border-radius: 0.875rem;
  box-shadow: var(--shadow-card);
}

.signout-dialog[open] {
  animation: signout-dialog-in var(--dur) var(--ease);
}

.signout-dialog::backdrop {
  background: rgb(11 11 11 / 0.55);
  backdrop-filter: blur(3px);
}

@keyframes signout-dialog-in {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.98);
  }
}

.signout-dialog__inner {
  padding: var(--space-7) var(--space-7) var(--space-6);
}

.signout-dialog__bar {
  display: block;
  width: 56px;
  height: 5px;
  background: var(--danger);
}

.signout-dialog__title {
  margin-top: 18px;
  font-size: var(--text-xl);
  line-height: 0.95;
}

.signout-dialog__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-4);
  margin-top: var(--space-7);
}

.signout-dialog__actions button {
  min-height: 44px;
  padding-inline: var(--space-6);
}

.signout-dialog__no {
  background: transparent;
  color: var(--ink);
  border-color: var(--rule-strong);
}

.signout-dialog__no:hover {
  background: var(--sheet-2);
  border-color: var(--rule-strong);
  color: var(--ink);
}

/* Same solid red as /me's Sign out button, and for the same reason: --paper
   text flips with the theme, so it reads in both. */
.signout-dialog__yes {
  background: var(--danger);
  color: var(--paper);
  border-color: var(--danger);
}

.signout-dialog__yes:hover {
  background: color-mix(in srgb, var(--danger) 85%, var(--ink));
  border-color: color-mix(in srgb, var(--danger) 85%, var(--ink));
  color: var(--paper);
}

@media (max-width: 480px) {
  .signout-dialog__actions {
    flex-direction: column-reverse;
  }
}

@media (prefers-reduced-motion: reduce) {
  .signout-dialog[open] {
    animation: none;
  }
}

/* Inside the open mobile menu the account menu doesn't appear at all -- the
   .account-menu--header twin in the header row (base.html) covers a signed-
   in reader instead, the same way .nav-links a.signin-btn below cedes to
   .signin-btn--header. */
@media (max-width: 1100px) {
  html.has-js .nav-links .account-menu {
    display: none;
  }
}

/* Match the 16px the search field takes on touch (see the pointer:coarse
   block further down), and give each row a ~48px target. */
@media (pointer: coarse) {
  .menu-trigger,
  .menu-option {
    font-size: 16px;
  }
  .menu-option {
    padding-block: var(--space-4);
  }
}

/* Lime is the action colour. Ink text on lime is 13:1, so the label stays
   sharp; hover deepens the fill rather than changing hue. Text uses the fixed
   --ink-on-lime rather than --ink: --ink flips to near-white in dark mode,
   which the lime fill does not, so the themed token would go illegible. */
button,
.btn {
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  background: var(--lime);
  color: var(--ink-on-lime);
  border: 1px solid var(--lime);
  border-radius: var(--radius-pill);
  padding: var(--space-3) var(--space-5);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}

/* The People filter form's Apply button — redundant once JS is live-filtering
   on every change (see initLiveFilter() in theoria.js). Gated on html.has-js
   the same way as the mobile nav toggle above: without it, a radio or select
   change has no auto-submit, so the button is the only way to apply one. */
html.has-js .apply-btn {
  display: none;
}

button:hover,
.btn:hover {
  background: var(--lime-hover);
  border-color: var(--lime-hover);
  color: var(--ink-on-lime);
  text-decoration: none;
}

/* Segmented sort control — replaces a bare <select>. Radio inputs so it
   works with no JS and submits with the form. Rendered as a <fieldset>, so
   the browser's default fieldset chrome has to be zeroed out. */
.segmented {
  display: flex;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  overflow: hidden;
  margin: 0;
  padding: 0;
  min-width: 0; /* fieldsets default to min-content; without this it won't shrink */
}

.segmented input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.segmented label {
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-3xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  border-right: 1px solid var(--rule);
  transition: background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
  white-space: nowrap;
}

.segmented label:last-of-type { border-right: none; }

.segmented label:hover { background: var(--sheet); }

.segmented input:checked + label {
  background: var(--lime);
  color: var(--ink-on-lime);
}

.segmented input:focus-visible + label {
  outline: 2px solid var(--lime-mark);
  outline-offset: -2px;
}

/* --- Season picker (show page episodes, Task 88) ----------------------------
   Same radio+label idiom as .segmented above, but each label is its own
   pill in a wrapping row rather than sharing one clipped, bordered strip —
   a show can carry 50+ seasons (The Daily Show, live-measured), and
   .segmented's overflow:hidden would make most of them unreachable.

   Hidden until html.has-js proves initSeasonPicker() (theoria.js) can act on
   it: with no JS every season's table is already stacked in the page under
   its own heading, so an inert picker would be a dead control — the same
   posture Task 75's click-to-play trailer and .apply-btn (further up) take,
   just inverted: this control appears only once JS is confirmed, instead of
   only until it is. */
.season-picker {
  display: none;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-6);
  border: none;
  padding: 0;
}

html.has-js .season-picker {
  display: flex;
}

.season-picker input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.season-picker label {
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-3xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
  white-space: nowrap;
}

.season-picker label:hover { background: var(--sheet); }

.season-picker input:checked + label {
  background: var(--lime);
  color: var(--ink-on-lime);
  border-color: var(--lime);
}

.season-picker input:focus-visible + label {
  outline: 2px solid var(--lime-mark);
  outline-offset: 1px;
}

/* Gated the same way — only once JS is driving the picker above is it safe
   to hide the seasons it isn't pointed at. */
html.has-js .season-panel:not(.is-active) {
  display: none;
}

/* --- Season KPI cards and the all-seasons table (show page, Tasks 130, 132) --
   Four cards above an Episodes table — the whole run under "All seasons",
   one season otherwise. They are the film page's .facts record grid turned
   into one row: hairline cells on the rule colour, the same 16px plate, so
   they read as part of the record, not a dashboard bolted on. Figures are
   mono at a reading size, not hero numbers: the chart below is where the
   eye goes for shape, the cards are for the exact value. */

.season-readout {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px;
  margin: var(--space-4) 0 var(--space-6);
  border: var(--hairline);
  border-radius: 16px;
  overflow: hidden;
  background: var(--rule);
}

.season-readout__cell {
  display: grid;
  align-content: start;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-6);
  background: var(--paper);
  min-width: 0;
}

.season-readout dd {
  margin: 0;
  font-size: var(--text-base);
  line-height: 1.35;
  overflow-wrap: anywhere;
}

.season-readout dd a {
  color: var(--lime-text);
  font-weight: 600;
}

.season-readout__figure {
  font-size: var(--text-xl);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.season-readout__unit {
  margin-left: var(--space-2);
  font-size: var(--text-sm);
  color: var(--ink-faint);
}

.season-readout__name {
  display: block;
  font-weight: 600;
}

.season-readout__sub {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-2xs);
  color: var(--ink-faint);
}

/* A best/weakest season card is a link with its figure beside it, not
   under it: "Season 4  8.9" is one reading. */
.season-readout dd a + .season-readout__sub {
  display: inline;
  margin: 0 0 0 var(--space-3);
  font-size: var(--text-sm);
}

.season-readout__none {
  color: var(--ink-muted);
}

/* A quiet qualifier beside a figure ("few ratings"). Body face: it is a
   word, not a measurement. */
.season-note {
  margin-left: var(--space-3);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--text-2xs);
  color: var(--ink-faint);
  white-space: nowrap;
}

/* The all-seasons table is the episode table's twin; only the season name
   is a link (to that season's panel), set in ink like an episode title. */
.season-link {
  color: var(--ink);
  white-space: nowrap;
}

.season-link:hover {
  color: var(--lime-text);
}

.season-table td.mono {
  white-space: nowrap;
}

@media (max-width: 900px) {
  .season-readout {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .season-readout__cell {
    padding: var(--space-4) var(--space-5);
  }
}

/* --- Season rating chart (show page episodes, Tasks 130–132) ---------------
   An area chart under the table: one season's episodes, or — under the
   picker's "All seasons" option — each season's average. It reads as
   part of the catalog sheet, not a dashboard widget: a hairline rule opens
   it like any other section block, the heading is the same condensed label
   the season heading above uses, and the only colour is the lime that marks
   a measurement. The canvas itself is drawn by static/js/series_ratings.js
   from these same tokens. */

.season-chart {
  margin: var(--space-7) 0 0;
  padding-top: var(--space-6);
  border-top: var(--hairline);
}

/* Kept authoritative: the figure stays hidden until the script draws. */
.season-chart[hidden] {
  display: none;
}

.season-chart__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3) var(--space-6);
  margin-bottom: var(--space-5);
}

.season-chart__title {
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink);
}

.season-chart__key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-2xs);
  color: var(--ink-muted);
}

.season-chart__key li {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
}

/* Each key mark drawn the way the canvas draws it. */
.season-chart__key li::before {
  content: "";
  flex: none;
}

.season-chart__key .is-line::before {
  width: 18px;
  height: 10px;
  border-top: 2px solid var(--lime-mark);
  background: linear-gradient(color-mix(in srgb, var(--lime-mark) 30%, transparent), transparent);
}

.season-chart__key [data-ref-value] {
  margin-left: calc(var(--space-2) - var(--space-3));
  color: var(--ink);
}

.season-chart__key .is-ref::before {
  width: 18px;
  border-top: 1px dashed var(--ink-faint);
}

.season-chart__key .is-best::before,
.season-chart__key .is-low::before {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 2px solid var(--lime-mark);
  box-sizing: border-box;
}

.season-chart__key .is-best::before {
  background: var(--lime-mark);
}

.season-chart__key .is-low::before {
  background: var(--paper);
}

.season-chart:not(.has-extremes) .is-best,
.season-chart:not(.has-extremes) .is-low,
.season-chart[data-view="all"] [data-chart-view="season"],
.season-chart[data-view="season"] [data-chart-view="all"] {
  display: none;
}

.season-chart__stage {
  position: relative;
  height: 280px;
}

/* The hover card: the site's own sheet and hairline, mono figures. Absolutely
   placed over the canvas by the script; never takes pointer events, so it
   can't steal the hover that's positioning it. */
.season-chart__tip {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  min-width: 11rem;
  max-width: 15rem;
  padding: var(--space-4) var(--space-5);
  background: var(--sheet);
  border: 1px solid var(--rule-strong);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}

.season-chart__tip.is-on {
  opacity: 1;
}

.season-chart__tip-title {
  margin: 0;
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--ink);
}

.season-chart__tip-sub {
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

.season-chart__tip dl {
  display: grid;
  gap: var(--space-1);
  margin: var(--space-3) 0 0;
  padding-top: var(--space-3);
  border-top: var(--hairline);
}

.season-chart__tip dl div {
  display: flex;
  justify-content: space-between;
  gap: var(--space-5);
}

.season-chart__tip dt {
  font-size: var(--text-2xs);
  color: var(--ink-faint);
}

.season-chart__tip dd {
  margin: 0;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--text-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

@media (max-width: 900px) {
  .season-chart__stage {
    height: 220px;
  }
}

/* --- Pagination ------------------------------------------------------------- */

.pagination {
  display: flex;
  gap: var(--space-5);
  align-items: center;
  justify-content: center;
  margin-top: var(--space-8);
  padding-top: var(--space-5);
  border-top: var(--hairline);
}

.pagination .page-state {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--ink-faint);
}

.pagination a {
  font-family: var(--font-label);
  font-weight: 700;
  font-stretch: 78%;
  font-size: var(--text-3xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink);
  background: var(--sheet-2);
  border: none;
  border-radius: var(--radius-pill);
  padding: var(--space-3) var(--space-5);
  transition: background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.pagination a:hover {
  background: var(--lime-wash);
  color: var(--lime-deep);
  text-decoration: none;
}

/* Client-side pager (movie page cast/crew). Buttons, not links: paging here
   changes nothing about the document's address. Styled to match .pagination a
   exactly — adding a variant to the component rather than restyling it, per
   the stylesheet contract at the top of this file. */
.pagination.is-client .page-btn {
  font-family: var(--font-label);
  font-weight: 700;
  font-stretch: 78%;
  font-size: var(--text-3xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink);
  background: var(--sheet-2);
  border: none;
  border-radius: var(--radius-pill);
  padding: var(--space-3) var(--space-5);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              opacity var(--dur-fast) var(--ease);
}

.pagination.is-client .page-btn:hover:not(:disabled) {
  background: var(--lime-wash);
  color: var(--lime-deep);
}

/* Kept in the layout at the ends of the range so the row doesn't reflow and
   the "3 / 8" readout doesn't jump sideways on the first and last page. */
.pagination.is-client .page-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

/* The JS pager toggles the `hidden` attribute on grid and flex children,
   whose own display rule would otherwise win over the UA sheet's [hidden]. */
[hidden] {
  display: none !important;
}

/* --- Specimen (movie detail) -----------------------------------------------
   A catalog record: framed plate + captioned facts. Deliberately NOT a
   full-bleed darkened backdrop with text on top — that's the streaming-app
   default and it makes every film look the same. The backdrop still appears,
   as an honest strip below the record. */

.specimen {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: var(--space-8);
  align-items: start;
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: var(--space-7);
}

.specimen-plate .poster,
.specimen-plate .placeholder {
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  background: var(--sheet);
  border: var(--hairline);
}

.specimen-plate .placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--rule-strong);
  font-size: var(--text-2xl);
}

.specimen-plate .plate-caption {
  margin-top: var(--space-3);
  font-size: var(--text-3xs);
  /* Bold ink, not the muted medium weight the card sub-lines use -- same
     "font only" weight bump the filled-pill pass gave the record-row values
     and the sheet sub-line, kept to the caption itself, no pill. */
  font-weight: 700;
  color: var(--ink);
}

/* Movie and series pages both: the three actions sit right under the poster
   and year rather than on top of the genre chips. Content-width pills
   packed snugly next to each other -- not the divider treatment a plain
   below-record-list placement would use, and not stretched to the plate's
   width either. Scoped to this ancestor, not a modifier class, because the
   include only ever lands inside .specimen-plate now -- nothing to keep in
   sync by hand. */
.specimen-plate .collection-actions {
  gap: var(--space-2);
  margin-top: var(--space-4);
  padding-top: 0;
  border-top: none;
}

/* A step up from the shared 2rem/text-3xs sizing -- this is the one place on
   the page these controls are the only thing in their row, so they can carry
   more visual weight than the compact pills sitting inline with prose
   elsewhere (series' record-list placement, the collections list). 40px,
   matching a standalone filled action pill rather than an inline chip. The
   row above keeps flex-wrap: wrap from the base rule, so at the plate's
   narrower breakpoints (180px) the third pill drops to its own line instead
   of overflowing or forcing the plate wider. */
.specimen-plate .collection-action {
  min-height: 2.5rem;
  padding-inline: var(--space-5);
  font-size: var(--text-2xs);
}

.specimen-plate .collection-action__icon {
  width: 1.1rem;
  height: 1.1rem;
}

.specimen-tagline {
  margin-top: var(--space-4);
  font-size: var(--text-md);
  color: var(--ink-muted);
  max-width: 54ch;
  text-wrap: balance;
}

.specimen-synopsis {
  margin-top: var(--space-4);
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--ink);
  max-width: 62ch;
}

/* --- Expandable person bio -----------------------------------------------
   TMDB biographies run to several paragraphs; left full they push the
   filmography far down the page. Clamp to six lines with a See more / See
   less toggle. Progressive enhancement, the same shape as the client pager
   and the mobile nav: the clamp is gated on html.has-js, and .bio-toggle
   ships `hidden` — initBioToggle() unhides it only when the text overflows
   the clamp, so with JS off or on a short bio the full text shows with no
   dead control. .bio-body is an additive class on .specimen-synopsis; the
   shared rule is untouched (stylesheet contract at the top of this file). */
html.has-js .bio-body.is-collapsed {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 6;
  overflow: hidden;
}

/* An outlined pill matching .pagination.is-client .page-btn — a control that
   changes nothing about the document's address, so a button not a link. */
.bio-toggle {
  margin-top: var(--space-4);
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-3xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink);
  background: none;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-pill);
  padding: var(--space-3) var(--space-5);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}

.bio-toggle:hover {
  background: var(--lime-wash);
  border-color: var(--lime-mark);
  color: var(--ink);
}

.specimen-original-title {
  margin-top: var(--space-1);
  font-size: var(--text-xs);
  color: var(--ink-faint);
}

/* Where a translated page shows English prose (an Uzbek overview, a Russian
   film TMDB never translated): one quiet line under the text, in the same
   register as the "originally ..." note above, so it reads as a caption to
   the paragraph rather than as a warning. */
.prose-note {
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-faint);
}

.record-list {
  margin: var(--space-6) 0 0;
  border-top: var(--hairline);
}

/* An outbound link leaves the site — a small arrow marks that visibly,
   rather than relying on target="_blank" alone (invisible to a reader). */
.ext-link::after {
  content: " ↗";
}

.record-row {
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: var(--space-5);
  padding: var(--space-3) 0;
  border-bottom: var(--hairline);
}

.record-row dt {
  padding-top: 2px;
}

.record-row dd {
  margin: 0;
  font-size: var(--text-sm);
}

/* Every value in the record list reads at one weight, whatever face it uses:
   500, which the numeric rows (.mono) already had and is the heaviest weight
   Spline Sans Mono is actually loaded at, so mono and prose rows match with no
   synthesised bold. The labels, title, tagline and synopsis are untouched —
   only the fact values. The rating figure drops from 600 to sit in line. */
.record-list dd,
.record-list dd.mono,
.record-list .rating-badge-figure {
  font-weight: 500;
}

/* The metric *names* (Rating, Released, Runtime, ...) go bold ink -- the
   "font only, no pill" treatment, on the label rather than the value this
   time. Scoped to .record-row so the shared .label rule (table headers,
   chips, stat labels elsewhere) is untouched. --font-label is Archivo,
   loaded 400-700, so a real 700 renders here with no synthesis. */
.record-row .label {
  font-weight: 700;
  color: var(--ink);
}

.specimen .chips {
  margin-top: var(--space-6);
}

/* The backdrop as a measured strip — cropped wide, hairline-framed. */
.backdrop-strip {
  margin: var(--space-7) 0 0;
  border: var(--hairline);
  overflow: hidden;
}

.backdrop-strip img {
  width: 100%;
  aspect-ratio: 32 / 9;
  object-fit: cover;
  object-position: center 30%;
}

/* --- Video embed (trailer) ---------------------------------------------
   A trailer sits where the backdrop strip would — same hairline frame and
   top margin — but 16:9, and click-to-play. Additive: nothing above is
   restyled (the Task 38 stylesheet contract). */

.video-strip {
  margin: var(--space-7) 0 0;
}

.video-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  border: var(--hairline);
  overflow: hidden;
  background: var(--sheet);
}

.video-play {
  position: absolute;
  inset: 0;
  display: block;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.video-play img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Lime disc + ink triangle, centred. --lime is a fill here (on a photo, with
   an --ink-on-lime glyph), never a mark on its own. */
.video-play-icon {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 56px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  background: var(--lime);
  color: var(--ink-on-lime);
  transition: background var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}

.video-play-icon svg {
  width: 26px;
  height: 26px;
  margin-left: 3px; /* optical-centre the triangle in the disc */
}

.video-play:hover .video-play-icon,
.video-play:focus-visible .video-play-icon {
  background: var(--lime-hover);
  transform: translate(-50%, -50%) scale(1.06);
}

.video-iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

@media (max-width: 860px) {
  .specimen {
    grid-template-columns: 180px 1fr;
    gap: var(--space-6);
  }
}

@media (max-width: 620px) {
  .specimen {
    grid-template-columns: 1fr;
  }

  .specimen-plate {
    max-width: 180px;
  }

  .record-row {
    grid-template-columns: 1fr;
    gap: var(--space-1);
  }
}

/* --- Person record header --------------------------------------------------
   A headshot plate beside the record. Shared by actor_detail and
   director_detail, which used to be duplicated templates. */

.person-head {
  display: grid;
  grid-template-columns: 148px 1fr;
  gap: var(--space-7);
  align-items: start;
  padding-bottom: var(--space-6);
}

.person-plate .headshot,
.person-plate .placeholder {
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  background: var(--sheet);
  border: var(--hairline);
}

.person-plate .placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--rule-strong);
  font-size: var(--text-xl);
}

.person-record .stats {
  margin-top: var(--space-6);
}

@media (max-width: 720px) {
  .person-head {
    grid-template-columns: 104px 1fr;
    gap: var(--space-5);
  }
}

/* --- Studio record header ------------------------------------------------
   The logo plate on the left, the record to its right — the same shape as
   .person-head. The plate is landscape: a studio's mark is a wordmark, framed
   the padded, contained way the /studios/ grid cards frame it (.poster-card
   .logo), not the 2:3 portrait crop the poster and headshot plates use, which
   would clip letters. The plate stays on the fixed --logo-plate: a light
   backing that is never redefined under [data-theme], so a dark-on-transparent
   wordmark — most of what TMDB serves — keeps its contrast when the site is in
   dark mode. The initial-letter monogram fallback (~46% of studios have no
   logo) shares that surface. */

.studio-head {
  display: grid;
  grid-template-columns: minmax(0, 224px) 1fr;
  gap: var(--space-8);
  align-items: start;
  padding-bottom: var(--space-6);
}

.studio-plate .logo,
.studio-plate .placeholder-studio {
  width: 100%;
  aspect-ratio: 3 / 2;
  background: var(--logo-plate);
  border: var(--hairline);
}

.studio-plate .logo {
  object-fit: contain;
  object-position: center;
  padding: var(--space-5);
}

.studio-plate .placeholder-studio {
  display: flex;
  align-items: center;
  justify-content: center;
}

.studio-record .stats {
  margin-top: var(--space-6);
}

@media (max-width: 720px) {
  .studio-head {
    grid-template-columns: 1fr;
    gap: var(--space-5);
  }

  .studio-plate {
    max-width: 176px;
  }
}

/* --- Scope switch ----------------------------------------------------------
   Sibling of .segmented, but each option is its own URL rather than a radio,
   so it's links. Matches .segmented's condensed label treatment; the current
   scope carries the lime because it's the one being measured. */

.scope {
  display: flex;
  gap: var(--space-4);
  align-items: baseline;
}

.scope a {
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 2px solid transparent;
}

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

.scope a[aria-current="true"] {
  color: var(--ink);
  border-bottom-color: var(--lime);
}

/* Wraps .scope so the People page's live filtering (static/js/theoria.js)
   has a single element to target and swap — but the wrapper itself must not
   become a flex item of .toolbar in its own right, or .scope stops lining up
   the way it did as a direct sibling of the form. display: contents keeps it
   purely addressable. */
#people-toolbar-meta {
  display: contents;
}

/* --- Credit list (movie detail, crew) ---------------------------------------
   Billed/full crew on the movie page. 1px gap on a --rule background with
   --paper rows draws hairline dividers with no border-bottom rules to fight
   in dark mode. A list, not poster cards, because crew profile-photo
   coverage (23.8%) is a fraction of cast's (70.1%) — three of four crew
   cards would be placeholder silhouettes. */

.credit-department {
  font-family: var(--font-label);
  font-weight: 600;
  font-stretch: 78%;
  font-size: var(--text-3xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-faint);
  margin: var(--space-6) 0 var(--space-2);
}

.credit-list {
  list-style: none;
  display: grid;
  gap: 1px;
  background: var(--rule);
  border-block: var(--hairline);
  /* A <ul> carries a UA default padding-inline-start of 40px. These lists
     paint their own background to draw the 1px gaps as hairlines, so that
     padding rendered as an unexplained grey column down the left of every
     crew list. Zero it explicitly. */
  padding: 0;
  margin: 0 0 var(--space-6);
}

.credit-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  background: var(--paper);
}

/* Name plus face, as one link target. */
.credit-person {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  text-decoration: none;
  color: inherit;
}

.credit-avatar {
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--radius);
  object-fit: cover;
  border: 1px solid var(--rule);
  transition: border-color var(--dur-fast) var(--ease);
}

/* The ~76% of crew with no headshot get the same person silhouette the cast
   cards use — one placeholder vocabulary across the page, rather than a
   second invention that has to be learned. .placeholder-person supplies the
   gradient and .person-icon its 42% sizing, both already defined above; this
   only has to centre the icon in a square instead of a 2:3 plate. */
.credit-list .placeholder-person {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The shared 42% is tuned to a 116px-wide poster plate; in a 36px square it
   leaves a 15px figure swimming in space. Scaled up so the silhouette reads
   at this size. */
.credit-list .placeholder-person .person-icon {
  width: 58%;
  height: 58%;
}

.credit-person:hover .credit-avatar,
.credit-person:focus-visible .credit-avatar {
  border-color: var(--lime-mark);
}

.credit-name {
  font-weight: 500;
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.credit-row:hover .credit-name,
.credit-person:focus-visible .credit-name {
  border-bottom-color: var(--lime-mark);
}

/* The crew job(s) a person held — same personnel face as a cast card's role,
   so directing, writing and camera all read as craft credits rather than
   monospaced field values. See --font-role. */
.credit-jobs {
  font-family: var(--font-role);
  font-size: var(--text-2xs);
  color: var(--ink-faint);
  text-align: right;
}

/* --- Empty states ----------------------------------------------------------
   An empty screen is an invitation to act — say what to do, not "no data". */

.empty {
  border: 1px dashed var(--rule-strong);
  border-radius: var(--radius);
  padding: var(--space-7);
  color: var(--ink-muted);
  font-size: var(--text-sm);
  text-align: center;
  /* Empty states are usually a {% empty %} clause inside a grid, so without
     this they'd be sized to a single poster column. */
  grid-column: 1 / -1;
}

/* --- Notices ---------------------------------------------------------------
   django.contrib.messages, rendered once here in base.html so every flow
   that queues one (the accounts app, so far) gets it for free. One quiet
   style for every level, in the same --ink on --sheet-2 vocabulary the auth
   pages' own form errors use: a message is information, not a measurement,
   so it never reaches for lime or an invented red. */

.notice-list {
  list-style: none;
  margin: 0 0 var(--space-7);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.notice {
  color: var(--ink);
  background: var(--sheet-2);
  border-left: 2px solid var(--rule-strong);
  padding: var(--space-4) var(--space-5);
  font-size: var(--text-sm);
}

/* --- Error pages ---------------------------------------------------------------
   The 404 template. Centred, narrow, and quiet — one message and one way back. */

.error-page {
  max-width: 32rem;
  margin: var(--space-10) auto;
  text-align: center;
}

.error-code {
  font-family: var(--font-label);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--lime-text);
  margin: 0 0 var(--space-4);
}

.error-title {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  margin: 0 0 var(--space-4);
}

.error-lead {
  color: var(--ink-muted);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-7);
}

/* --- Film guide ------------------------------------------------------------ */
/* A chat that hands back films, so it borrows the site's own vocabulary
   instead of a support-widget look: the hero's heavy uppercase Archivo for the
   greeting, mono for the numbers on each pick, and posters doing the visual
   work. Shapes follow the site rule: sheets and plates are square-cornered
   (--radius), anything you press to act is a pill or a circle. Colours are
   tokens only, so the dark counterpart needs no rules of its own here. */

.ai-assistant {
  position: fixed;
  right: var(--space-6);
  bottom: var(--space-6);
  z-index: var(--z-assistant);
  --ai-header-height: 64px;
}

.ai-assistant-trigger,
.ai-assistant-close,
.ai-assistant-new,
.ai-assistant-history-toggle,
.ai-assistant-resize,
.ai-assistant-send {
  display: inline-grid;
  place-items: center;
  border: 0;
  cursor: pointer;
}

.ai-assistant-trigger {
  position: relative;
  width: 58px;
  height: 58px;
  padding: 0;
  border-radius: 50%;
  color: #f2f1ed;
  background: #0b0b0b;
  box-shadow: 0 8px 24px rgba(11, 11, 11, 0.24);
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}

.ai-assistant-trigger:hover,
.ai-assistant-trigger:focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 12px 28px rgba(11, 11, 11, 0.3);
}

.ai-assistant-trigger:active {
  transform: translateY(0) scale(0.96);
}

.ai-assistant-trigger svg {
  width: 27px;
  height: 33px;
}

.ai-assistant-trigger::after {
  position: absolute;
  right: calc(100% + var(--space-3));
  top: 50%;
  width: max-content;
  max-width: 180px;
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-label);
  font-size: var(--text-3xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--paper);
  background: var(--ink);
  border-radius: var(--radius);
  content: attr(data-label);
  opacity: 0;
  pointer-events: none;
  transform: translateY(-50%) translateX(4px);
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}

.ai-assistant-trigger:hover::after,
.ai-assistant-trigger:focus-visible::after {
  opacity: 1;
  transform: translateY(-50%);
}

/* The label only explains a closed button; once the panel is open the
   button's job is obvious and the tooltip would sit on top of the panel. */
.ai-assistant-trigger[aria-expanded="true"]::after {
  display: none;
}

/* --- Welcome nudge --- */

.ai-assistant-welcome {
  position: absolute;
  right: 0;
  bottom: 70px;
  width: min(320px, calc(100vw - 2 * var(--space-6)));
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  animation: ai-rise var(--dur-slow) var(--ease-bloom) both;
}

.ai-assistant-welcome[hidden] {
  display: none;
}

.ai-assistant-welcome::after {
  position: absolute;
  right: 23px;
  bottom: -7px;
  width: 12px;
  height: 12px;
  background: var(--paper);
  border-right: 1px solid var(--rule-strong);
  border-bottom: 1px solid var(--rule-strong);
  content: "";
  transform: rotate(45deg);
}

.ai-assistant-welcome-open {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr);
  gap: var(--space-4);
  width: 100%;
  padding: var(--space-5) var(--space-7) var(--space-5) var(--space-5);
  color: inherit;
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 400;
  font-stretch: normal;
  letter-spacing: normal;
  text-align: left;
  text-transform: none;
  background: transparent;
  border: 0;
  border-radius: 0;
  cursor: pointer;
  transition: none;
}

.ai-assistant-welcome-open:hover,
.ai-assistant-welcome-open:active {
  color: var(--ink);
  background: transparent;
  border-color: transparent;
  text-decoration: none;
}

.ai-assistant-welcome-open:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--lime-mark);
}

.ai-assistant-welcome-mark {
  display: inline-grid;
  place-items: center;
  width: 38px;
  height: 38px;
  color: #f2f1ed;
  background: #0b0b0b;
  border-radius: 50%;
}

.ai-assistant-welcome-mark svg {
  width: 17px;
  height: 21px;
}

.ai-assistant-welcome-copy {
  display: grid;
  gap: var(--space-2);
}

.ai-assistant-welcome-copy strong {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: 1.15;
}

.ai-assistant-welcome-copy span {
  color: var(--ink-muted);
  font-size: var(--text-sm);
  line-height: 1.45;
}

.ai-assistant-welcome-close {
  position: absolute;
  z-index: 2;
  top: var(--space-2);
  right: var(--space-2);
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  padding: 0;
  color: var(--ink-faint);
  background: transparent;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
}

.ai-assistant-welcome-close:hover,
.ai-assistant-welcome-close:focus-visible {
  color: var(--ink);
  background: var(--sheet);
  outline: none;
}

.ai-assistant-welcome-close:focus-visible {
  box-shadow: 0 0 0 2px var(--lime-mark);
}

.ai-assistant-welcome-close svg {
  width: 15px;
  height: 15px;
}

/* --- Panel --- */

.ai-assistant-panel {
  position: absolute;
  right: 0;
  bottom: 70px;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  grid-template-columns: minmax(0, 1fr);
  width: min(400px, calc(100vw - 2 * var(--space-6)));
  height: min(600px, calc(100vh - 112px));
  overflow: hidden;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card), 0 28px 64px -24px rgba(11, 11, 11, 0.28);
  transform-origin: bottom right;
  animation: ai-panel-in var(--dur) var(--ease-bloom) both;
  transition: width var(--dur) var(--ease), height var(--dur) var(--ease),
              opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}

/* The brand's lime band, drawn once across the top edge of the sheet. */
.ai-assistant-panel::before {
  position: absolute;
  inset: 0 0 auto;
  z-index: 3;
  height: 3px;
  background: var(--lime);
  content: "";
}

.ai-assistant-panel.is-expanded {
  width: min(680px, calc(100vw - 2 * var(--space-6)));
  height: min(760px, calc(100vh - 112px));
}

.ai-assistant-panel.is-closing {
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px) scale(0.98);
}

@keyframes ai-panel-in {
  from {
    opacity: 0;
    transform: translateY(10px) scale(0.97);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes ai-rise {
  from {
    opacity: 0;
    transform: translateY(8px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.ai-assistant-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  height: var(--ai-header-height);
  padding: 3px var(--space-4) 0 var(--space-5);
  border-bottom: var(--hairline);
}

.ai-assistant-identity {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-width: 0;
}

.ai-assistant-identity > div {
  min-width: 0;
}

.ai-assistant-badge {
  display: inline-grid;
  flex: none;
  place-items: center;
  width: 34px;
  height: 34px;
  color: #f2f1ed;
  background: #0b0b0b;
  border-radius: 50%;
}

/* On the dark surface an ink disc would vanish into the header, so it gets
   a hairline ring to keep its edge. */
[data-theme="dark"] .ai-assistant-badge,
[data-theme="dark"] .ai-assistant-welcome-mark {
  box-shadow: 0 0 0 1px var(--rule-strong);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .ai-assistant-badge,
  :root:not([data-theme]) .ai-assistant-welcome-mark {
    box-shadow: 0 0 0 1px var(--rule-strong);
  }
}

.ai-assistant-badge svg {
  width: 15px;
  height: 19px;
}

.ai-assistant-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: 1.2;
}

.ai-assistant-subtitle {
  margin: 1px 0 0;
  overflow: hidden;
  color: var(--ink-faint);
  font-size: var(--text-2xs);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ai-assistant-controls {
  display: flex;
  flex: none;
  gap: 2px;
}

.ai-assistant-close,
.ai-assistant-new,
.ai-assistant-history-toggle,
.ai-assistant-resize {
  width: 34px;
  height: 34px;
  padding: 0;
  color: var(--ink-muted);
  background: transparent;
  border-radius: 50%;
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

.ai-assistant-close:hover,
.ai-assistant-close:focus-visible,
.ai-assistant-new:hover,
.ai-assistant-new:focus-visible,
.ai-assistant-history-toggle:hover,
.ai-assistant-history-toggle:focus-visible,
.ai-assistant-history-toggle[aria-expanded="true"],
.ai-assistant-resize:hover,
.ai-assistant-resize:focus-visible {
  color: var(--ink);
  background: var(--sheet);
  border-color: transparent;
}

.ai-assistant-controls button:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--lime-mark);
}

.ai-assistant-close svg,
.ai-assistant-new svg,
.ai-assistant-history-toggle svg,
.ai-assistant-resize svg {
  width: 18px;
  height: 18px;
}

.ai-assistant-shrink-icon {
  display: none;
}

.ai-assistant-panel.is-expanded .ai-assistant-expand-icon {
  display: none;
}

.ai-assistant-panel.is-expanded .ai-assistant-shrink-icon {
  display: inline;
}

/* --- Recent chats drawer --- */

.ai-assistant-history {
  position: absolute;
  z-index: 2;
  inset: var(--ai-header-height) 0 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: var(--space-5);
  background: var(--paper);
  animation: ai-rise var(--dur) var(--ease-bloom) both;
}

.ai-assistant-history[hidden] {
  display: none;
}

.ai-assistant-history-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-bottom: var(--space-4);
  border-bottom: var(--hairline);
}

.ai-assistant-history-header h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
}

.ai-assistant-history-close {
  display: inline-grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  color: var(--ink-muted);
  background: transparent;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
}

.ai-assistant-history-close:hover,
.ai-assistant-history-close:focus-visible {
  color: var(--ink);
  background: var(--sheet);
}

.ai-assistant-history-close svg {
  width: 17px;
  height: 17px;
}

.ai-assistant-history-list {
  display: grid;
  align-content: start;
  gap: var(--space-2);
  padding-top: var(--space-4);
  overflow-y: auto;
}

.ai-assistant-history-item {
  width: 100%;
  padding: var(--space-4) var(--space-5);
  overflow: hidden;
  color: var(--ink);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  text-align: left;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: var(--paper);
  border: var(--hairline);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

.ai-assistant-history-item:hover,
.ai-assistant-history-item:focus-visible {
  background: var(--sheet);
  border-color: var(--rule-strong);
}

.ai-assistant-history-item.is-current {
  background: var(--lime-wash);
  border-color: var(--lime-mark);
  box-shadow: inset 3px 0 0 var(--lime-mark);
}

.ai-assistant-history-empty,
.ai-assistant-history-error {
  margin: 0;
  color: var(--ink-muted);
  font-size: var(--text-sm);
  line-height: 1.45;
}

/* --- Conversation --- */

.ai-assistant-messages {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-height: 0;
  padding: var(--space-6) var(--space-5) var(--space-6);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--rule-strong) transparent;
}

/* Opening state: the hero's voice, scaled to fit the sheet. */
.ai-assistant-intro {
  padding-top: var(--space-3);
}

.ai-assistant-greeting {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.625rem, 1.2rem + 1.4vw, 2rem);
  font-weight: 800;
  line-height: 0.98;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  text-wrap: balance;
}

.ai-assistant-lede {
  max-width: 34ch;
  margin: var(--space-4) 0 0;
  color: var(--ink-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.ai-assistant-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}

.ai-assistant-panel.is-expanded .ai-assistant-actions {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.ai-assistant-actions button {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-6);
  min-height: 92px;
  padding: var(--space-4);
  color: var(--ink);
  font-family: var(--font-label);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: normal;
  line-height: 1.2;
  text-align: left;
  text-transform: none;
  background: var(--sheet);
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  animation: ai-rise var(--dur-slow) var(--ease-bloom) both;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}

.ai-assistant-actions button:nth-child(2) { animation-delay: 40ms; }
.ai-assistant-actions button:nth-child(3) { animation-delay: 80ms; }
.ai-assistant-actions button:nth-child(4) { animation-delay: 120ms; }

.ai-assistant-actions svg {
  width: 22px;
  height: 22px;
  color: var(--lime-text);
  transition: transform var(--dur) var(--ease-bloom);
}

.ai-assistant-actions button:hover,
.ai-assistant-actions button:focus-visible {
  color: var(--ink);
  background: var(--lime-wash);
  border-color: var(--lime-mark);
  outline: none;
}

.ai-assistant-actions button:hover svg,
.ai-assistant-actions button:focus-visible svg {
  transform: translateY(-2px) rotate(-6deg);
}

.ai-assistant-actions button:active {
  transform: scale(0.98);
}

.ai-message {
  max-width: 88%;
  font-size: var(--text-sm);
  line-height: 1.55;
  animation: ai-rise var(--dur) var(--ease-bloom) both;
}

.ai-message p {
  margin: 0;
  overflow-wrap: anywhere;
}

.ai-message strong {
  font-weight: 600;
}

/* The guide talks in plain type on the page; only the reader's own lines
   get a block, so the eye can find "what did I ask" at a glance. */
.ai-message-assistant {
  align-self: flex-start;
  max-width: 100%;
  color: var(--ink);
}

.ai-message-user {
  align-self: flex-end;
  padding: var(--space-3) var(--space-4);
  color: var(--paper);
  background: var(--ink);
  border-radius: var(--radius);
}

.ai-message-thinking {
  color: var(--ink-muted);
}

.ai-message-thinking p::after {
  display: block;
  width: 112px;
  height: 2px;
  margin-top: var(--space-3);
  background: linear-gradient(90deg, var(--rule) 0%, var(--lime-mark) 50%, var(--rule) 100%);
  background-size: 300% 100%;
  content: "";
  animation: ai-sweep 1.3s linear infinite;
}

@keyframes ai-sweep {
  from { background-position: 100% 0; }
  to { background-position: -50% 0; }
}

.ai-assistant-actions button:disabled,
.ai-recommendation-feedback button:disabled {
  cursor: wait;
  opacity: 0.55;
}

/* --- Picks --- */

.ai-picks {
  display: grid;
  gap: var(--space-3);
  margin-top: calc(-1 * var(--space-2));
}

.ai-assistant-panel.is-expanded .ai-picks {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.ai-recommendation {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: var(--space-4);
  padding: var(--space-3);
  background: var(--paper);
  border: var(--hairline);
  border-radius: var(--radius);
  animation: ai-rise var(--dur-slow) var(--ease-bloom) both;
  animation-delay: calc(var(--pick-index, 0) * 70ms);
  transition: border-color var(--dur-fast) var(--ease);
}

.ai-recommendation:hover {
  border-color: var(--rule-strong);
}

.ai-recommendation-poster {
  display: block;
  width: 64px;
  aspect-ratio: 2 / 3;
  overflow: hidden;
  background: var(--sheet);
  border-radius: var(--radius);
}

.ai-recommendation-poster img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-bloom);
}

a.ai-recommendation-poster:hover img,
a.ai-recommendation-poster:focus-visible img {
  transform: scale(1.05);
}

.ai-recommendation-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding-top: 2px;
}

.ai-recommendation-title {
  display: block;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: 1.2;
  text-decoration: none;
}

a.ai-recommendation-title:hover,
a.ai-recommendation-title:focus-visible {
  color: var(--lime-text);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.ai-recommendation-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin: var(--space-2) 0 0;
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: var(--text-3xs);
}

.ai-recommendation-rating {
  color: var(--ink);
  font-weight: 600;
}

.ai-recommendation-status {
  padding: 1px var(--space-2);
  color: var(--ink-on-lime);
  font-family: var(--font-label);
  font-size: var(--text-3xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  background: var(--lime);
  border-radius: var(--radius-pill);
}

.ai-recommendation-reason {
  margin: var(--space-3) 0 0;
  color: var(--ink-muted);
  font-size: var(--text-2xs);
  line-height: 1.45;
}

.ai-recommendation-feedback {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: var(--space-4);
  color: var(--lime-text);
  font-size: var(--text-2xs);
  font-weight: 500;
}

.ai-recommendation-feedback button {
  padding: var(--space-1) var(--space-4);
  color: var(--ink-muted);
  font-family: var(--font-body);
  font-size: var(--text-2xs);
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  background: transparent;
  border: var(--hairline);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}

.ai-recommendation-feedback button:hover,
.ai-recommendation-feedback button:focus-visible {
  color: var(--ink);
  background: var(--sheet);
  border-color: var(--rule-strong);
}

/* --- Composer --- */

.ai-assistant-form {
  padding: var(--space-4) var(--space-4) var(--space-4);
  background: var(--paper);
  border-top: var(--hairline);
}

.ai-assistant-field {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 38px;
  align-items: center;
  gap: var(--space-2);
  padding: 4px 4px 4px var(--space-5);
  background: var(--sheet);
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}

.ai-assistant-field:focus-within {
  background: var(--paper);
  border-color: var(--lime-mark);
  box-shadow: 0 0 0 3px var(--lime-wash);
}

.ai-assistant-field input {
  width: 100%;
  min-width: 0;
  min-height: 38px;
  padding: 0;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  outline: none;
}

.ai-assistant-field input:focus {
  border: 0;
  box-shadow: none;
}

.ai-assistant-field input::placeholder {
  color: var(--ink-faint);
}

.ai-assistant-send {
  width: 38px;
  height: 38px;
  padding: 0;
  color: var(--ink-faint);
  background: var(--rule);
  border-radius: 50%;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}

.ai-assistant-send svg {
  width: 18px;
  height: 18px;
}

.ai-assistant-send:not(:disabled) {
  color: var(--ink-on-lime);
  background: var(--lime);
}

.ai-assistant-send:not(:disabled):hover,
.ai-assistant-send:not(:disabled):focus-visible {
  background: var(--lime-hover);
  outline: none;
}

.ai-assistant-send:not(:disabled):active {
  transform: scale(0.92);
}

.ai-assistant-send:disabled {
  cursor: not-allowed;
}

@media (prefers-reduced-motion: reduce) {
  .ai-assistant-panel,
  .ai-assistant-welcome,
  .ai-assistant-history,
  .ai-assistant-actions button,
  .ai-message,
  .ai-recommendation,
  .ai-message-thinking p::after {
    animation: none;
  }
}

/* --- Cinema layer (redesign) --------------------------------------------------
   The pieces every page shares since the cinema redesign: the room light,
   the film grain, the search palette, the hero buttons and page-to-page
   transitions. Page stylesheets (home.css, film.css) build on these.

   Lime keeps its job as the measurement mark. The new colour on the page is
   --glow, which is never a token pick: cinema.js samples it from the film on
   screen, so the room takes on the colour of what is showing. */

:root {
  --glow: 120 110 90;   /* "r g b", rewritten per film by cinema.js */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

/* The room light: two soft pools of the film's colour, drifting slowly.
   Fixed behind everything; main and footer sit above it. */
.ambient {
  position: fixed;
  inset: -20vh -20vw auto;
  height: 120vh;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(50% 45% at 30% 30%, rgb(var(--glow) / 0.26), transparent 70%),
    radial-gradient(40% 40% at 75% 20%, rgb(var(--glow) / 0.15), transparent 70%);
  filter: blur(40px);
  transition: background 1.6s var(--ease);
  animation: ambient-drift 18s ease-in-out infinite alternate;
}

/* On light paper the same light reads as a stain, so it is kept faint. */
[data-theme="light"] .ambient { opacity: 0.45; }

@keyframes ambient-drift {
  to { transform: translate3d(4vw, 6vh, 0) scale(1.08); }
}

/* Film grain: a tiled noise texture jittered a few frames a second. */
.grain {
  position: fixed;
  inset: -50%;
  z-index: 300;
  pointer-events: none;
  opacity: 0.07;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grain-shift 0.9s steps(6) infinite;
}

[data-theme="light"] .grain { opacity: 0.04; }

@keyframes grain-shift {
  0%   { transform: translate(0, 0); }
  20%  { transform: translate(-3%, 2%); }
  40%  { transform: translate(2%, -4%); }
  60%  { transform: translate(-4%, -1%); }
  80%  { transform: translate(3%, 3%); }
  100% { transform: translate(0, 0); }
}

main,
.site-footer {
  position: relative;
  z-index: 1;
}

/* Eyebrow: a short label above a heading, led by a lit lime point. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-3xs);
  line-height: 1;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.eyebrow::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--lime);
  box-shadow: 0 0 12px var(--lime);
}

/* Hero buttons: larger than .btn, with a light sweep across the primary. */
.cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: 14px 22px;
  border-radius: var(--radius-pill);
  overflow: hidden;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-2xs);
  line-height: 1;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  background: var(--lime);
  color: var(--ink-on-lime);
  border: 0;
  cursor: pointer;
  transition: transform var(--dur-slow) var(--ease-out), background var(--dur) var(--ease);
}

.cta:hover {
  background: var(--lime-hover);
  color: var(--ink-on-lime);
  text-decoration: none;
}

.cta:active { transform: scale(0.97); }

.cta::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-120%);
  background: linear-gradient(100deg, transparent 30%, rgb(255 255 255 / 0.55), transparent 70%);
}

.cta:hover::after {
  transform: translateX(120%);
  transition: transform 0.8s var(--ease);
}

.cta svg {
  width: 14px;
  height: 14px;
  transition: transform var(--dur) var(--ease-out);
}

.cta:hover svg { transform: translateX(3px); }

.cta--ghost {
  background: color-mix(in srgb, var(--ink) 5%, transparent);
  color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

.cta--ghost:hover {
  background: color-mix(in srgb, var(--ink) 10%, transparent);
  border-color: var(--ink-muted);
  color: var(--ink);
}

.cta--ghost::after { display: none; }

/* Round icon button (rail arrows). */
.round-btn {
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--rule-strong);
  background: none;
  color: var(--ink);
  cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.round-btn:hover {
  background: var(--sheet-2);
  border-color: var(--ink-muted);
  color: var(--ink);
}

.round-btn svg { width: 16px; height: 16px; }

/* --- Header search -----------------------------------------------------------
   Wide: the field sits in the bar and its matches drop down beneath it.
   Narrow (<= 1240px, where the bar has no room for a field): the magnifier
   button stands in and opens the field as a row under the bar. Without JS
   there is no button and the field simply stays in the bar. */

.search-btn {
  display: none;
  align-items: center;
  justify-content: center;
  padding: 9px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--rule-strong);
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.search-btn:hover,
.search-btn[aria-expanded="true"] {
  color: var(--ink);
  border-color: var(--ink-faint);
}

.search-btn svg { width: 15px; height: 15px; flex: none; }

/* The bar's width is fixed and a longer language (Russian) leaves little of
   it spare, so the field takes whatever room is left, between a floor that
   still fits "Search" and a ceiling. Its basis is 0 so the row's wrapping
   sizes it at the floor and only then lets it grow. */
.header-search {
  position: relative;
  flex: 1 1 0;
  min-width: 96px;
  max-width: 232px;
}

.header-search__field {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: 0 var(--space-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--rule-strong);
  color: var(--ink-muted);
  cursor: text;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.header-search__field:hover { border-color: var(--ink-faint); }

.header-search__field:focus-within {
  color: var(--ink);
  border-color: var(--lime-mark);
}

.header-search__field svg { width: 15px; height: 15px; flex: none; }

.header-search__field input[type="search"] {
  flex: 1;
  min-width: 0;
  padding: 8px 0;
  font-size: var(--text-xs);
  text-overflow: ellipsis;
}

.header-search__list {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  z-index: 30;
  width: min(420px, calc(100vw - 2 * var(--space-5)));
  max-height: min(60vh, 460px);
  overflow-y: auto;
  padding: var(--space-2);
  background: var(--paper);
  border: var(--hairline);
  border-radius: 0.875rem;
  box-shadow: var(--shadow-card);
}

.header-search__list[hidden] { display: none; }

.header-search__item {
  display: grid;
  grid-template-columns: 36px minmax(0, 1fr) auto;
  gap: var(--space-5);
  align-items: center;
  padding: var(--space-3) var(--space-4);
  border-radius: 10px;
  color: var(--ink);
}

.header-search__item:hover,
.header-search__item[aria-selected="true"] {
  background: var(--sheet-2);
  text-decoration: none;
}

.header-search__item img,
.header-search__item .header-search__ph {
  width: 36px;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  border-radius: 4px;
  background: var(--sheet-2);
}

.header-search__item b {
  display: block;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.header-search__item small {
  color: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
}

.header-search__note {
  padding: var(--space-5) var(--space-4);
  color: var(--ink-faint);
  text-align: center;
  font-size: var(--text-sm);
}

/* --- Posters come alive ---------------------------------------------------------
   Every poster card on the site lifts toward the reader on hover. */

.poster-card .poster,
.poster-card .headshot,
.poster-card .placeholder {
  border-radius: 8px;
}

.poster-card a:hover .poster,
.poster-card a:focus-visible .poster,
.poster-card a:hover .headshot,
.poster-card a:focus-visible .headshot {
  transform: translateY(-6px);
  box-shadow: 0 24px 40px -18px rgb(0 0 0 / 0.7), 0 0 0 1px var(--rule-strong);
}

/* --- Page transitions -------------------------------------------------------------
   Same-origin navigations cross-fade (Chrome/Edge 126+; other browsers just
   navigate). A poster clicked on one page morphs into the film page's poster:
   cinema.js names the clicked image "film-poster", and film.css gives the
   film page's poster the same name. */
@view-transition { navigation: auto; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.35s;
}

::view-transition-group(film-poster) {
  animation-duration: 0.5s;
  animation-timing-function: var(--ease-out);
}

/* Sections ease in as they scroll into view, where the browser supports
   scroll-driven animation. Everything is fully visible without it. */
@supports (animation-timeline: view()) {
  .reveal {
    animation: reveal-in linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 60%;
  }

  @keyframes reveal-in {
    from { opacity: 0.15; transform: translateY(40px); }
    to { opacity: 1; transform: none; }
  }
}

@media (max-width: 1240px) {
  html.has-js .search-btn { display: inline-flex; }

  /* The field becomes a full-width row under the bar, shown on demand. */
  html.has-js .header-search {
    display: none;
    order: 9;
    flex: none;
    width: 100%;
    max-width: none;
  }

  html.has-js .header-search.is-open { display: block; }

  html.has-js .header-search__field {
    padding-inline: var(--space-5);
    gap: var(--space-3);
  }

  html.has-js .header-search__list {
    left: 0;
    width: auto;
  }
}

/* Below the hamburger breakpoint the search button joins the header row's
   controls, between Sign in (or the account trigger, which carries the
   row's auto margin) and the language pill -- the ordering scheme of the
   mobile nav block below. */
@media (max-width: 1100px) {
  html.has-js .search-btn { order: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .ambient,
  .grain { animation: none; }
}

/* --- Responsive ------------------------------------------------------------- */

@media (max-width: 720px) {
  .container { padding-inline: var(--space-5); }
  main.container { padding-block: var(--space-7) var(--space-8); }
  .nav { --nav-gap: var(--space-4); }
  .sheet-title { font-size: var(--text-xl); }
  .stats { gap: var(--space-6); }

  /* The 200px floor on a search field exists so it never shrinks to a
     useless nub on a desktop toolbar; on a phone-width sheet it's the
     opposite problem, since 200px alone can be most of the viewport. */
  input[type="text"],
  input[type="search"] {
    min-width: 0;
  }

  .toolbar form { width: 100%; }
  .toolbar .field { flex: 1 1 160px; }

  /* Four fixed-width labels in one unwrapping row is the one place the
     segmented control can genuinely overflow a narrow sheet — the buttons
     have to keep working without JS, so this stays flex-wrap rather than a
     <select>. */
  .segmented {
    flex-wrap: wrap;
    width: 100%;
  }

  .segmented label {
    flex: 1 1 auto;
    text-align: center;
  }
}

/* Mobile Safari zooms the whole viewport in when a form control it is about to
   focus computes to under 16px, and does not zoom back out on blur — so tapping
   the search box on /movies/ leaves the reader on a magnified, side-scrolling
   page for the rest of the visit. The threshold is absolute, which is why 16px
   is written literally here rather than as --text-base: if that token were ever
   retuned, this fix would stop working silently.

   Keyed on pointer:coarse, not a width — an iPad in landscape is 1024px wide
   and zooms exactly the same. Desktop keeps the 14px the design calls for.
   The alternative, maximum-scale=1 on the viewport meta, would also stop the
   zoom, by taking pinch-zoom away from everyone. */
@media (pointer: coarse) {
  input[type="text"],
  input[type="search"],
  .field select,
  .header-search__field input[type="search"] {
    font-size: 16px;
  }
}

/* --- Mobile nav --------------------------------------------------------------
   Gated on html.has-js (set by an inline, before-paint script in base.html —
   see the comment there). Without it .nav-links keeps the plain wrapping row
   it has always had: a working, if longer, list of links rather than a
   hamburger button with nothing behind it. With it, the links collapse into
   a panel that opens below the header, and the hamburger takes their place
   in the row. */
@media (max-width: 1100px) {
  html.has-js .nav-toggle {
    display: inline-grid;
    order: 2;
    margin-left: auto;
  }

  /* Signed out, Sign in stays in the header row beside the hamburger rather
     than behind it: the one action a new visitor is here to find. Matches the
     34px icon buttons' height so the row reads as one set of controls. */
  html.has-js .signin-btn--header {
    display: inline-flex;
    order: 1;
    margin-left: auto;
    height: 34px;
    padding-block: 0;
  }

  html.has-js .nav-links a.signin-btn {
    display: none;
  }

  /* Signed in, the account trigger takes the same spot the Sign in chip
     would -- the reader's own account is exactly as findable either way,
     never a hamburger-menu-only affordance. */
  html.has-js .account-menu--header {
    display: inline-flex;
    order: 1;
    margin-left: auto;
  }

  /* Whichever chip is actually in the DOM (signed out vs. signed in) took
     the auto margin; pull the hamburger in beside it either way. */
  html.has-js .signin-btn--header + .nav-toggle,
  html.has-js .account-menu--header + .nav-toggle {
    margin-left: calc(var(--space-2) - var(--space-4));
  }

  /* The language switcher stands on its own in the header row, right after
     Sign in / the account trigger, like they do -- never inside the hamburger
     panel. Its list floats as a popover under it, as on desktop. */
  html.has-js .lang-switch {
    order: 1;
    margin-left: calc(var(--space-2) - var(--nav-gap));
  }

  html.has-js .theme-toggle {
    order: 3;
  }

  /* With Sign in (or the account trigger), the language pill, the hamburger
     and the theme toggle all in the header row, the wordmark is the one piece
     that doesn't fit a phone: it needs ~400px and a 360px screen has ~330.
     The Θ mark stays and carries the brand; the name drops out of the layout
     but stays in the DOM, via the same .visually-hidden recipe, so the link
     still reads "Theoria" to a screen reader. */
  @media (max-width: 440px) {
    html.has-js .brand-name {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
      border: 0;
    }
  }

  html.has-js .nav-links {
    display: none;
    order: 10;
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    margin-left: 0;
    margin-top: var(--space-2);
    padding-top: var(--space-2);
    border-top: var(--hairline);
  }

  html.has-js .nav-links.is-open {
    display: flex;
  }

  html.has-js .nav-links a {
    padding-block: var(--space-4);
    border-bottom: var(--hairline);
  }

  html.has-js .nav-links a:last-child {
    border-bottom: none;
  }

  html.has-js .nav-links a[aria-current="page"]::after {
    display: none;
  }

  html.has-js .nav-links a[aria-current="page"] {
    color: var(--lime-text);
  }
}

/* Just above the hamburger breakpoint the full nav, the Sign in chip and the
   theme toggle overflow one row, so the links close up until there's room. */
@media (min-width: 1101px) and (max-width: 1240px) {
  .nav-links {
    gap: var(--space-4);
  }

  .nav-links a.signin-btn {
    margin-left: 0;
  }

  .lang-switch {
    margin-left: calc(var(--space-4) - var(--nav-gap));
  }

  /* Too close to the viewport edge to centre under a short username. */
  .account-panel {
    left: auto;
    right: 0;
    translate: none;
    transform-origin: top right;
  }
}

/* Below ~400px (a 360px Android, a 375px iPhone) the Θ, the chip, search, the
   language pill and two icon buttons overflow one row and the theme toggle
   wraps onto a second line, so the chip sheds its icon and the controls
   close up. */
@media (max-width: 399px) {
  .site-header .nav {
    --nav-gap: var(--space-3);
    padding-left: var(--space-5);
  }

  html.has-js .signin-btn--header {
    padding-inline: var(--space-3);
    font-size: var(--text-xs);
  }

  html.has-js .signin-btn--header .signin-icon {
    display: none;
  }

  /* The disc alone still opens the menu and still identifies the account;
     the username itself goes the .visually-hidden route (theoria.css's own
     recipe, inlined here) rather than display:none, so it's still there for
     a screen reader even though the row has no room left to show it. */
  html.has-js .account-menu--header .menu-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  html.has-js .account-menu--header .account-trigger {
    gap: 0;
    padding-right: var(--space-3);
  }

  html.has-js .signin-btn--header ~ .theme-toggle,
  html.has-js .account-menu--header ~ .theme-toggle {
    margin-left: calc(var(--space-2) - var(--space-4));
  }
}

/* At 320px even that is ~10px too wide: the language pill drops its chevron
   (the pill still opens on a tap, and the code alone names it). */
@media (max-width: 359px) {
  html.has-js .lang-menu__chevron {
    display: none;
  }

  html.has-js .lang-menu__trigger {
    padding-inline: var(--space-4);
  }
}

@media (max-width: 720px) {
  .ai-assistant {
    right: var(--space-4);
    bottom: var(--space-4);
  }

  .ai-assistant-panel {
    position: fixed;
    right: var(--space-4);
    bottom: 82px;
    width: calc(100vw - 2 * var(--space-4));
    height: min(560px, calc(100dvh - 104px));
  }

  .ai-assistant-panel.is-expanded {
    bottom: var(--space-4);
    height: calc(100dvh - 2 * var(--space-4));
  }

  .ai-assistant-welcome {
    position: fixed;
    right: var(--space-4);
    bottom: 82px;
    width: min(310px, calc(100vw - 2 * var(--space-4)));
  }
}

/* Studio logos are wide plates: on a phone one per row is a full screen of
   blank plate each, so they pair up. */
@media (max-width: 599px) {
  .poster-grid.is-wide {
    grid-template-columns: repeat(auto-fill, minmax(136px, 1fr));
    gap: var(--space-4);
  }
}

/* On the desktop the wordmark bleeds off both edges on purpose; on a phone or tablet a
   clipped final letter just reads as a bug, so it is sized to fit whole. */
@media (max-width: 900px) {
  .footer-mark {
    font-size: calc(16.2 * var(--vw, 1vw));
  }
}

/* A 320px phone has 272px inside the gutters: the 148px minimum would make
   that one enormous poster per row instead of two. Not under TV, whose media
   queries see a 960px+ screen anyway. */
@media (max-width: 359px) {
  .poster-grid {
    grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
    gap: var(--space-4);
  }

  .poster-grid.is-tight {
    grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
    gap: var(--space-3);
  }

  .poster-grid.is-wide {
    grid-template-columns: repeat(auto-fill, minmax(124px, 1fr));
    gap: var(--space-4);
  }
}

/* --- Touch targets ------------------------------------------------------------
   The header's icon buttons are 34px circles and a 360px phone has no room
   for six 44px ones in a row, so their hit area grows past the drawn shape
   instead: an invisible plate that reaches into the gaps between neighbours
   (never past the midpoint, so two plates never compete for one tap). The
   controls that sit alone on a line simply get taller. */
@media (pointer: coarse) {
  .site-header :where(.theme-toggle, .nav-toggle, .search-btn, .lang-menu__trigger,
                      .account-trigger, .signin-btn--header) {
    position: relative;
  }

  .site-header :where(.theme-toggle, .nav-toggle, .search-btn, .lang-menu__trigger,
                      .account-trigger, .signin-btn--header)::after {
    content: "";
    position: absolute;
    inset: -5px -3px;
  }

  /* The bare Θ is 17px wide once the name drops out on a phone. */
  .site-header .brand {
    position: relative;
  }

  .site-header .brand::after {
    content: "";
    position: absolute;
    inset: -12px -10px;
  }

  .lang-switch__opt,
  .pagination a,
  .pagination.is-client .page-btn,
  html.has-js .nav-links a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  html.has-js .nav-links a {
    display: flex;
  }

  .pagination a,
  .pagination.is-client .page-btn {
    justify-content: center;
  }

  .chip {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
  }

  .menu-trigger:not(.account-trigger),
  .field input[type="text"],
  .field input[type="search"],
  .field select {
    min-height: 44px;
  }
}

/* --- TV shell ---------------------------------------------------------------
   The ten-foot page, for every page (the home page's reel, wall and rails
   stay in home.css). base.html marks a TV browser, or any screen 3000px+
   wide, with html.is-tv and works out --tv-zoom so that the screen reads as a
   1280px-wide page: 1.5x on a 1080p TV, 3x on a 4K one, 1x on a TV that
   already reports a halved 960px viewport. Media queries still see the real
   screen, which is why nothing in this section is a width query: a 4K TV has
   to come out looking exactly like the 1080p one, and its 3840px would
   otherwise be treated as a very wide desktop.

   Four rules for any page CSS written against this:
     - Full-bleed or screen-relative lengths use var(--vw, 1vw) and
       var(--vh, 1vh). Under a zoom a plain vw is multiplied too, so 100vw
       comes out 1.5 screens wide.
     - Edges use var(--tv-gutter) (set below, in pre-zoom px): many TVs crop
       the outer few percent of the picture.
     - Focus is the cursor. Anything focusable already gets the heavier ring
       below; a card-like element should lift and take the lime frame the way
       .poster-card does (see "Focus" below) rather than rely on the ring.
     - Nothing may need a hover. Hover-only affordances get a :focus-visible
       twin, or are shown outright under html.is-tv. */

html.is-tv body {
  --tv-gutter: 56px;
  --vw: var(--tv-vw, 1vw);
  --vh: var(--tv-vh, 1vh);
  zoom: var(--tv-zoom, 1);
}

/* scroll-padding belongs to the root, which is not zoomed, so the header's
   clearance has to be scaled by hand or an anchor lands under the bar. */
html.is-tv {
  scroll-padding-top: calc((60px + var(--space-5)) * var(--tv-zoom, 1));
}

html.is-tv .container {
  max-width: none;
  padding-inline: var(--tv-gutter);
}

/* Reading sizes for a sofa. The small tokens are 11-13px, which at 1.5x is
   still under 20 physical px on a 1080p panel; the home page sets its own
   sizes and keeps the tokens it was tuned with. */
html.is-tv body:not(.page-home) {
  --text-3xs: 0.8125rem;
  --text-2xs: 0.875rem;
  --text-xs: 0.9375rem;
  --text-sm: 1rem;
  --text-md: 1.1875rem;
}

html.is-tv .site-header {
  padding-top: var(--space-6);
}

html.is-tv .site-header .nav {
  width: calc(100% - 2 * (var(--tv-gutter) - var(--space-6)));
  max-width: none;
}

/* The header search folds to its magnifier, as it does on a laptop under
   1240px. A TV's media queries still see the full screen, so that rule never
   fires here, and the 200px field is what pushes the longer Russian and
   Uzbek nav labels onto a second row. Typing with a remote starts from a
   button anyway. */
html.is-tv.has-js .search-btn {
  display: inline-flex;
}

html.is-tv.has-js .header-search {
  display: none;
  order: 9;
  flex: none;
  width: 100%;
  max-width: none;
}

html.is-tv.has-js .header-search.is-open {
  display: block;
}

html.is-tv.has-js .header-search__list {
  left: 0;
  width: auto;
}

/* Screen-relative caps on floating lists: 60vh would be 60% of the screen
   times the zoom. */
html.is-tv .header-search__list {
  max-height: min(calc(60 * var(--vh, 1vh)), 460px);
}

html.is-tv .menu-panel {
  max-height: min(calc(60 * var(--vh, 1vh)), 20rem);
}

html.is-tv body:not(.page-home) .site-footer {
  font-size: var(--text-sm);
}

/* One step bigger posters, so a row of them still reads from the sofa: six
   across a 1280px page rather than seven. */
html.is-tv .poster-grid {
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: var(--space-6);
}

html.is-tv .poster-grid.is-tight {
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--space-5);
}

html.is-tv .poster-grid.is-wide {
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

/* --- Focus ----------------------------------------------------------------
   A poster, headshot or logo under focus lifts and takes the lime frame, the
   treatment the home page's rows use, instead of a thin outline round the
   whole card (which would also frame the title). Everything else focusable
   gets a heavier version of the site's ring. */

html.is-tv :where(a, button, input, select, summary, [tabindex]):focus-visible {
  outline-width: 3px;
  outline-offset: 4px;
}

/* A toolbar field draws its focus on the wrapper — a 1px lime border that
   vanishes from across a room — so on a TV the wrapper takes a full frame
   and the control inside drops its own ring. */
html.is-tv .field:focus-within {
  box-shadow: 0 0 0 3px var(--lime-mark);
}

html.is-tv .field :is(input, select):focus-visible {
  outline: none;
}

/* The segmented strip clips its children (overflow: hidden), so the ring
   above would be cut off. The focused segment fills instead, which reads from
   across a room and survives the clip; the checked one keeps its lime and
   takes an ink frame, since lime on lime would vanish. */
html.is-tv .segmented input:focus-visible + label {
  outline: 3px solid var(--lime-mark);
  outline-offset: -3px;
  background: var(--sheet);
  color: var(--lime-text);
}

html.is-tv .segmented input:checked:focus-visible + label {
  background: var(--lime);
  color: var(--ink-on-lime);
  outline-color: var(--ink-on-lime);
}

html.is-tv .poster-card a:focus-visible {
  outline: none;
}

html.is-tv .poster-card .poster,
html.is-tv .poster-card .headshot,
html.is-tv .poster-card .logo,
html.is-tv .poster-card .placeholder {
  transition: transform var(--dur-slow) var(--ease-out),
              box-shadow var(--dur-slow) var(--ease-out),
              border-color var(--dur) var(--ease);
}

html.is-tv .poster-card .poster,
html.is-tv .poster-card .headshot,
html.is-tv .poster-card .logo,
html.is-tv .poster-card .placeholder {
  /* Grows up into the gap above rather than down over its own title. */
  transform-origin: 50% 100%;
}

html.is-tv .poster-card a:focus-visible .poster,
html.is-tv .poster-card a:focus-visible .headshot,
html.is-tv .poster-card a:focus-visible .logo,
html.is-tv .poster-card a:focus-visible .placeholder {
  transform: scale(1.06);
  border-color: var(--lime-mark);
  box-shadow: 0 0 0 3px var(--lime-mark), 0 28px 56px -20px rgb(0 0 0 / 0.85);
}

/* A lifted frame must not be cut off by the grid's neighbours: the title
   below it stays put, the image grows over the gap. */
html.is-tv .poster-card a:focus-visible .card-title {
  color: var(--lime-text);
}

/* --- Quality floor ---------------------------------------------------------- */

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