
* { box-sizing: border-box; margin: 0; padding: 0; }

/* Watch (cinematic dark) — default theme */
:root {
  --bg: #0A0A10;
  --surface: #161821;
  --surface-2: #1F2230;
  --surface-3: #2A2D3D;
  --border: #2E3145;
  --text: #F5F2EA;
  --text-dim: #A19B8E;
  --text-muted: #6B6354;
  --accent: #D4AF37;
  --accent-hover: #B8962E;
  --success: #4ADE80;
  --success-dim: rgba(74, 222, 128, 0.15);
  --warning: #F59E0B;
  --warning-dim: rgba(245, 158, 11, 0.15);
  --vpn: #38BDF8;
  --vpn-dim: rgba(56, 189, 248, 0.15);
  --cinema: #EF4444;
  --cinema-dim: rgba(239, 68, 68, 0.15);
  --cinema-soon: #A855F7;
  --cinema-soon-dim: rgba(168, 85, 247, 0.15);
  --danger: #ef4444;
  --danger-dim: rgba(239, 68, 68, 0.12);
  --font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, system-ui, sans-serif;
  --font-display: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, system-ui, sans-serif;
  --topbar-bg: rgba(22, 24, 33, 0.94);
  /* Foreground color for chips / badges / pills painted on a colored
     background (--accent, --success, --vpn, --warning). Defaults to --bg
     because each dark theme's bg is dark enough to read on the light-tinted
     accent. The light Watch override flips it to white (#92). */
  --on-accent: var(--bg);
}

/* Read (Kindle / parchment) — applied when category === 'books' */
body.cat-books {
  --bg: #F4ECD7;
  --surface: #EBE0C4;
  --surface-2: #E0D2B0;
  --surface-3: #D4C49B;
  --border: #C9B98B;
  --text: #3C2E26;
  --text-dim: #6B5947;
  --text-muted: #8B7960;
  --accent: #7B2C2C;
  --accent-hover: #5A1F1F;
  --success: #2F7D4E;
  --success-dim: rgba(47, 125, 78, 0.18);
  --warning: #B58A2A;
  --warning-dim: rgba(181, 138, 42, 0.15);
  --vpn: #355C7D;
  --vpn-dim: rgba(53, 92, 125, 0.18);
  --danger: #B91C1C;
  --danger-dim: rgba(185, 28, 28, 0.12);
  --font-body: Georgia, 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', serif;
  --font-display: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Georgia, serif;
  --topbar-bg: rgba(235, 224, 196, 0.94);
}

/* Play (arcade / neon) — applied when category === 'games'.
   Deep violet base + electric purple accent + cyan secondary. Reads as
   "gamer" without going garish; surfaces still have enough contrast for
   long browsing sessions. */
body.cat-games {
  --bg: #0E0B1A;
  --surface: #1A1530;
  --surface-2: #251D40;
  --surface-3: #312657;
  --border: #3A2D6E;
  --text: #F2F0FF;
  --text-dim: #B0A4D8;
  --text-muted: #6E5FA0;
  --accent: #A855F7;
  --accent-hover: #9333EA;
  --success: #22D3EE;
  --success-dim: rgba(34, 211, 238, 0.16);
  --warning: #FBBF24;
  --warning-dim: rgba(251, 191, 36, 0.15);
  --vpn: #38BDF8;
  --vpn-dim: rgba(56, 189, 248, 0.16);
  --danger: #ef4444;
  --danger-dim: rgba(239, 68, 68, 0.12);
  --font-display: 'JetBrains Mono', 'SF Mono', 'Source Code Pro', 'Menlo', 'Consolas', ui-monospace, monospace;
  --topbar-bg: rgba(26, 21, 48, 0.94);
}

/* Light Watch — daytime variant of the cinematic dark default (#92, #228).
   The accent / success / vpn / warning colors are darkened so badges that
   paint --on-accent (white) on top hit WCAG AA. Triggered by explicit Light
   mode OR by Auto mode + system-light (#228). */
body.cat-movies.theme-light,
body.cat-movies.theme-auto.system-light {
  --bg: #FAFAF7;
  --surface: #FFFFFF;
  --surface-2: #F2EFE8;
  --surface-3: #E9E5DA;
  --border: #E1DDD2;
  --text: #1A1818;
  --text-dim: #5C564B;
  --text-muted: #8A8474;
  --accent: #B8862E;
  --accent-hover: #9C7124;
  --success: #15803D;
  --success-dim: rgba(21, 128, 61, 0.15);
  --warning: #B45309;
  --warning-dim: rgba(180, 83, 9, 0.15);
  --vpn: #0369A1;
  --vpn-dim: rgba(3, 105, 161, 0.15);
  --cinema: #DC2626;
  --cinema-dim: rgba(220, 38, 38, 0.15);
  --cinema-soon: #7C3AED;
  --cinema-soon-dim: rgba(124, 58, 237, 0.15);
  --danger: #DC2626;
  --danger-dim: rgba(220, 38, 38, 0.12);
  --topbar-bg: rgba(255, 255, 255, 0.94);
  /* #958: white on the light-theme gold (#B8862E) is ~3.3:1, an AA fail at
     button text sizes. Near-black mirrors the dark theme's bg-on-gold and
     reads as the gold-ticket look. */
  --on-accent: #1A1818;
  color-scheme: light;
}

/* Read dark — paper-feel night mode (#228). Inverts parchment while keeping
   the bookish serif identity (--font-body inherits from body.cat-books).
   Triggered by explicit Dark mode OR by Auto mode + system-dark. White on
   the warm-brick accent reads ~5:1 (AA). */
body.cat-books.theme-dark,
body.cat-books.theme-auto.system-dark {
  --bg: #1A1410;
  --surface: #241B14;
  --surface-2: #2D2218;
  --surface-3: #36281D;
  --border: #3A2E22;
  --text: #E8DCC4;
  --text-dim: #B8A988;
  --text-muted: #9A8B6E;
  --accent: #A0524C;
  --accent-hover: #8A453F;
  --success: #4ADE80;
  --success-dim: rgba(74, 222, 128, 0.16);
  --warning: #F59E0B;
  --warning-dim: rgba(245, 158, 11, 0.15);
  --vpn: #38BDF8;
  --vpn-dim: rgba(56, 189, 248, 0.16);
  --danger: #ef4444;
  --danger-dim: rgba(239, 68, 68, 0.12);
  --topbar-bg: rgba(36, 27, 20, 0.94);
  --on-accent: #FFFFFF;
  color-scheme: dark;
}

/* Play light — non-neon games variant (#228). Saturated violet accent stays
   — that's the gaming hook — but bumped from #A855F7 to #7C3AED so accent
   links + badges (white text on accent bg) hit AA on the pale lavender
   surface. Triggered by explicit Light mode OR by Auto + system-light. */
body.cat-games.theme-light,
body.cat-games.theme-auto.system-light {
  --bg: #F2EEFA;
  --surface: #FFFFFF;
  --surface-2: #EAE3F5;
  --surface-3: #DDD2EC;
  --border: #CFC1E0;
  --text: #1A0F2E;
  --text-dim: #5C4A85;
  --text-muted: #6B5B96;
  --accent: #7C3AED;
  --accent-hover: #6D28D9;
  --success: #0891B2;
  --success-dim: rgba(8, 145, 178, 0.16);
  --warning: #B45309;
  --warning-dim: rgba(180, 83, 9, 0.15);
  --vpn: #0369A1;
  --vpn-dim: rgba(3, 105, 161, 0.15);
  --danger: #DC2626;
  --danger-dim: rgba(220, 38, 38, 0.12);
  --topbar-bg: rgba(255, 255, 255, 0.94);
  --on-accent: #FFFFFF;
  color-scheme: light;
}

/* ==========================================================================
   Design token scales (#957)
   These are additive: new CSS should use the tokens; existing rules are
   migrated opportunistically. Do NOT add brand colours here (those live in
   the per-theme blocks above); these are purely structural / layout tokens.
   ========================================================================== */
:root {
  /* Type scale */
  --fs-2xs:  10px;   /* timestamp / badge microtext */
  --fs-xs:   11px;   /* poster badge, legend */
  --fs-sm:   12px;   /* helper / secondary label */
  --fs-base: 14px;   /* body default */
  --fs-md:   15px;   /* slightly-larger body */
  --fs-lg:   16px;   /* subhead / card title secondary */
  --fs-xl:   18px;   /* section heading */
  --fs-2xl:  22px;   /* card title */
  --fs-3xl:  28px;   /* page / modal title */
  --fs-hero: 40px;   /* stats hero number */

  /* Radius scale */
  --r-sm:   6px;
  --r-md:   8px;
  --r-lg:   12px;
  --r-xl:   16px;
  --r-pill: 999px;

  /* Spacing scale (4px base grid) */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-7: 32px;
  --sp-8: 48px;

  /* Elevation (cinematic dark default -- warm-black shadows) */
  --shadow-1: 0 2px 8px rgba(0, 0, 0, 0.30);
  --shadow-2: 0 4px 16px rgba(0, 0, 0, 0.40);
  --shadow-3: 0 8px 32px rgba(0, 0, 0, 0.55);

  /* Scrim overlay (shared by card info gradients and modal close buttons;
     kept as a token so both can be updated in one place). */
  --scrim: rgba(0, 0, 0, 0.62);

  /* Missing aliases (#957 / V-7): these were referenced in 27 Party and
     filter-chip rules but never defined, causing transparent backgrounds.
     Defined here so all callers resolve correctly without code changes. */
  --bg-card:    var(--surface);
  --bg-soft:    var(--surface-2);
  --accent-soft: color-mix(in srgb, var(--accent) 12%, transparent);
  --font-mono:  ui-monospace, SFMono-Regular, Menlo, 'Roboto Mono', monospace;
  /* Two more vocabulary strays the css-vars guard surfaced (#957): wizard
     legal links used --muted (other-project name for --text-dim) and the
     admin cards used --bg-elev (elevated surface = --surface-2). */
  --muted: var(--text-dim);
  --bg-elev: var(--surface-2);
  /* Stats chart ramp (#958): theme-automatic via color-mix; replaces the
     old brightness()/saturate() filter hacks that muddied light themes. */
  --chart-1: var(--accent);
  --chart-2: color-mix(in srgb, var(--accent) 72%, var(--surface-3));
  --chart-3: color-mix(in srgb, var(--accent) 48%, var(--surface-3));
  --chart-4: color-mix(in srgb, var(--accent) 28%, var(--surface-3));
}

/* Light Watch: lighter warm-tinted shadows (dark-tuned defaults look smudgy) */
body.cat-movies.theme-light,
body.cat-movies.theme-auto.system-light {
  --shadow-1: 0 2px 8px rgba(60, 40, 20, 0.14);
  --shadow-2: 0 4px 16px rgba(60, 40, 20, 0.20);
  --shadow-3: 0 8px 32px rgba(60, 40, 20, 0.28);
}

/* Read parchment: warm sepia shadows */
body.cat-books {
  --shadow-1: 0 2px 8px rgba(60, 40, 20, 0.16);
  --shadow-2: 0 4px 16px rgba(60, 40, 20, 0.22);
  --shadow-3: 0 8px 32px rgba(60, 40, 20, 0.30);
}

/* Play light: soft lavender-tinted shadows */
body.cat-games.theme-light,
body.cat-games.theme-auto.system-light {
  --shadow-1: 0 2px 8px rgba(30, 15, 60, 0.14);
  --shadow-2: 0 4px 16px rgba(30, 15, 60, 0.20);
  --shadow-3: 0 8px 32px rgba(30, 15, 60, 0.28);
}

/* Depth language: in light Watch and light Play the shadow now carries card
   depth, so the border would double-encode the edge. Remove it so the card
   reads as floating rather than outlined. Hover border-color: var(--accent)
   still shows on interaction (specificity wins). Read parchment stays
   border-led per the design spec; dark themes keep the border for contrast. */
body.cat-movies.theme-light .movie-card,
body.cat-movies.theme-auto.system-light .movie-card,
body.cat-games.theme-light .movie-card,
body.cat-games.theme-auto.system-light .movie-card {
  border-color: transparent;
}

body.cat-movies.theme-light .modal,
body.cat-movies.theme-auto.system-light .modal,
body.cat-games.theme-light .modal,
body.cat-games.theme-auto.system-light .modal {
  border-color: transparent;
}

/* ==========================================================================
   Display type primitives (#970)
   .display-num  — category-font large numeral (stats hero, key counts)
   .eyebrow      — 12px uppercase label above a headline or section title
   ========================================================================== */

/* Large numeral: uses the per-category display font so Watch reads in the
   system sans, Read in the Iowan/Palatino serif, Play in the mono stack.
   clamp(40px, 12cqw, 56px) scales inside container queries; falls back to
   40px on browsers without cq support (all modern engines have it). */
.display-num {
  font-family: var(--font-display);
  font-size: clamp(40px, 12cqw, 56px);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--text);
}

/* Uppercase label used above headings and as section titles.
   Generalises .recap-hero-eyebrow and converges .stat-section-title. */
.eyebrow {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text-dim);
}

/* Belt-and-braces against horizontal page scroll: any descendant that
   accidentally exceeds 100vw (e.g. a header overflow under a longer
   Romance-language locale + iOS larger Dynamic Type) won't make the
   document scroll sideways. Don't lean on this — fix the root cause —
   but it stops past-shape regressions like #53 and #98 from recurring. */
html, body {
  /* 'clip' (not 'hidden') prevents creating a scroll container — on Android
     Chrome, overflow-x:hidden on html can give the document a non-zero
     scrollLeft which shifts touch event clientX coords left of the actual
     tap point. clip clips without a scroll container so scrollLeft stays 0.
     #505: also use width:100% (not max-width:100vw) — 100vw can transiently
     exceed the visual viewport during Android URL-bar collapse/expand,
     re-inviting the same scrollLeft drift `clip` is meant to prevent. */
  overflow-x: clip;
  width: 100%;
  position: relative;
}
body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  line-height: 1.5;
  color-scheme: dark;
  /* Don't let iOS auto-scale text in WebKit (separate from Dynamic Type).
     We size things deliberately for narrow viewports; auto-adjust on top
     of those values pushes the header back into overflow territory. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* Force dark theming on native <option> dropdowns so they don't render
   white-on-white in light-OS-default browsers (notably the region picker). */
select option {
  background: var(--surface-2);
  color: var(--text);
}

button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
a { color: var(--accent); }

input, select {
  font: inherit; color: inherit;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 14px;
  outline: none;
  transition: border-color 0.15s;
}
input:focus, select:focus { border-color: var(--accent); }

/* IMPORTANT: don't put backdrop-filter on #topbar — it creates a containing
   block, which makes any descendant position:fixed (e.g. mobile bottom tabs)
   stick to the topbar instead of the viewport. The blur lives on header
   and on .tabs individually so the layout still looks frosted. */
#topbar {
  position: sticky;
  top: 0;
  z-index: 100;
}
header {
  /* position + z-index keep the header's stacking context above .tabs and
     above card elements (.provider-cluster z-index:2, .poster-quick-add-row
     z-index:3) so the avatar dropdown overflow paints over cards and their
     platform badges. Must stay below mobile .tabs (z-index:100) and modals
     (z-index:200+). */
  position: relative;
  z-index: 10;
  padding: 14px 24px;
  /* iOS PWA standalone (#283): with apple-mobile-web-app-status-bar-style
     "black-translucent", page content extends *behind* the status bar. Add
     the top inset so the wordmark / pills / icons clear the status-bar UI.
     Counterpart to the bottom-safe-area fix in #223 (.tabs padding-bottom).
     env(safe-area-inset-top) is 0 outside notched standalone PWA contexts,
     so this is a no-op on desktop / regular Safari / Android. The header's
     existing background fills the padded zone, so the status-bar UI sits on
     a themed strip rather than transparent space. */
  padding-top: calc(14px + env(safe-area-inset-top));
  display: flex; align-items: center; gap: 12px;
  border-bottom: 1px solid var(--border);
  background: var(--topbar-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

/* #1090 persistent BETA badge. A block-level strip below the header row
   (not another header-right icon) so it can never overlap or shrink the
   header's own controls at a 390pt viewport — it pushes content down by
   its own height instead of competing for header space. Sits inside the
   sticky #topbar so it stays visible while scrolling, same as .tabs.
   Warning-colored (not accent-colored) so it reads as "different / caution"
   rather than blending in as a themed brand element, and stays legible in
   both light and dark themes since --warning + --on-accent are defined per
   theme (see the :root / body.cat-* blocks above). */
.beta-channel-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 5px 12px;
  background: var(--warning);
  color: var(--on-accent);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--border);
  user-select: none;
}
.beta-channel-badge[hidden] { display: none; }
.beta-channel-badge svg { flex-shrink: 0; }

.tabs {
  background: var(--topbar-bg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.logo {
  display: flex; align-items: center;
}
.wordmark {
  font-size: 22px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--text);
  user-select: none;
}
.wordmark-accent { color: var(--accent); }
body.cat-books .wordmark {
  font-family: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Georgia, serif;
  font-weight: 700;
  font-style: italic;
  letter-spacing: 0;
}
/* Play wordmark: monospace gives it an "arcade marquee" / dev-console feel
   that pairs with the violet+cyan palette. */
body.cat-games .wordmark {
  font-family: 'JetBrains Mono', 'SF Mono', 'Source Code Pro', 'Menlo', 'Consolas', ui-monospace, monospace;
  font-weight: 700;
  letter-spacing: -0.04em;
}
body.cat-games .wordmark-accent {
  text-shadow: 0 0 12px rgba(168, 85, 247, 0.45);
}
.spacer { flex: 1; }

/* Right-side icon cluster (#550). Pushes itself to the right edge via
   margin-left:auto (cleaner than relying on .spacer) and gives the icon
   buttons a tight, consistent gap. */
.header-right {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

/* Unified header icon button (#550). Base treatment for all utility icons
   in the header right-cluster (donate heart, friends, notification bell).
   Profile avatar (.header-avatar) is intentionally NOT a .header-icon-btn —
   it stays bordered + filled as the identity anchor so users can always
   spot it. Keep this rule lean: any per-icon polish goes on the specific
   class below. */
.header-icon-btn {
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  background: transparent;
  position: relative;
  border-radius: 50%;
  color: var(--text-dim);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-sizing: border-box;
  transition: background 0.12s, color 0.12s, transform 0.12s;
}
.header-icon-btn:hover,
.header-icon-btn[aria-expanded="true"] {
  background: var(--surface-2);
  color: var(--accent);
}
.header-icon-btn:active { transform: scale(0.94); }
.header-icon-btn[hidden] { display: none; }
@media (max-width: 600px) {
  .header-right { gap: 2px; }
  .header-icon-btn { width: 32px; height: 32px; }
  .header-icon-btn svg { width: 18px; height: 18px; }
}
@media (max-width: 380px) {
  .header-icon-btn { width: 30px; height: 30px; }
  .header-icon-btn svg { width: 17px; height: 17px; }
  /* Region picker hides at this width — it's available in Settings ▸
     Preferences and the header gets too cramped otherwise. */
  .region-picker { display: none; }
  /* #940: extend tap target to 44pt via transparent ::after overlay.
     The overlay is centered on the element and does not affect layout
     (absolute, no z-index raise, transparent). */
  .header-icon-btn::after {
    content: '';
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    min-width: 44px; min-height: 44px;
  }
}
.region-picker {
  display: flex; align-items: center;
  height: 32px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0;
  width: 38px; min-width: 38px;
  justify-content: center;
  box-sizing: border-box;
  position: relative;
}
.region-picker:hover { border-color: var(--accent); }
.region-globe { display: none; }
/* Flag-chip — visible at all viewport widths (#507). The native select sits
   opacity:0 over the chip so taps open the OS dropdown. */
.region-flag-display {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 16px;
  pointer-events: none;
}
/* Header category selector — segmented (Watch / Read / Play). Tighter than
   filter-bar segmenteds since the header is a denser strip. Same height
   as .region-picker so the two controls line up cleanly.
   Wrapper uses min-height (not height) so it can grow to accommodate the
   36px touch-target min-height applied to .segmented button on coarse-
   pointer devices. With a hard height, the buttons overflowed and the
   active pill (with its accent background) appeared bulkier than the
   unselected ones. */
.segmented.category-toggle {
  min-height: 32px;
  padding: 2px;
  box-sizing: border-box;
}
.segmented.category-toggle button {
  padding: 0 10px;
  font-size: 12px;
  display: inline-flex;
  align-items: center;
}
/* Brand selector (#243) — unify wordmark + category pills into one rounded
   rectangle on the left of the header. The wrapper provides the chrome
   (border + tinted bg); the inner segmented strips its own. Wordmark uses
   per-category typography (sans / serif / mono) and is non-interactive so
   users don't tap it expecting a 4th option. A subtle divider separates
   the wordmark from the pills so the brand reads as identity, not button. */
.brand-selector {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 4px 3px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  height: 38px;
  box-sizing: border-box;
}
.brand-selector .logo {
  height: 100%;
  padding-right: 10px;
  border-right: 1px solid var(--border);
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.brand-logo-icon {
  width: 24px;
  height: 24px;
  border-radius: 6px;
  display: block;
  flex: 0 0 auto;
}
/* Settings ▸ App icon picker (#189) — smart categorised picker with search */
.app-icon-picker-wrap {
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* App-icon live preview tile (#565) */
.app-icon-preview {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
}
.app-icon-preview-img {
  width: 56px;
  height: 56px;
  border-radius: 14px;
  display: block;
}

.icon-picker-controls {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.icon-picker-search-wrap {
  position: relative;
}
.icon-picker-search {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 12px 8px 34px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  font-size: 13px;
  outline: none;
  transition: border-color 0.15s;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 10px center;
}
.icon-picker-search:focus {
  border-color: var(--accent);
}
.icon-picker-search::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M18 6 6 18M6 6l12 12'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  width: 14px;
  height: 14px;
}
.icon-picker-cats {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
}
.icon-picker-cats::-webkit-scrollbar { display: none; }
.icon-cat-tab {
  flex: 0 0 auto;
  padding: 5px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 20px;
  color: var(--text-dim);
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.13s, color 0.13s, background 0.13s;
}
.icon-cat-tab:hover {
  border-color: var(--accent);
  color: var(--text);
}
.icon-cat-tab.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.icon-picker-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
  gap: 8px;
  max-height: 320px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
  padding: 2px;
}
.icon-picker-grid::-webkit-scrollbar { width: 4px; }
.icon-picker-grid::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.app-icon-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 8px 4px 7px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  cursor: pointer;
  color: var(--text);
  font-size: 11px;
  transition:
    border-color 0.13s ease,
    box-shadow 0.13s ease,
    transform 0.1s ease;
}
.app-icon-option:hover {
  border-color: var(--accent);
  transform: translateY(-1px);
}
.app-icon-option.active {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.app-icon-option img {
  width: 48px;
  height: 48px;
  border-radius: 10px;
  display: block;
}
.app-icon-label {
  font-size: 11px;
  line-height: 1.2;
  text-align: center;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.icon-picker-empty {
  color: var(--text-dim);
  font-size: 13px;
  text-align: center;
  padding: 24px 0;
  margin: 0;
}
.brand-selector .segmented.category-toggle {
  background: transparent;
  border: 0;
  padding: 0;
  min-height: auto;
  gap: 2px;
}
.brand-selector .segmented.category-toggle button {
  height: 28px;
  padding: 0 10px;
}
/* Mobile fallback — Option A (icon pills). Full localized label is rendered
   alongside a small SVG icon; CSS picks one based on viewport. Icons are
   used (not first-letter abbreviations) because Italian Guardare/Giocare
   and German Sehen/Spielen both collide on a single starting letter. */
.cat-pill-short { display: none; }
.cat-pill-short svg { display: block; }
.region-picker select {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0;
  cursor: pointer;
  padding: 0;
}
/* Header profile slot (#120, simplified in #183 / #185). Hosts the single
   silhouette button + its absolutely-positioned dropdown menu. */
.header-avatar-slot {
  position: relative;
  display: inline-flex;
  align-items: center;
}
/* Single profile-icon button (#183). Always shows the silhouette in both
   signed-in and signed-out states; the dropdown menu items adapt by auth
   state instead. Unified sizing in #550 — kept distinct from the ghost
   .header-icon-btn utility icons via a persistent filled background +
   1px ring, so identity reads at a glance vs. tap-to-act utilities. */
.header-avatar {
  width: 34px; height: 34px;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-dim);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  border: 1px solid var(--border);
  overflow: hidden;
  position: relative;
  box-sizing: border-box;
  transition: border-color 0.12s, color 0.12s, background 0.12s;
}
.header-avatar:hover,
.header-avatar[aria-expanded="true"] {
  border-color: var(--accent);
  color: var(--accent);
}
@media (max-width: 600px) {
  .header-avatar { width: 32px; height: 32px; }
}
@media (max-width: 380px) {
  .header-avatar { width: 30px; height: 30px; }
}
/* Signed-in state (#187): fill with accent + on-accent silhouette so auth state
   reads at a glance. Toggled in renderHeaderAvatar() via .signed-in. The
   hover/expanded overrides use var(--on-accent) (the default :hover sets
   color to accent - on an accent background that disappears). */
.header-avatar.signed-in {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}
.header-avatar.signed-in:hover,
.header-avatar.signed-in[aria-expanded="true"] {
  filter: brightness(1.08);
  color: var(--on-accent);
  border-color: var(--accent);
}
.header-avatar-icon {
  width: 60%; height: 60%;
}
/* When the signed-in user has an avatar_url cached, .header-avatar-has-image
   replaces the silhouette glyph with an <img>. The image fills the round
   button via object-fit: cover so non-square portraits don't distort. */
.header-avatar-img {
  display: block;
  width: 100%;
  height: 100%;
  /* #968: clip the photo on the img itself; the container goes
     overflow:visible at <=480px for the #940 hit-area overlay. */
  border-radius: 50%;
  object-fit: cover;
}
.header-avatar.header-avatar-has-image {
  /* Drop the accent fill so the photo isn't tinted by .signed-in's
     background, and reuse the same border treatment to signal auth. */
  background: var(--surface-2);
}

.header-avatar-menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 50;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 6px;
  box-shadow: var(--shadow-2);
  min-width: 176px;
  display: flex; flex-direction: column;
  gap: 1px;
}
.header-avatar-menu[hidden] { display: none; }
.header-avatar-menu-item {
  text-align: left;
  padding: 9px 12px;
  font-size: 13.5px;
  border-radius: 6px;
  color: var(--text);
  background: transparent;
  cursor: pointer;
  border: none;
}
.header-avatar-menu-item:hover { background: var(--surface-2); }
/* Top-row "Logged in as <email>" item (#new). Reads as a contextual
   label and a shortcut into Settings → Account. Dimmer text, tighter
   line-height, single-line truncated with ellipsis so long addresses
   don't blow out the menu width. */
.header-avatar-menu-item.header-avatar-menu-account {
  font-size: 12px;
  color: var(--text-dim);
  border-bottom: 1px solid var(--border);
  margin-bottom: 1px;
  padding-bottom: 8px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 220px;
}
.header-avatar-menu-item.header-avatar-menu-account:hover {
  color: var(--text);
}

.tabs {
  /* Lower stacking context than header (which is z-index: 2) so the
     avatar dropdown overflowing out of the header paints over the tabs.
     The mobile rule below switches to position: fixed; z-index: 100. */
  position: relative;
  z-index: 1;
  display: flex; gap: 4px;
  padding: 16px 24px 0;
  border-bottom: 1px solid var(--border);
}
.tab {
  padding: 12px 18px;
  font-weight: 500; font-size: 14px;
  color: var(--text-dim);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 0.15s, border-color 0.15s;
  display: flex; align-items: center; gap: 8px;
  background: transparent;
}
.tab:hover { color: var(--text); }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
svg.tab-icon {
  width: 20px; height: 20px;
  display: block;
  flex-shrink: 0;
}
.tab-label { display: inline-flex; align-items: center; gap: 6px; }

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

.search-bar {
  position: relative;
  margin-bottom: 24px;
}
.search-bar input {
  width: 100%;
  /* Right padding leaves room for both the clear-button and the labelled
     AI button (1.7.37 — sparkle + "Ask AI" text). The AI pill is up to
     ~130px wide depending on locale; 150px of right padding clears it +
     the 32px clear-X with breathing room. */
  padding: 14px 150px 14px 48px;
  font-size: 16px;
  background: var(--surface);
}
.search-bar-clear {
  position: absolute;
  /* Sits to the left of the AI pill. Localised "Ask AI" can grow up to
     ~120px wide; reserve 130px so the clear-X never overlaps. */
  right: 130px;
  top: 50%;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  border: none;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  padding: 0;
}
.search-bar-clear:hover,
.search-bar-clear:focus-visible {
  color: var(--text);
  background: var(--surface-2);
  outline: none;
}
.search-bar-clear[hidden] { display: none; }
.search-bar-clear svg { width: 14px; height: 14px; }
/* "Ask AI" pill at the right edge of the search bar (1.7.37). Icon
   (sparkle) + localized label. Hidden when input is empty; reappears
   on typing. Tapping it escalates the current query to Llama-backed
   suggestions resolved against TMDB / OL / RAWG. The accent-tinted
   chrome doubles as the AI surface affordance. */
.search-bar-ai {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 12px 0 10px;
  max-width: 130px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  border-radius: 999px;
  transition: transform 0.12s ease, box-shadow 0.16s ease, background 0.16s ease;
  white-space: nowrap;
}
.search-bar-ai:hover,
.search-bar-ai:focus-visible {
  transform: translateY(-50%) scale(1.04);
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  box-shadow: 0 6px 16px -6px color-mix(in srgb, var(--accent) 55%, transparent);
  outline: none;
}
.search-bar-ai:active { transform: translateY(-50%) scale(0.96); }
.search-bar-ai[hidden] { display: none; }
.search-bar-ai-icon {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
}
.search-bar-ai-label {
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
/* When the clear-X is hidden, snap the AI button to its base position
   and shrink the input's right padding so the text doesn't strand. */
.search-bar:not(:has(.search-bar-clear:not([hidden]))) input {
  padding-right: 130px;
}
/* When the AI pill is hidden, shrink right padding to just the magnifier area. */
.search-bar:has(.search-bar-ai[hidden]) input {
  padding-right: 48px;
}
.search-bar input::placeholder {
  text-overflow: ellipsis;
}
/* Narrow viewports: collapse the AI pill back to icon-only so the
   search input doesn't disappear behind the button on phones. Aria
   label still announces "Ask AI" to screen readers. */
@media (max-width: 480px) {
  /* Clear button is at right:48px, 32px wide → left edge at 80px from right.
     Use 88px so text never slides under it. Without-clear case stays at 50px
     (AI icon only, left edge at 44px). */
  .search-bar input { padding: 14px 88px 14px 44px; }
  .search-bar-clear { right: 48px; }
  .search-bar:not(:has(.search-bar-clear:not([hidden]))) input {
    padding-right: 50px;
  }
  .search-bar:has(.search-bar-ai[hidden]) input {
    padding-right: 44px;
  }
  .search-bar-ai {
    padding: 0;
    width: 36px;
    height: 36px;
    justify-content: center;
    max-width: none;
    border-radius: 10px;
  }
  .search-bar-ai-icon { width: 18px; height: 18px; }
  .search-bar-ai-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
}
.search-icon {
  position: absolute; left: 16px; top: 50%;
  transform: translateY(-50%);
  color: var(--text-dim);
  pointer-events: none;
}
/* AI-search results banner (1.7.35). Sits above the resolved grid when
   the user tapped the sparkle button or pressed Enter on a free-text
   query, so the source of the results reads as "AI-suggested" rather
   than "canonical title search". The pulsing dot in the loading variant
   covers the ~1–3s window between AI request → resolved metadata. */
.ai-search-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  margin-bottom: 14px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent) 9%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--text);
  font-size: 13.5px;
}
.ai-search-banner-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  flex-shrink: 0;
}
.ai-search-banner-text {
  flex: 1 1 auto;
  min-width: 0;
}
.ai-search-banner-loading .ai-search-banner-icon {
  animation: aiSparklePulse 1.6s ease-in-out infinite;
}
@keyframes aiSparklePulse {
  0%, 100% { opacity: 0.4; transform: scale(0.92); }
  50% { opacity: 1; transform: scale(1.08); }
}
@media (prefers-reduced-motion: reduce) {
  .ai-search-banner-loading .ai-search-banner-icon { animation: none; }
}
.btn-icon { vertical-align: -2px; margin-right: 4px; flex-shrink: 0; }
.inline-icon { vertical-align: -2px; }
.ui-icon { display: inline-block; vertical-align: middle; }

.filter-bar {
  display: flex; gap: 12px; align-items: center;
  margin-bottom: 20px;
  flex-wrap: wrap;
}
.filter-bar label {
  font-size: 13px; color: var(--text-dim);
}
.filter-bar select { padding: 8px 12px; font-size: 14px; }

.segmented {
  display: inline-flex;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 3px;
  gap: 2px;
}
.segmented button {
  padding: 6px 12px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-dim);
  border-radius: 6px;
  transition: background 0.15s, color 0.15s;
}
.segmented button:hover { color: var(--text); }
.segmented button.active {
  background: var(--accent);
  color: var(--on-accent);
}

.movie-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 20px;
}
/* Skip rendering off-screen grid cards to cut layout cost. The intrinsic
   hint (320px) lets the browser reserve space so scroll-height is stable
   and the IntersectionObserver cover-lazy-load in src/util.js ~369 still
   fires at the right time (IO uses rootMargin:200px, so cards become
   visible before the browser paints them). */
.movie-grid .movie-card {
  content-visibility: auto;
  contain-intrinsic-size: auto 320px;
}

/* Load-more button below the Discover grid (#500). Centered pill that
   appends the next TMDB page to the grid on tap. Hidden once
   browseCache.exhausted flips (API returned no new items or the
   LOAD_MORE_MAX_PAGE cap is hit). Geometry borrows from the corner
   .poster-quick-add-primary so it reads as the same button family —
   accent fill, white inset ring for affordance on busy backgrounds,
   slightly larger touch target since it's not poster-overlaid. */
.load-more-wrap {
  display: flex;
  justify-content: center;
  margin-top: 24px;
}
.load-more-btn {
  border: none;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  padding: 12px 24px;
  min-height: 44px;
  cursor: pointer;
  background: var(--accent);
  color: var(--on-accent);
  box-shadow:
    inset 0 0 0 2px rgba(255, 255, 255, 0.85),
    0 2px 10px rgba(0, 0, 0, 0.3);
  transition: transform 0.15s ease, background 0.15s ease, opacity 0.15s ease;
}
.load-more-btn:hover:not(:disabled) { background: var(--accent-hover, var(--accent)); }
.load-more-btn:active:not(:disabled) { transform: scale(0.96); }
.load-more-btn:disabled { opacity: 0.7; cursor: progress; }
.load-more-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Grid card chrome mirrors the .swipe-card visual language (#195): larger
   border-radius, baseline shadow, immersive feel. Distinct from .swipe-card
   in that the action strip lives below the poster as a slim button row —
   that frequent-use target stays outside the art. */
.movie-card {
  background: var(--surface);
  border-radius: 18px;
  overflow: hidden;
  border: 1px solid var(--border);
  box-shadow: var(--shadow-2);
  transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
  cursor: pointer;
  display: flex; flex-direction: column;
  position: relative;
}
.movie-card:hover {
  transform: translateY(-3px);
  border-color: var(--accent);
  box-shadow: var(--shadow-3);
}
/* Touch press state: scale-down on tap without interfering with the
   desktop hover lift (which lives in the pointer:fine block above). */
@media (hover: none) {
  .movie-card:active { transform: scale(0.97); }
}
/* Theatrical highlights for movies — declared first so .available /
   .vpn-available win when both classes coexist (CSS source order; both
   selectors have the same specificity).
   Each modifier stacks an inset border-glow on top of the baseline drop
   shadow (#195) so the unified card chrome holds across status states. */
.movie-card.in-theaters-soon {
  border-color: var(--cinema-soon);
  box-shadow: 0 0 0 1px var(--cinema-soon-dim) inset, var(--shadow-2);
}
.movie-card.in-theaters-soon:hover {
  border-color: var(--cinema-soon);
  box-shadow: 0 0 0 1px var(--cinema-soon-dim) inset, 0 12px 28px rgba(168, 85, 247, 0.3);
}
.movie-card.in-theaters {
  border-color: var(--cinema);
  box-shadow: 0 0 0 1px var(--cinema-dim) inset, var(--shadow-2);
}
.movie-card.in-theaters:hover {
  border-color: var(--cinema);
  box-shadow: 0 0 0 1px var(--cinema-dim) inset, 0 12px 28px rgba(239, 68, 68, 0.3);
}
.movie-card.vpn-available {
  border-color: var(--vpn);
  box-shadow: 0 0 0 1px var(--vpn-dim) inset, var(--shadow-2);
}
.movie-card.vpn-available:hover {
  border-color: var(--vpn);
  box-shadow: 0 0 0 1px var(--vpn-dim) inset, 0 12px 28px rgba(56, 189, 248, 0.3);
}
.movie-card.available {
  border-color: var(--success);
  box-shadow: 0 0 0 1px var(--success-dim) inset, var(--shadow-2);
}
.movie-card.available:hover {
  border-color: var(--success);
  box-shadow: 0 0 0 1px var(--success-dim) inset, 0 12px 28px rgba(74, 222, 128, 0.3);
}
.movie-poster {
  aspect-ratio: 2/3;
  background: var(--surface-2);
  background-size: cover; background-position: center;
  position: relative;
  overflow: hidden;
  perspective: 1100px;
}
/* Native-lazy <img> sits inside .movie-poster so cover requests fire only as
   cards approach the viewport. Object-fit: cover preserves the prior crop
   behavior (game art is 16:9 — sides crop into the 2:3 frame). */
.movie-poster .poster-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
/* Game cards use the same 2:3 poster aspect as movies and books — even
   though RAWG hero art is 16:9 landscape (so object-fit: cover crops the
   sides), keeping a single card shape across Watch / Read / Play matters
   more than showing every pixel of the source art. */
.movie-poster.no-image::before {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 48px; height: 48px;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M6%2020.25h12m-7.5-3v3m3-3v3m-10.125-3h17.25c.621%200%201.125-.504%201.125-1.125V4.875c0-.621-.504-1.125-1.125-1.125H3.375c-.621%200-1.125.504-1.125%201.125v11.25c0%20.621.504%201.125%201.125%201.125Zm4.125-12h.008v.008H7.5V5.25Zm0%202.25h.008v.008H7.5V7.5Zm0%202.25h.008v.008H7.5V9.75Zm0%202.25h.008v.008H7.5V12Zm0%202.25h.008v.008H7.5v-.008ZM9.75%205.25h4.5M9.75%207.5h4.5m-4.5%202.25h4.5m-4.5%202.25h4.5m-4.5%202.25h4.5M16.5%205.25h.008v.008H16.5V5.25Zm0%202.25h.008v.008H16.5V7.5Zm0%202.25h.008v.008H16.5V9.75Zm0%202.25h.008v.008H16.5V12Zm0%202.25h.008v.008H16.5v-.008Z%22%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M6%2020.25h12m-7.5-3v3m3-3v3m-10.125-3h17.25c.621%200%201.125-.504%201.125-1.125V4.875c0-.621-.504-1.125-1.125-1.125H3.375c-.621%200-1.125.504-1.125%201.125v11.25c0%20.621.504%201.125%201.125%201.125Zm4.125-12h.008v.008H7.5V5.25Zm0%202.25h.008v.008H7.5V7.5Zm0%202.25h.008v.008H7.5V9.75Zm0%202.25h.008v.008H7.5V12Zm0%202.25h.008v.008H7.5v-.008ZM9.75%205.25h4.5M9.75%207.5h4.5m-4.5%202.25h4.5m-4.5%202.25h4.5m-4.5%202.25h4.5M16.5%205.25h.008v.008H16.5V5.25Zm0%202.25h.008v.008H16.5V7.5Zm0%202.25h.008v.008H16.5V9.75Zm0%202.25h.008v.008H16.5V12Zm0%202.25h.008v.008H16.5v-.008Z%22%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
  opacity: 0.3;
}
.poster-badge {
  position: absolute; top: 8px;
  padding: 4px 8px;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 600;
  color: white;
  display: flex; align-items: center; gap: 4px;
  max-width: calc(100% - 16px);
  white-space: nowrap;
  overflow: hidden;
}
/* Left + right max-widths must sum to less than 100% minus the two 8px
   inset offsets, otherwise the badges visibly overlap on narrow cards.
   Combined max here is 100% - 24px (8 inset + 8 inset + 8 gap). */
.poster-badge.right { right: 8px; max-width: calc(45% - 12px); }
.poster-badge.left { left: 8px; max-width: calc(55% - 12px); }
/* #434: when the .left chip is alone (no right-side peer competing for
   space — corner-status ★N or tv-progress S05E12), let it use almost
   the full poster width. Without this, the strict 55% cap clips longer
   translations on narrow 2-col mobile grids ("Not streaming" en,
   "Non in streaming" it, "Prochainement au cinéma" fr) even when the
   right side is empty. The :has() guard keeps the safe 55%/45% split
   the moment a right-side peer appears. */
.movie-poster:not(:has(> .poster-badge.right, > .poster-badge.tv-progress)) .poster-badge.left {
  max-width: calc(100% - 16px);
}
.poster-badge .badge-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.poster-badge.streaming { background: var(--success); color: var(--on-accent); }
.poster-badge.ads { background: var(--accent); color: var(--on-accent); }
.poster-badge.vpn { background: var(--vpn); color: var(--on-accent); }
.poster-badge.unavailable { background: rgba(0,0,0,0.78); color: #E5E5E5; }
.poster-badge.theatrical-now  { background: var(--cinema); color: white; }
.poster-badge.theatrical-soon { background: var(--cinema-soon); color: white; }
.poster-badge.corner-status.watched { background: var(--warning); color: var(--on-accent); }
.poster-badge.corner-status.onlist { background: var(--accent); color: var(--on-accent); }

/* Quick-action overlay button on Watchlist/Watched grid cards (#255).
   Top-right of the poster, dedicated slot. Watchlist cards get a "mark
   watched / read / played" check; Watched cards get a "move back to
   watchlist / reading list / wishlist" bookmark. The whole-card click
   still opens the modal — this button is a one-tap shortcut for the
   most common state move. Kept visually distinct from .poster-badge
   (circular, blurred, larger tap target) so it reads as interactive. */
.poster-quick-action {
  position: absolute;
  top: 8px; right: 8px;
  width: 36px; height: 36px;
  border-radius: 50%;
  border: none;
  background: rgba(0,0,0,0.62);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  z-index: 2;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  padding: 0;
  transition: opacity 0.15s ease, transform 0.15s ease, background 0.15s ease;
}
.poster-quick-action:hover { background: rgba(0,0,0,0.82); }
.poster-quick-action:active { transform: scale(0.92); }
.poster-quick-action:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Push the TV-progress pill below the quick-action button so they don't
   overlap when both are present on the same card. */
.movie-card .poster-quick-action ~ .poster-badge.tv-progress {
  top: 52px;
}
/* On hover-capable devices, hide the button until the card is hovered or
   focused — keeps the immersive poster quiet for desktop browse. On
   touch (the most common surface for quick moves) it's always visible. */
@media (hover: hover) and (pointer: fine) {
  .poster-quick-action { opacity: 0; transform: scale(0.9); }
  .movie-card:hover .poster-quick-action,
  .movie-card:focus-within .poster-quick-action {
    opacity: 1; transform: scale(1);
  }
}
/* Optimistic-leave animation when the user taps the quick-action — the
   card fades + scales down before re-render removes it from the grid. */
.movie-card.is-leaving {
  transition: opacity 0.22s ease, transform 0.22s ease;
  opacity: 0;
  transform: scale(0.94);
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .movie-card.is-leaving { transition: none; }
}

/* Quick-add corner buttons (#259) on Discover / For-You-grid cards.
   Anchored over the .movie-card-info gradient at the bottom of the
   poster so they don't fight the title for vertical space — the meta
   line (year + rating) is hidden on .has-quick-add cards because the
   action buttons need that strip. The .movie-card-info gradient gives
   the buttons enough contrast against bright posters; both buttons use
   the same dark blurred chrome as #255's .poster-quick-action with the
   primary swapped to accent for the dominant action. z-index sits above
   .movie-card-info (z-index 1) and the .provider-cluster (z-index 2)
   so the buttons stay tappable on cards where availability info loaded
   in afterwards. */
.poster-quick-add-row {
  position: absolute;
  left: 8px; right: 8px; bottom: 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  z-index: 3;
  pointer-events: none;
}
/* #315 retired the .single-action variant — every grid surface now uses the
   same 2-button [secondary, primary] row (Discover: + Watchlist / ✓ Watched;
   Watchlist: Remove / ✓ Watched; Watched: Remove / ★ Rate). The class is
   kept as a no-op alias for any legacy callers; new callers should not
   add it. */
.poster-quick-add-row.single-action { justify-content: space-between; }
.poster-quick-add-btn {
  pointer-events: auto;
  border: none;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  /* #512: flex:1 so both buttons always share the row equally; min-height 44px
     matches Apple HIG / WCAG 2.5.5 touch target on every screen size. */
  flex: 1;
  min-width: 0;
  min-height: 44px;
  padding: 10px 10px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  transition: transform 0.15s ease, background 0.15s ease, opacity 0.15s ease;
}
.poster-quick-add-btn:active { transform: scale(0.94); }
.poster-quick-add-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* #415 — visual affordance pass on the corner quick-action cluster.
   Both buttons gain a 2px outline ring + a heavier drop shadow so the
   pill chrome reads as a button — not a passive label — against any
   poster background (light, dark, or busy). The existing translucent
   scrim on .poster-quick-add-secondary stays; the ring is the new
   load-bearing affordance. Primary keeps the accent fill but gets a
   white ring to lift it off accent-colored posters where the fill
   would otherwise blend. */
.poster-quick-add-primary {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow:
    inset 0 0 0 2px rgba(255, 255, 255, 0.85),
    0 2px 10px rgba(0, 0, 0, 0.4);
}
.poster-quick-add-primary:hover { background: var(--accent-hover, var(--accent)); }
.poster-quick-add-secondary {
  /* Bumped scrim alpha (0.62 → 0.72) for stronger contrast against light
     posters where the previous translucent fill was washing out. The
     accent-colored ring is the dominant affordance. */
  background: rgba(0, 0, 0, 0.72);
  color: #fff;
  box-shadow:
    inset 0 0 0 2px var(--accent),
    0 2px 10px rgba(0, 0, 0, 0.4);
}
.poster-quick-add-secondary:hover { background: rgba(0, 0, 0, 0.88); }
/* Status chip (#323) — replaces the 2-button row on Discover cards
   that are already on the user's Watchlist or Watched. The user wants
   the *same* bottom-strip real-estate, just rendering a non-interactive
   "✓ On list" or "★ N" pill instead of action buttons. Geometry mirrors
   .poster-quick-add-btn so the bottom strip reads as a sibling family;
   chip color follows the prior corner-status convention (#301):
   --warning for Watched (rated/unrated), --accent for Watchlist. */
.poster-quick-add-row.is-status { justify-content: center; pointer-events: none; }
.poster-quick-add-chip {
  pointer-events: none;
  border: none;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  padding: 8px 14px;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  box-shadow: 0 2px 8px rgba(0,0,0,0.35);
}
.poster-quick-add-chip.is-watched { background: var(--warning); color: var(--on-accent); }
.poster-quick-add-chip.is-onlist { background: var(--accent); color: var(--on-accent); }
/* Reserve the bottom strip the action row occupies (#299, #306, #311)
   so the title and meta line (year + source rating) sit above the
   buttons. The reservation fits the pill (~28px tall + 8px bottom
   inset = 36px) plus a small breathing gap. The reservation is
   constant on both touch and desktop so the title position does NOT
   shift when the row fades in on hover (the prior #306 hover-toggled
   value caused a visible vertical jump on every desktop hover, #311).
   Provider icons moved to the top-left in #311 so they no longer eat
   into the bottom strip; the icon cluster sits next to the title only
   if it would otherwise overflow the available width. */
/* #512: reserve space for the 44px button row + 8px inset + gap */
.movie-card.has-quick-add .movie-card-info { padding-bottom: 58px; }
/* #315 dropped the hover-only opacity gating that #259 and #306 had on
   desktop. The row is now permanently visible on every surface — touch
   and desktop alike — because the user wants the action affordances
   discoverable without needing to hover, especially given the Watchlist /
   Watched grids where the primary action (graduate / re-rate) is the
   point of the surface. Layout reservation is the constant 44px above
   so nothing layout-shifts; only this opacity rule was a hover toggle,
   and it's gone. */
/* Bottom-right .provider-cluster (#210) overlaps the pill row's
   bottom-anchored buttons — drop it on .has-quick-add cards (#306).
   #311 relocates the cluster to the top-left on these surfaces (see
   .provider-cluster.top-left below) so the icons replace the prior
   single-name text chip; this rule still hides the bottom-right
   variant. The :not(.top-left) scope keeps the new top-left cluster
   visible. JS render path also short-circuits to skip the bottom-right
   DOM insertion entirely; this rule is the safety net + handles cases
   where the .has-quick-add class is added after the cluster renders. */
.movie-card.has-quick-add .provider-cluster:not(.top-left) { display: none; }
/* On the smallest grid (390pt mobile) the buttons shrink so they fit
   side-by-side without the secondary getting elided. The 140-180px card
   width on mobile leaves ~70px per button after the 50% max-width split,
   so the padding has to be tight to keep "+ Watchlist" / "✓ Watched"
   readable end-to-end without ellipsis. The slimmer buttons also need
   slightly less reserved space below the title. */
@media (max-width: 600px) {
  .poster-quick-add-row { left: 6px; right: 6px; bottom: 6px; gap: 6px; }
  /* #512: font-size slightly smaller on mobile to keep labels untruncated, but
     min-height stays 44px (Apple HIG touch target applies to touch surfaces). */
  .poster-quick-add-btn { font-size: 11px; padding: 10px 6px; }
  .movie-card.has-quick-add .movie-card-info { padding-bottom: 56px; }
}
/* ==========================================================================
   #513 — Long-press card context menu
   ========================================================================== */

/* --- Hold-state visual feedback on the card itself ----------------------- */
.movie-card {
  /* GPU-composited transform so the scale animation doesn't trigger a relayout. */
  will-change: transform;
  /* #517: block iOS text-selection popover and Android long-press selection
     during the 500ms hold — both layers are needed: CSS suppresses selection,
     -webkit-touch-callout suppresses the iOS callout specifically. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
/* Gentle "pulse" while the hold timer is running (dims slightly). */
.movie-card.card-hold-active {
  opacity: 0.85;
  transition: opacity 0.1s ease;
}
/* Brief scale-up to confirm the hold registered before the sheet appears. */
@keyframes card-hold-confirm {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.04); }
  100% { transform: scale(1); }
}
.movie-card.card-hold-confirmed {
  animation: card-hold-confirm 0.2s ease-out;
}

/* --- Backdrop ------------------------------------------------------------- */
.card-context-backdrop {
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: rgba(0, 0, 0, 0);
  -webkit-backdrop-filter: blur(0px);
  backdrop-filter: blur(0px);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  transition: background 0.22s ease, backdrop-filter 0.22s ease, -webkit-backdrop-filter 0.22s ease;
}
.card-context-backdrop--visible {
  background: rgba(0, 0, 0, 0.45);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.card-context-backdrop--dismissing {
  background: rgba(0, 0, 0, 0) !important;
  -webkit-backdrop-filter: blur(0px) !important;
  backdrop-filter: blur(0px) !important;
  pointer-events: none;
}

/* --- Bottom sheet --------------------------------------------------------- */
.card-context-menu {
  width: 100%;
  max-width: 480px;
  background: var(--surface-2, #1c1c1e);
  border-radius: 20px 20px 0 0;
  padding: 0 0 max(20px, env(safe-area-inset-bottom)) 0;
  box-shadow: 0 -4px 32px rgba(0, 0, 0, 0.4);
  transform: translateY(100%);
  transition: transform 0.25s cubic-bezier(0.32, 0.72, 0, 1);
  /* Prevent scroll bleed-through on iOS. */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  max-height: 80svh;
}
.card-context-menu--visible {
  transform: translateY(0);
}
.card-context-menu--dismissing {
  transform: translateY(100%) !important;
}

/* Handle bar */
.card-context-handle {
  width: 36px;
  height: 4px;
  border-radius: 2px;
  background: var(--border, rgba(255,255,255,0.18));
  margin: 12px auto 4px;
}

/* Item title */
.card-context-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted, rgba(255,255,255,0.5));
  text-align: center;
  padding: 4px 20px 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Divider */
.card-context-divider {
  height: 1px;
  background: var(--border, rgba(255,255,255,0.1));
  margin: 4px 0;
}

/* Menu rows */
.card-context-item {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 14px 20px;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text, #fff);
  font-size: 16px;
  font-weight: 500;
  text-align: left;
  min-height: 52px;
  transition: background 0.12s ease;
  -webkit-tap-highlight-color: transparent;
}
.card-context-item:active {
  background: var(--surface-3, rgba(255,255,255,0.08));
}
.card-context-item.danger {
  color: var(--error, #ff453a);
}
.card-context-item-icon {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: var(--surface-3, rgba(255,255,255,0.1));
  color: var(--accent, #d4a017);
}
.card-context-item.danger .card-context-item-icon {
  color: var(--error, #ff453a);
  background: rgba(255, 69, 58, 0.12);
}
.card-context-item-label {
  flex: 1;
  min-width: 0;
}

.modal-info h2 .modal-type-pill {
  display: inline-block;
  vertical-align: middle;
  margin-left: 8px;
  background: var(--vpn-dim);
  color: var(--vpn);
  border: 1px solid var(--vpn);
  font-size: 11px;
  font-weight: 700;
  padding: 3px 8px;
  border-radius: 6px;
  letter-spacing: 0.04em;
}

/* Poster-bottom info overlay (#195) — parallels .swipe-card-info. Title +
   one meta line (type pill + year) sit on a scrim gradient over the cover
   art. Grid cards are poster-only with no below-poster strip; the whole
   card opens the modal where actions live. The dark gradient works across
   all three themes (Watch / Read / Play) because it sits over the cover
   image, not the surface — book covers and game art vary, but white-on-
   dark-scrim clears WCAG AA at any grid scale. */
.movie-card-info {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 28px 12px 10px;
  background: linear-gradient(to top, rgba(0,0,0,0.94) 30%, rgba(0,0,0,0.55) 70%, transparent);
  color: #fff;
  pointer-events: none;
  z-index: 1;
}
.movie-card-title {
  font-size: 15px; font-weight: 700;
  line-height: 1.25;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-shadow: 0 1px 2px rgba(0,0,0,0.6);
}
.movie-card-meta {
  font-size: 11px;
  margin-top: 4px;
  display: flex; gap: 6px; flex-wrap: wrap; align-items: center;
  text-shadow: 0 1px 2px rgba(0,0,0,0.6);
  opacity: 0.92;
}
/* Source rating in the meta line (#261) — TMDB vote_average for movies/TV,
   RAWG/IGDB metacritic for games. Same typography as the year it sits next
   to; no separate chip surface, just a sibling span the meta line's flex
   layout handles like any other entry. Reserved width on the right keeps
   the rating from sliding under the bottom-right .provider-cluster on
   cards that have one (Watch streaming icons / Play platform icons); on
   cards without a cluster (books, no-availability) the meta line uses
   the full width as before. */
.movie-card-rating { white-space: nowrap; }
.movie-card:has(.provider-cluster:not(.top-left)) .movie-card-meta { padding-right: 64px; }
/* #674.7 "Because you liked X" chip. Renders inside .movie-card-info under
   the meta line, opt-out of the parent's `pointer-events: none` so it can
   be tapped to open the seed item's modal. One-line ellipsis keeps the
   chip from blowing out narrow cards on mobile. */
.movie-card-because {
  display: block;
  margin-top: 5px;
  padding: 3px 8px;
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  color: #fff;
  font-size: 10.5px;
  font-weight: 500;
  line-height: 1.3;
  cursor: pointer;
  pointer-events: auto;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: left;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}
.movie-card-because:hover { background: rgba(255, 255, 255, 0.22); }
/* Bottom-right brand cluster (#210) — Watch streaming providers and Play
   parent platforms, anchored over the poster scrim. Replaces the previous
   inline meta-line .platform-icon treatment. Up to 3 icons + a +N overflow
   chip; fits inside the .movie-card-info gradient so legibility holds
   across all three themes without an additional backdrop. */
.provider-cluster {
  position: absolute;
  right: 8px;
  bottom: 8px;
  display: flex;
  gap: 4px;
  align-items: center;
  z-index: 2;
  pointer-events: none;
}
.provider-cluster-icon {
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.55);
  border-radius: 4px;
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.provider-cluster-icon img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
/* Letter fallback (#323) — when a provider/platform doesn't have an icon
   asset available (TMDB returned no logo_path, or a game's parent
   platform isn't in PARENT_BRAND), render its initial inside the same
   chip so the slot still carries identity. Tunes to ~70% of the chip
   for legibility without crowding. Mobile shrinks proportionally to
   match the 20px chip override below. */
.provider-cluster-icon-letter {
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
}
@media (max-width: 600px) {
  .provider-cluster-icon-letter { font-size: 11px; }
}
/* Bootstrap-icons SVGs are monochrome — paint them white via a CSS mask so
   Xbox / Nintendo / Windows show on the dark backdrop (parallels the
   .brand-mask treatment used for store / platform chips elsewhere). */
.provider-cluster-icon .brand-mask {
  width: 14px;
  height: 14px;
  background: white;
  -webkit-mask: var(--brand-mask) center / contain no-repeat;
          mask: var(--brand-mask) center / contain no-repeat;
}
.provider-cluster-icon.owned {
  outline: 1.5px solid var(--success);
  outline-offset: 1px;
}
.provider-cluster-overflow {
  font-size: 10px;
  font-weight: 700;
  color: white;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 4px;
  padding: 2px 5px;
  line-height: 1;
  letter-spacing: 0.02em;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
/* On the smallest grid (390pt mobile) the cluster shrinks to keep poster
   real-estate for the title overlay. */
@media (max-width: 600px) {
  .provider-cluster { gap: 3px; right: 6px; bottom: 6px; }
  .provider-cluster-icon { width: 20px; height: 20px; }
  .provider-cluster-icon .brand-mask { width: 12px; height: 12px; }
  .provider-cluster-overflow { font-size: 9px; padding: 2px 4px; }
}
/* Top-left cluster variant (#311). Replaces the single-name
   .poster-badge.streaming/.vpn text chip on .has-quick-add cards
   (Discover quick-add, Watchlist, Watched). Multiple icons fit in the
   same horizontal slot a single ellipsised provider name was eating,
   and the icons read faster on a glance. The bottom-right variant is
   suppressed on .has-quick-add cards (see :not(.top-left) rule above);
   this is the relocation, not a duplication. The .is-vpn modifier
   tints the wrapper to encode the VPN-required state that the prior
   blue-vs-green text chip carried, and a small leading globe glyph
   makes the VPN-ness scannable without color alone (a11y). Max-width
   matches .poster-badge.left so right-side corner-status badges
   (Watched / On list) on Watchlist + Watched cards don't collide. */
.provider-cluster.top-left {
  top: 8px;
  left: 8px;
  right: auto;
  bottom: auto;
  max-width: calc(55% - 12px);
  flex-wrap: nowrap;
}
.provider-cluster.top-left.is-vpn::before {
  content: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2211%22%20height%3D%2211%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22white%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M12%2021a9.004%209.004%200%200%200%208.716-6.747M12%2021a9.004%209.004%200%200%201-8.716-6.747M12%2021c2.485%200%204.5-4.03%204.5-9S14.485%203%2012%203m0%2018c-2.485%200-4.5-4.03-4.5-9S9.515%203%2012%203m0%200a8.997%208.997%200%200%201%207.843%204.582M12%203a8.997%208.997%200%200%200-7.843%204.582m15.686%200A11.953%2011.953%200%200%201%2012%2010.5c-2.998%200-5.74-1.1-7.843-2.918m15.686%200A8.959%208.959%200%200%201%2021%2012c0%20.778-.099%201.533-.284%202.253m0%200A17.919%2017.919%200%200%201%2012%2016.5c-3.162%200-6.133-.815-8.716-2.247m0%200A9.015%209.015%200%200%201%203%2012c0-1.605.42-3.113%201.157-4.418%22%2F%3E%3C%2Fsvg%3E");
  line-height: 1;
  background: var(--vpn);
  color: var(--on-accent);
  padding: 3px 5px;
  border-radius: 4px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
@media (max-width: 600px) {
  .provider-cluster.top-left { top: 6px; left: 6px; }
  .provider-cluster.top-left.is-vpn::before { padding: 2px 4px; }
}
/* Swipe card top-left cluster (#315). Mirrors the grid card's
   .provider-cluster.top-left convention from #311 so the For You surface
   reads the same as the Watchlist / Watched / Discover grids. Slightly
   larger icons (22px vs 18px) since the swipe card is the focal element.
   The previous in-flow `.swipe-card-info .provider-cluster` rule (icons
   sat under the meta line at the bottom of the gradient) retired in
   favour of this absolute top-left placement. */
.swipe-card .provider-cluster.top-left {
  top: 14px;
  left: 14px;
  gap: 5px;
}
.swipe-card .provider-cluster.top-left .provider-cluster-icon { width: 22px; height: 22px; }
.swipe-card .provider-cluster.top-left .provider-cluster-icon .brand-mask { width: 14px; height: 14px; }
.swipe-card .provider-cluster.top-left .provider-cluster-overflow { font-size: 11px; padding: 3px 6px; }

/* Watched-state dim (#195): apply opacity to the poster's children rather
   than the poster element itself, so the .movie-card-info title overlay
   stays at full opacity (legibility) while the cover art + badges dim. */
.movie-card.is-watched .movie-poster > *:not(.movie-card-info) {
  opacity: 0.78;
  transition: opacity 0.15s;
}
.movie-card.is-watched:hover .movie-poster > *:not(.movie-card-info) {
  opacity: 1;
}
.movie-card.is-watched .movie-poster.no-image::before { opacity: 0.78; }
.movie-card.is-watched:hover .movie-poster.no-image::before { opacity: 1; }

.empty-state {
  text-align: center;
  padding: 80px 24px;
  color: var(--text-dim);
}
.empty-state-icon {
  font-size: 48px;
  margin-bottom: 12px;
  opacity: 0.5;
  display: flex; justify-content: center;
}
.empty-state-icon svg { width: 56px; height: 56px; }
.empty-state-title {
  font-size: 22px; font-weight: 600;
  color: var(--text);
  margin-bottom: 6px;
}
.empty-state-cta {
  display: inline-flex; align-items: center; gap: 4px;
  margin-top: 20px;
  padding: 10px 20px;
  border-radius: 10px;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 14px; font-weight: 600;
  cursor: pointer;
  transition: background 0.15s, opacity 0.15s;
}
.empty-state-cta:hover { background: var(--accent-hover); }

/* #183 — three-CTA empty-state row. Wraps to a vertical stack on narrow
   viewports; the primary CTA keeps the filled accent, secondary CTAs are
   outline so the visual hierarchy stays "Discover first". */
.empty-state-cta-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 20px;
}
.empty-state-cta-row .empty-state-cta { margin-top: 0; }
.empty-state-cta-secondary {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--accent);
}
.empty-state-cta-secondary:hover {
  background: var(--accent);
  color: var(--on-accent);
}
@media (max-width: 480px) {
  .empty-state-cta-row {
    flex-direction: column;
    align-items: stretch;
  }
}

/* #941: wrapper for a Discover error emptyState + inline Retry button.
   Centering mirrors the existing .empty-state-cta-row pattern. */
.discover-error-state {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.discover-error-state .discover-retry-btn {
  margin-top: 16px;
}

.modal-overlay {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(0,0,0,0.75);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  opacity: 0;
  pointer-events: none;
  /* #999: closed overlays must NOT stay rendered. opacity:0 alone keeps all
     ~22 of them alive as full-viewport backdrop-filter surfaces (and keeps
     animations inside them compositing invisibly), which starves the Android
     WebView compositor's tile memory and blacks out panel content.
     visibility:hidden drops the paint/composite cost while preserving layout
     (closed-state measurement still works, unlike display:none). The 0.3s
     visibility delay on close lets the 0.2s opacity fade and the 0.25s
     bottom-sheet slide-out finish before the surface stops rendering. */
  visibility: hidden;
  transition: opacity 0.2s, visibility 0s linear 0.3s;
}
.modal-overlay.visible {
  opacity: 1;
  pointer-events: auto;
  /* #999: visibility is not in this transition list, so it flips to visible
     instantly on open; the opacity fade-in plays on a rendered surface. */
  visibility: visible;
  transition: opacity 0.2s;
}
/* #833 — legal modals must stack above every surface they can be opened
   from: onboarding (200), settings (200), and sheets up through 260
   (#importsModal/#personSheet 220, #rateModal/#pwaInstallModal 250,
   .party-name-prompt-overlay 260). Toasts (.toast 300) and
   banners/tutorial/notification-inbox (1000-1001) stay above. */
#termsModal, #privacyModal { z-index: 270; }
.modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 16px;
  max-width: 800px; width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  overflow-x: hidden;
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-3);
}
/* #1012 CLS fix: reserve the detail modal's final height for the full open
   lifecycle so the card never grows-and-recenters from the loading skeleton
   to the full card (which was 0.30 of the 0.46 CLS score). The min-height is
   set when the modal opens (modal-detail-open class) and cleared on close so
   rare short modals don't leave trailing whitespace permanently. */
#movieModal.modal-detail-open .modal {
  min-height: min(90vh, calc(100dvh - 48px));
}
.modal-close {
  position: absolute; top: 12px; right: 12px; z-index: 10;
  width: 36px; height: 36px;
  border-radius: 8px;
  background: rgba(0,0,0,0.6);
  display: flex; align-items: center; justify-content: center;
  color: white;
  font-size: 16px;
}
.modal-close:hover { background: rgba(0,0,0,0.9); }

/* Bottom-sheet drag handle (#91). On mobile (≤720px) the movie / rate /
   settings modals render as bottom sheets that slide up from the bottom
   edge; the handle is the grab affordance for drag-to-dismiss and the
   tap target for toggling between peek (60%) and full (90%) snap-points.
   Hidden entirely on desktop — the modals stay centered dialogs there. */
.bottom-sheet-handle {
  display: none;
}
.bottom-sheet-handle::before {
  content: '';
  width: 36px;
  height: 4px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.55);
}

/* Floating close — child of .modal that sticks to the top-right of the card
   itself (not the viewport) so it stays on the card on wide screens and
   stays visible while the modal content scrolls. The negative margin-bottom
   reclaims the vertical space the button would otherwise reserve, so
   #modalContent still starts at the top of the card. */
.modal-close-floating {
  position: sticky;
  top: 12px;
  align-self: flex-end;
  margin: 12px 12px -48px 0;
  z-index: 11;
  width: 40px; height: 40px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.12);
  font-size: 18px;
  color: white;
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow-2);
  transition: background 0.15s, transform 0.1s;
  cursor: pointer;
  flex-shrink: 0;
}
.modal-close-floating:hover {
  background: rgba(0, 0, 0, 0.95);
  transform: scale(1.08);
}
.modal-close-floating:active { transform: scale(0.96); }

/* Floating share + close row (#149). Sticky flex-row holding both buttons so
   they share the same top-edge and never collide. Negative margin-bottom
   reclaims the row's height so #modalContent still starts flush with the
   top of the card, with the buttons floating over the first ~52px of it. */
/* Canonical detail-overlay float-row (#571) — all chrome buttons (share,
 * block, close, …) cluster on the **top-right** with an 8px gap between
 * them. Close is always the rightmost element so the user's eye lands in
 * the same corner across every overlay (movie modal, person sheet, profile
 * modal). Matches iOS HIG + Material + Netflix/Disney+/Apple Music. New
 * detail-style overlays should reuse this exact markup: render any chrome
 * buttons in source order with `.modal-close-floating-inrow` last so it
 * anchors the right edge. */
.modal-float-row {
  position: sticky;
  top: 0;
  z-index: 11;
  display: flex;
  justify-content: flex-end;
  align-items: flex-start;
  gap: 8px;
  padding: 12px 12px 0;
  margin-bottom: -52px;
  pointer-events: none;
}
.modal-float-row > * { pointer-events: auto; }
.modal-icon-floating,
.modal-close-floating-inrow {
  /* Override `.modal-close { position: absolute; top; right }` so close
     stays inside the flex row alongside share/block, instead of detaching
     and floating over the rightmost button. */
  position: static;
  top: auto;
  right: auto;
  width: 40px; height: 40px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.12);
  color: white;
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow-2);
  transition: background 0.15s, transform 0.1s;
  cursor: pointer;
  flex-shrink: 0;
  padding: 0;
}
.modal-icon-floating:hover,
.modal-close-floating-inrow:hover {
  background: rgba(0, 0, 0, 0.95);
  transform: scale(1.08);
}
.modal-icon-floating:active,
.modal-close-floating-inrow:active { transform: scale(0.96); }

.modal-backdrop {
  height: 280px;
  background-size: cover; background-position: center;
  background-color: var(--surface-2);
  position: relative;
}
.modal-backdrop::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(to bottom, rgba(20,20,31,0) 0%, var(--surface) 95%);
}
.modal-backdrop.playing {
  height: 0;
  padding-bottom: 56.25%;
  background: #000 !important;
  background-image: none !important;
}
.modal-backdrop.playing::after { display: none; }
.modal-backdrop.playing iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0;
  z-index: 1;
}
.trailer-fallback {
  position: absolute;
  bottom: 8px; right: 8px;
  background: rgba(0, 0, 0, 0.78);
  color: white;
  padding: 5px 10px;
  font-size: 11px;
  font-weight: 500;
  border-radius: 6px;
  text-decoration: none;
  z-index: 2;
  opacity: 0.85;
  transition: opacity 0.15s, background 0.15s;
}
.trailer-fallback:hover { opacity: 1; background: rgba(0, 0, 0, 0.95); }
.modal:has(.modal-backdrop.playing) .modal-body { margin-top: 16px; }
.action-trailer {
  background: rgba(255, 0, 0, 0.12) !important;
  color: #ff5555 !important;
  border-color: rgba(255, 0, 0, 0.3) !important;
  font-weight: 600;
}
.action-trailer:hover {
  background: rgba(255, 0, 0, 0.22) !important;
  border-color: #ff5555 !important;
}
.action-watched-state {
  background: rgba(251, 191, 36, 0.12) !important;
  color: var(--warning) !important;
  border-color: rgba(251, 191, 36, 0.3) !important;
  font-weight: 600;
}
.action-watched-state:hover {
  background: rgba(251, 191, 36, 0.22) !important;
  border-color: var(--warning) !important;
}
/* .modal has z-index auto but is flex child of .modal-overlay (flex;
#swipeView {
  position: relative;
}

/* Tinder-style swipe deck (For You → Swipe mode) */
.swipe-stack {
  position: relative;
  /* #972/#979: explicit z-index:1 keeps the stack above any backdrop layer
     behind the card even when both are positioned inside #swipeView */
  z-index: 1;
  width: 100%;
  /* #777 desktop fit: cap the card so its height (= width * 3/2, via the
     aspect-ratio below) never exceeds the viewport minus the chrome above
     (topbar + tabs + For-You toolbar) and the action row + hint below it
     (~360px total). Without this the 380px card is ~570px tall and pushes
     the rate / skip / add row below the fold on a laptop viewport. The 380px
     cap keeps the full-size card on tall screens; on shorter ones the card
     scales down proportionally so the controls stay in view. The
     max-width:600px block overrides this with its own mobile math. */
  max-width: min(380px, calc((100dvh - 360px) * 2 / 3));
  aspect-ratio: 2/3;
  margin: 0 auto;
  user-select: none;
  /* All gestures inside the stack belong to the swipe handler — never let the
     browser interpret a vertical drag as a page scroll. */
  touch-action: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  overscroll-behavior: contain;
}
.swipe-card {
  position: absolute;
  inset: 0;
  border-radius: 18px;
  overflow: hidden;
  cursor: grab;
  background: var(--surface-2);
  background-size: cover;
  background-position: center;
  box-shadow: var(--shadow-3);
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s;
  will-change: transform, opacity;
  touch-action: none;
  perspective: 1100px;
  z-index: 1;
}
.swipe-card.dragging { cursor: grabbing; transition: none; }
.swipe-card.gone-right {
  transform: translate(150%, -10%) rotate(28deg);
  opacity: 0;
  transition: transform 0.4s ease-out, opacity 0.3s;
}
.swipe-card.gone-left {
  transform: translate(-150%, -10%) rotate(-28deg);
  opacity: 0;
  transition: transform 0.4s ease-out, opacity 0.3s;
}
.swipe-card.gone-up {
  transform: translate(0, -150%) rotate(0);
  opacity: 0;
  transition: transform 0.4s ease-out, opacity 0.3s;
}
/* #974 — Real deck stack: .behind card peeks under the active card.
   --deck-progress (0..1) is set by bindSwipeGestures on the .swipe-stack
   during drag so the behind card interpolates toward identity as the active
   card is dragged away. CSS-driven: no per-frame JS style math on this card.
   Under prefers-reduced-motion the behind card simply sits at rest (the
   transition and calc() interpolation are both skipped by the @media block). */
.swipe-card.behind {
  transform: scale(calc(0.95 + 0.05 * var(--deck-progress, 0)))
             translateY(calc(8px * (1 - var(--deck-progress, 0))));
  filter: brightness(calc(0.85 + 0.15 * var(--deck-progress, 0)));
  pointer-events: none;
  transition: transform 0.2s ease-out, filter 0.2s ease-out;
  z-index: 0;
}
@media (prefers-reduced-motion: reduce) {
  .swipe-card.behind {
    transform: scale(0.95) translateY(8px);
    filter: brightness(0.85);
    transition: none;
  }
}
/* #681 Undo restore — start the card off-stage in the direction it left,
   then drop the class on the next frame so the existing 0.3s cubic-bezier
   transition runs it back to center. Mirrors the gone-* exit animations
   so the user sees the card visibly come back. */
.swipe-card.restore-from-left {
  transform: translate(-150%, -10%) rotate(-28deg);
  opacity: 0;
  transition: none;
}
.swipe-card.restore-from-right {
  transform: translate(150%, -10%) rotate(28deg);
  opacity: 0;
  transition: none;
}
.swipe-card.restore-from-up {
  transform: translate(0, -150%) rotate(0);
  opacity: 0;
  transition: none;
}
.swipe-card-info {
  position: absolute; bottom: 0; left: 0; right: 0;
  padding: 28px 22px 22px;
  background: linear-gradient(to top, rgba(0,0,0,0.96) 30%, rgba(0,0,0,0.6) 70%, transparent);
  color: white;
}
.swipe-card-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; line-height: 1.2; }
.swipe-card-meta {
  font-size: 12px;
  opacity: 0.85;
  margin-bottom: 8px;
  display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
}
.swipe-card-overview {
  font-size: 13px;
  line-height: 1.5;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  opacity: 0.9;
}
.swipe-card-typepill {
  background: rgba(56, 189, 248, 0.85);
  color: #0A0A10;
  font-size: 10px;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 4px;
}
.swipe-card-info-btn {
  position: absolute;
  bottom: 14px; right: 14px;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  display: flex; align-items: center; justify-content: center;
  color: white;
  font-family: Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-size: 17px;
  font-weight: 700;
  cursor: pointer;
  z-index: 5;
  border: 1px solid rgba(255, 255, 255, 0.32);
  user-select: none;
  transition: background 0.15s, transform 0.1s;
}
.swipe-card-info-btn:hover {
  background: rgba(0, 0, 0, 0.78);
  transform: scale(1.08);
}
.swipe-card-info-btn:active { transform: scale(0.94); }

.swipe-card-pill {
  position: absolute;
  top: 24px;
  font-size: 30px; font-weight: 900;
  letter-spacing: 2px;
  padding: 6px 14px;
  border-radius: 10px;
  border: 4px solid;
  opacity: 0;
  transition: opacity 0.1s;
  pointer-events: none;
  text-transform: uppercase;
  white-space: nowrap;
  z-index: 2;
}
.swipe-card-pill.like {
  right: 24px; color: #4ade80; border-color: #4ade80; transform: rotate(15deg);
}
.swipe-card-pill.skip {
  left: 24px; color: #ef4444; border-color: #ef4444; transform: rotate(-15deg);
}
.swipe-card-pill.watched {
  top: 24px; left: 50%; color: var(--warning); border-color: var(--warning);
  transform: translateX(-50%);
}
.swipe-actions {
  display: flex; justify-content: center; gap: 22px;
  margin-top: 24px;
}
.swipe-btn {
  width: 60px; height: 60px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 24px;
  font-weight: 700;
  background: var(--surface);
  border: 2px solid var(--border);
  transition: transform 0.1s, background 0.15s, box-shadow 0.15s;
  cursor: pointer;
}
.swipe-btn.skip { color: var(--danger); border-color: var(--danger); }
.swipe-btn.watchlist { color: var(--accent); border-color: var(--accent); }
.swipe-btn.watched { color: var(--warning); border-color: var(--warning); }
/* #681 Undo button: a quieter secondary action — smaller, neutral color,
   sits at the start of the action row so it never competes visually with
   the three primary swipe actions. Disabled state fades out fully so an
   empty history doesn't look like a tappable affordance. */
.swipe-btn.undo {
  width: 44px; height: 44px;
  font-size: 18px;
  color: var(--text-muted);
  border-color: var(--border);
  align-self: center;
  margin-right: 6px;
}
.swipe-btn.undo:hover:not(:disabled) {
  color: var(--text);
  border-color: var(--text-dim);
  box-shadow: 0 4px 16px rgba(0,0,0,0.18);
}
/* Discovery pulse: draws attention to the undo button the first time
   it becomes enabled after a swipe commit. Added/removed by JS. */
@keyframes undo-pulse {
  0%   { box-shadow: 0 0 0 0px color-mix(in srgb, var(--accent) 50%, transparent); }
  60%  { box-shadow: 0 0 0 8px transparent; }
  100% { box-shadow: 0 0 0 0px transparent; }
}
.swipe-btn.undo.undo-pulse {
  animation: undo-pulse 0.6s ease-out;
}
.swipe-btn:hover:not(:disabled) { transform: scale(1.1); box-shadow: 0 4px 16px color-mix(in srgb, currentColor 25%, transparent); }
.swipe-btn:active:not(:disabled) { transform: scale(0.92); }
.swipe-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; box-shadow: none; }
.swipe-hint {
  text-align: center;
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 16px;
}
.swipe-empty {
  text-align: center;
  padding: 60px 20px;
  color: var(--text-dim);
}
.swipe-empty-title { color: var(--text); font-weight: 600; font-size: 16px; margin-bottom: 6px; }
.swipe-empty button {
  margin-top: 16px;
  padding: 10px 20px;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: 8px;
  font-weight: 500;
  border: none;
  cursor: pointer;
}

@media (max-width: 600px) {
  /* Bound the entire #swipeView to the available viewport height so the
     actions row and hint always sit above the fixed tab bar. This mirrors
     the .party-swipe approach: the parent is bounded by an explicit height
     calc, .swipe-stack grows to fill whatever actions+hint leave behind, and
     nothing overflows into the tab bar's z-index. Chrome budget: header+topbar
     (~60px) + main padding-top (~14px) + For-You toolbar (~52px) + bottom
     clearance for fixed tabs incl. safe-area (~80px). The safe-area-inset-top
     guards against the notch on iPhone models. */
  #swipeView {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0;
    height: calc(
      100dvh
      - 60px
      - 14px
      - 52px
      - 80px
      - env(safe-area-inset-top, 0px)
      - env(safe-area-inset-bottom, 0px)
    );
    min-height: 0;
  }
  /* Stack grows to fill whatever the actions row and hint leave behind. */
  .swipe-stack {
    flex: 1 1 0;
    min-height: 0;
    aspect-ratio: 2/3;
    width: auto;
    max-width: min(380px, 100%);
    height: auto;
    max-height: 100%;
    overflow: hidden;
    /* Remove the old hardcoded height formula; height is now derived from
       the flex parent's bounded height minus siblings' natural sizes. */
  }
  .swipe-card-title { font-size: 19px; }
  .swipe-card-info { padding: 22px 18px 18px; }
  .swipe-actions { gap: 16px; margin-top: 14px; flex-shrink: 0; }
  .swipe-btn { width: 54px; height: 54px; font-size: 22px; }
  .swipe-btn.undo { width: 40px; height: 40px; font-size: 16px; margin-right: 2px; }
  .swipe-hint { margin-top: 10px; font-size: 11px; flex-shrink: 0; }
}

/* Cast & crew */
.cast-section {
  padding: 0 24px 8px;
}
.cast-section h3 {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  margin-bottom: 12px;
}
.cast-strip {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* overflow-x: auto forces overflow-y to auto per spec, so the hover
     lift + shadow halo would clip against the content box without
     symmetric vertical padding here. */
  padding: 8px 0;
}
.cast-strip::-webkit-scrollbar { display: none; }
.cast-card {
  flex: 0 0 90px;
  display: flex;
  flex-direction: column;
  text-decoration: none;
  text-align: left;
  color: inherit;
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  transition: transform 0.15s ease;
  min-height: 44px;
}
.cast-card:hover { transform: translateY(-2px); }
.cast-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 8px;
}
.cast-photo {
  width: 90px; height: 110px;
  border-radius: 8px;
  background-size: cover;
  background-position: center top;
  background-color: var(--surface-2);
  border: 1px solid var(--border);
  margin-bottom: 6px;
  position: relative;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.cast-photo.no-image::after {
  content: '👤';
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 32px; opacity: 0.4;
}
.cast-card:hover .cast-photo {
  border-color: var(--accent);
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.28);
}
.cast-card:hover .cast-name { color: var(--accent); }
.cast-name {
  font-size: 12px;
  font-weight: 600;
  line-height: 1.25;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  transition: color 0.15s ease;
}
.cast-role {
  font-size: 11px;
  color: var(--text-dim);
  line-height: 1.2;
  margin-top: 2px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

@media (max-width: 600px) {
  .cast-section { padding: 0 16px 4px; }
  .cast-card { flex: 0 0 76px; }
  .cast-photo { width: 76px; height: 96px; }
}

/* Cinema / showtimes */
.cinema-row {
  display: flex; gap: 10px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.cinema-btn {
  padding: 10px 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, transform 0.1s;
  display: flex; align-items: center; gap: 8px;
}
.cinema-btn:hover {
  background: var(--surface-3);
  border-color: var(--accent);
}
.cinema-btn:active { transform: translateY(1px); }
.cinema-btn:disabled { opacity: 0.6; cursor: wait; }
.cinema-btn-icon { font-size: 16px; }

/* Similar movies */
.similar-section {
  padding: 20px 24px 24px;
  border-top: 1px solid var(--border);
}
.similar-section h3 {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  margin-bottom: 14px;
}
.similar-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 12px;
}
.similar-card {
  cursor: pointer;
  display: flex; flex-direction: column;
  gap: 6px;
  min-width: 0;
}
.similar-poster {
  aspect-ratio: 2/3;
  border-radius: 8px;
  background-size: cover;
  background-color: var(--surface-2);
  background-position: center;
  border: 1px solid var(--border);
  transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
  position: relative;
}
.similar-card:hover .similar-poster {
  transform: scale(1.04);
  border-color: var(--accent);
  box-shadow: 0 4px 14px rgba(255, 77, 109, 0.2);
}
.similar-title {
  font-size: 12px;
  font-weight: 500;
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.similar-year {
  font-size: 11px;
  color: var(--text-dim);
}
.similar-badge {
  position: absolute;
  top: 4px; right: 4px;
  padding: 2px 6px;
  font-size: 10px;
  font-weight: 700;
  border-radius: 4px;
  background: rgba(0,0,0,0.78);
  color: white;
}
.similar-badge.watched { background: var(--warning); color: var(--on-accent); }
.similar-badge.onlist { background: var(--accent); color: var(--on-accent); }
.modal-body {
  padding: 24px;
  margin-top: -120px;
  position: relative; z-index: 2;
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: 24px;
}
.modal-poster {
  position: relative;
  aspect-ratio: 2/3;
  border-radius: 12px;
  background-size: cover;
  background-color: var(--surface-2);
  box-shadow: var(--shadow-3);
  overflow: hidden;
  perspective: 1100px;
}
.modal-info h2 {
  font-size: 26px;
  margin-bottom: 6px;
  line-height: 1.2;
}
.modal-meta {
  font-size: 14px; color: var(--text-dim);
  margin-bottom: 14px;
  display: flex; gap: 8px; flex-wrap: wrap;
  align-items: center;
}
.modal-meta-dot { color: var(--text-muted); }

/* Edition selector strip — shown in game modals when multiple editions of the
   same title are on the user's list (#151). */
.edition-strip {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin: 6px 0 12px;
}
.edition-chip {
  background: var(--surface-2);
  border: 1px solid var(--border, rgba(255,255,255,0.12));
  border-radius: 20px;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 12px; font-weight: 500;
  line-height: 1;
  padding: 5px 12px;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.edition-chip.active {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}
.edition-chip:not(.active):hover {
  border-color: var(--text-muted); color: var(--text);
}
/* Card-level edition selector strip — one chip per edition in the bundle so
   users can switch from the grid without opening the modal (#151/#152).
   Single-line layout: if the full chip set would overflow the card width,
   JS in renderMovieGrid swaps the strip for a single ".is-summary" chip
   ("N editions") so nothing is ever clipped mid-chip. */
.movie-card-editions-strip {
  display: flex; flex-wrap: nowrap; gap: 4px;
  margin-top: 6px;
  min-width: 0;
  overflow: hidden;
}
.movie-card-edition-chip {
  background: var(--surface-2);
  border: 1px solid var(--border, rgba(255,255,255,0.12));
  border-radius: 12px;
  color: var(--text-dim);
  cursor: pointer;
  flex: 0 0 auto;
  font-size: 10px; font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1;
  padding: 4px 8px;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
  white-space: nowrap;
}
.movie-card-edition-chip.is-summary {
  cursor: inherit;
  color: var(--accent);
  border-color: var(--accent);
  background: transparent;
}
.movie-card-edition-chip.active {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}
.movie-card-edition-chip:not(.active):hover {
  border-color: var(--text-muted); color: var(--text);
}
.modal-overview {
  font-size: 14px; line-height: 1.6;
  color: var(--text-dim);
  margin-bottom: 20px;
}

/* Item notes / mini-reviews (#179). Rendered under the modal action row when
 * the item is in watchlist or watched. Auto-saves on blur and 450ms after
 * the last keystroke; the live counter shows remaining chars (1000 max).
 *
 * Collapsed by default since #557 — a compact summary row carries the
 * existing note's preview (or an "Add a note" CTA when empty); the textarea
 * + public-toggle + counter are revealed when the user opens the disclosure.
 */
.item-note-section {
  padding: 0 24px 16px;
}
.item-note-details {
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  overflow: hidden;
}
.item-note-details[open] {
  background: var(--surface-2);
}
.item-note-summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  cursor: pointer;
  list-style: none;
  user-select: none;
  font-size: 13px;
  color: var(--text-dim);
  min-height: 24px;
}
.item-note-summary::-webkit-details-marker { display: none; }
.item-note-summary:hover { color: var(--text); }
.item-note-summary-icon {
  font-size: 13px;
  color: var(--text-muted);
  flex-shrink: 0;
}
.item-note-summary-label {
  font-weight: 600;
  color: var(--text);
  flex-shrink: 0;
}
.item-note-summary-preview {
  color: var(--text-muted);
  font-style: italic;
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
}
.item-note-summary-preview:empty { display: none; }
.item-note-summary-chevron {
  font-size: 18px;
  line-height: 1;
  color: var(--text-muted);
  transition: transform 0.18s;
  flex-shrink: 0;
}
.item-note-details[open] .item-note-summary-chevron {
  transform: rotate(90deg);
}
.item-note-body {
  padding: 4px 12px 12px;
}
.item-note-label {
  display: block;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  margin-bottom: 6px;
}
.item-note-input {
  width: 100%;
  min-height: 64px;
  resize: vertical;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 10px;
  font-size: 13px;
  line-height: 1.45;
  font-family: inherit;
  box-sizing: border-box;
}
.item-note-input:focus {
  outline: none;
  border-color: var(--accent);
}
.item-note-foot {
  display: flex; justify-content: space-between; align-items: center;
  gap: 10px; flex-wrap: wrap;
  margin-top: 4px;
  font-size: 11px;
  color: var(--text-dim);
}
.item-note-status { font-style: italic; }
.item-note-counter { font-variant-numeric: tabular-nums; }
.item-note-public-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  cursor: pointer; user-select: none;
}
.item-note-public-toggle input[type="checkbox"] {
  width: 13px; height: 13px;
  accent-color: var(--accent);
}

/* Report-sheet reason block (#803) -- mirrors .item-note-input / .item-note-counter */
.report-reason-block {
  padding: 12px 20px 8px;
}
.report-reason-label {
  display: block;
  font-size: 12px;
  color: var(--text-dim);
  margin-bottom: 6px;
}
.report-reason-input {
  width: 100%;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 10px;
  font-size: 13px;
  line-height: 1.45;
  font-family: inherit;
  box-sizing: border-box;
  resize: none;
}
.report-reason-input:focus {
  outline: none;
  border-color: var(--accent);
}
.report-reason-counter {
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--text-dim);
  text-align: right;
  margin-top: 4px;
}
.report-reason-counter--warn {
  color: var(--warning);
}

.providers-section { padding: 0 24px 24px; }
.providers-section h3 {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  margin-bottom: 12px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  display: flex; align-items: center; gap: 8px;
}
.providers-section h3:first-child { padding-top: 0; border-top: none; }

/* Primary providers section (#557) — the first thing the user sees after the
 * modal hero. This is the app's headline answer ("where can I watch / read /
 * play this?") so give the first heading a touch more weight without dragging
 * other section heads along. */
.providers-section-primary h3:first-child {
  font-size: 14px;
  color: var(--text);
  letter-spacing: 0.05em;
  margin-bottom: 14px;
}
.providers-section-primary h3:first-child .h3-meta {
  color: var(--text-dim);
}
/* #1012 CLS fix: while the providers loading row is present, reserve enough
   vertical space so the real provider chips (which are taller) don't shift
   the cast strip down. The :has guard removes the reservation automatically
   once hydrateMovieExtras swaps in real content (no .loading-inline present).
   ~180px covers the common case: one subscribed-chips row + one streaming row.
   No-provider titles never show a loading row so the min-height never applies. */
.providers-section-primary:has(.loading-inline) {
  min-height: 180px;
}
.providers-section h3 .h3-meta {
  color: var(--text-muted);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  font-size: 11px;
}
.providers-section h3 .h3-tag {
  background: var(--success-dim);
  color: var(--success);
  padding: 2px 8px;
  border-radius: 6px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
}
.providers-section h3 .h3-tag.vpn {
  background: var(--vpn-dim);
  color: var(--vpn);
}
.providers-row {
  display: flex; gap: 10px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.providers-row:last-child { margin-bottom: 0; }
.provider-chip {
  display: flex; align-items: center; gap: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  padding: 8px 12px;
  border-radius: 10px;
  font-size: 13px;
}
.provider-chip.subscribed {
  border-color: var(--success);
  background: var(--success-dim);
}
.provider-chip.vpn {
  border-color: var(--vpn);
  background: var(--vpn-dim);
}
.provider-chip-logo {
  width: 28px; height: 28px;
  border-radius: 6px;
  background-size: cover;
  flex-shrink: 0;
}
/* Brand-mono variant for trimmed transparent brand SVGs (Steam, PlayStation,
   Apple, Xbox, Nintendo, …): smaller than TMDB logos, no rounded square,
   contain-not-cover so SVGs aren't cropped. Two sources, two markups:
   - simpleicons (colored): rendered as <img>, brand color baked in.
   - bootstrap-icons (monochrome SVG with fill="currentColor"): rendered as
     a <span> with mask-image so we can paint it currentColor / theme-aware. */
.provider-chip-logo.brand-mono {
  width: 22px; height: 22px;
  border-radius: 0;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  display: inline-block;
}
.provider-chip-logo.brand-mono.brand-mask {
  background-color: currentColor;
  background-image: none;
  -webkit-mask: var(--brand-mask) center / contain no-repeat;
          mask: var(--brand-mask) center / contain no-repeat;
}
img.provider-chip-logo.brand-mono { object-fit: contain; }

.modal-actions {
  display: flex; gap: 8px;
  margin-top: 16px;
  flex-wrap: wrap;
}
.modal-actions button {
  padding: 10px 16px;
  font-size: 14px; font-weight: 500;
  border-radius: 8px;
  border: 1px solid var(--border);
  color: var(--text);
  transition: background 0.15s;
  display: flex; align-items: center; gap: 6px;
}
.modal-actions button:hover { background: var(--surface-2); }

/* Settings modal */
.settings-modal {
  max-width: 640px;
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  max-height: 90vh;
}
.settings-modal-scroll {
  padding: 28px 32px 24px;
  overflow-y: auto;
  /* overflow-y: auto implicitly turns overflow-x into auto as well, which lets
     a too-wide flex row inside the modal (e.g. the account email + "Send magic
     link" row in long-string locales) start panning the whole modal sideways.
     Pin overflow-x to hidden so only vertical scrolling is possible. */
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  flex: 1 1 auto;
  min-height: 0;
}
.settings-modal-close {
  position: absolute;
  top: 12px; right: 12px;
  z-index: 11;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: background 0.15s, transform 0.1s;
}
.settings-modal-close:hover { background: var(--surface-3); transform: scale(1.06); }
.settings-modal-close:active { transform: scale(0.96); }
.settings-modal h2 {
  font-size: 22px;
  margin-bottom: 10px;
}
/* Settings subtitle (#302): styled as a small info chip rather than a bare
   <p>, since the previous `.settings-modal > p.subtitle` selector never
   matched (the <p> is nested inside `.settings-modal-scroll`, not a direct
   child of `.settings-modal`) so the line was shipping unstyled and
   crammed against the content below. */
.settings-modal-scroll .subtitle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 22px;
  padding: 6px 12px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--text-dim);
  font-size: 12.5px;
  line-height: 1.3;
  max-width: 100%;
}
.settings-modal-scroll .subtitle svg {
  flex-shrink: 0;
  opacity: 0.7;
}
/* Settings sections rendered as cards (#558): each section gets its own
   rounded surface-2 plate, replacing the previous hairline-top separator
   style that read as one undifferentiated wall. The parent `.settings-group`
   is flex-column with `gap: 14px` (set on the active-group rule lower in the
   file) so cards stack with consistent rhythm. `.about-section` keeps its
   inner-zero padding via `padding: 0 !important;` and overrides this card's
   padding to host its own hero / attribution / action sub-cards. */
.settings-section {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 18px 20px;
}
.settings-section h3 {
  font-size: 15px;
  font-weight: 700;
  margin-bottom: 4px;
}
.settings-section p.section-desc {
  color: var(--text-dim);
  font-size: 13px;
  margin-bottom: 14px;
}
/* The section card itself is now var(--surface-2); inner elements that were
   also surface-2 (import-card, region-pill, provider-chip, platform-pill,
   segmented controls, search inputs) would visually merge into the card.
   Bump them one step to var(--surface-3) when nested in a settings card so
   they remain distinguishable. (#558) */
.settings-section .import-card,
.settings-section .region-pill,
.settings-section .provider-chip,
.settings-section .platform-pill,
.settings-section .segmented,
.settings-section .settings-filter-input,
.settings-section .account-info,
.settings-section .profile-signin-hint,
.settings-section input[type="text"],
.settings-section input[type="email"],
.settings-section input[type="number"],
.settings-section input[type="search"],
.settings-section select,
.settings-section textarea {
  background: var(--surface-3);
}
.settings-section .import-card:hover,
.settings-section .region-pill:hover {
  background: color-mix(in srgb, var(--accent) 14%, var(--surface-3));
}
.settings-section input[type="text"] {
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
}
.api-row {
  display: flex; gap: 8px;
  flex-wrap: wrap;
}
.api-row input { flex: 1 1 180px; min-width: 0; }
.api-row button {
  padding: 10px 16px;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: 8px;
  font-weight: 500;
  white-space: nowrap;
  transition: background 0.15s;
}
.api-row button:hover { background: var(--accent-hover); }
.api-row button:disabled { opacity: 0.6; cursor: wait; }
.error-msg { color: #f87171; font-size: 13px; margin-top: 8px; }
.success-msg { color: var(--success); font-size: 13px; margin-top: 8px; }

.settings-filter-input {
  width: 100%;
  padding: 8px 12px;
  font-size: 13px;
  background: var(--surface);
  margin-bottom: 10px;
  /* Pre-1.7.38 this was position: sticky; top: 0; z-index: 5 so the search
     bar followed the scroll. The pinned bar read as "locked" — the user
     asked to remove it. Now it scrolls with the rest of the settings
     section, which is the standard iOS / Material settings pattern. */
}
.vpn-regions-mode { margin-bottom: 12px; }
/* Theme picker (#250, #451): 4-option segmented control. With 4 buttons inside
   the ~360–600px Settings content panel — and longer locale labels (es
   "Predeterminado", de "Automatisch") — `flex: 1 1 80px` packed all four
   buttons to ~equal width (~90px on iPhone) and let the longest label
   overflow its slot because `min-width: 0` allows flex items to shrink
   below their intrinsic content. The `.segmented` mobile override
   (`flex: 1`, source-order winner at same specificity) made it worse on
   iPhone. Selector raised to `.segmented.theme-mode button` so its
   content-driven flex wins everywhere; `flex: 1 0 auto` keeps each
   button at least as wide as its own text and lets the row wrap to a 2x2
   when the four labels can't fit one row (the safety net). */
.segmented.theme-mode { width: 100%; flex-wrap: wrap; }
.segmented.theme-mode button {
  flex: 1 0 auto;
  min-width: 0;
  padding: 6px 8px;
  text-align: center;
  white-space: nowrap;
}
.settings-filter-input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  height: 14px; width: 14px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'><path d='M11 3 3 11M3 3l8 8' stroke='%239090a8' stroke-width='1.5' fill='none' stroke-linecap='round'/></svg>");
  cursor: pointer;
}
.provider-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 8px;
  padding: 4px 4px 4px 0;
}
/* "Show all (N more)" button that lives inside the grid as a full-width row */
.provider-show-more {
  grid-column: 1 / -1;
  padding: 10px;
  background: transparent;
  border: 1px dashed var(--border);
  border-radius: 6px;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.provider-show-more:hover {
  background: var(--surface-2);
  color: var(--text);
  border-color: var(--accent);
}
.picker-bulk-actions {
  display: flex; gap: 8px;
  margin-bottom: 10px;
}
.picker-bulk-actions button {
  padding: 6px 12px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-dim);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.picker-bulk-actions button:hover {
  background: var(--surface-3);
  color: var(--text);
  border-color: var(--accent);
}
.provider-pill {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
  font-size: 13px;
  user-select: none;
}
.provider-pill:hover { border-color: var(--accent); }
.provider-pill.selected {
  background: var(--success-dim);
  border-color: var(--success);
}
.provider-pill .logo {
  width: 28px; height: 28px;
  border-radius: 6px;
  background-size: cover;
  flex-shrink: 0;
}
.provider-pill .name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.region-pill-grid {
  display: flex; flex-wrap: wrap; gap: 6px;
}
.region-pill {
  padding: 6px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
  user-select: none;
}
.region-pill:hover { border-color: var(--accent); }
.region-pill.selected {
  background: var(--vpn-dim);
  border-color: var(--vpn);
  color: var(--vpn);
}
.region-pill.disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.spinner {
  display: inline-block;
  width: 16px; height: 16px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.loading-row {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  padding: 40px;
  color: var(--text-dim);
}

/* Skeleton cards — placeholder while a grid is loading. Mirrors .movie-card
   geometry (2:3 poster only, no strip below) so the layout doesn't jump
   when real cards swap in. Themed via existing CSS vars so Watch / Read /
   Play all get a subject-appropriate shimmer. JS-free animation. */
.skeleton-card {
  background: var(--surface);
  border-radius: 18px;
  border: 1px solid var(--border);
  overflow: hidden;
  display: flex; flex-direction: column;
}
.skeleton-poster {
  aspect-ratio: 2/3;
  background: var(--surface-2);
  background-image: linear-gradient(
    90deg,
    var(--surface-2) 0%,
    var(--surface-3) 50%,
    var(--surface-2) 100%
  );
  background-size: 200% 100%;
  animation: skeletonShimmer 1.4s ease-in-out infinite;
}
/* #993 — modal loading-state skeleton primitives. Inline style="" is dead
   under the style-src 'self' CSP, so these MUST be classes. */
.skeleton-line {
  background: var(--surface-2);
  background-image: linear-gradient(90deg, var(--surface-2) 0%, var(--surface-3) 50%, var(--surface-2) 100%);
  background-size: 200% 100%;
  animation: skeletonShimmer 1.4s ease-in-out infinite;
  border-radius: 4px;
}
.skeleton-line.sk-w70 { width: 70%; }
.skeleton-line.sk-w45 { width: 45%; }
.skeleton-line.sk-w100 { width: 100%; }
.skeleton-line.sk-w92 { width: 92%; }
.skeleton-line.sk-w75 { width: 75%; }
.skeleton-line.sk-h22 { height: 22px; border-radius: 6px; margin-bottom: 8px; }
.skeleton-line.sk-h14 { height: 14px; }
.skeleton-line.sk-h13 { height: 13px; margin-bottom: 7px; }
.skeleton-line.sk-btn { flex: 1; height: 40px; border-radius: 10px; }
.modal-actions .skeleton-line.sk-btn + .skeleton-line.sk-btn { margin-inline-start: 8px; }
@media (prefers-reduced-motion: reduce) {
  .skeleton-line { animation: none; }
}

@keyframes skeletonShimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .skeleton-poster { animation: none; }
}
.loading-inline {
  display: flex; align-items: center; gap: 8px;
  padding: 12px;
  color: var(--text-dim);
  font-size: 13px;
}

/* #691 — Party tab first-paint loading spinner. activateTab('party') primes
   the empty #partyContent with this so the panel.hidden swap commits with
   visual feedback while the deferred renderParty() task fills in the real
   surface. Sized + centered to roughly match the home-screen hero so the
   spinner sits where content lands, not on the toolbar edge. */
.party-loading {
  display: flex; align-items: center; justify-content: center;
  min-height: 240px;
}
.party-loading-spinner {
  width: 28px; height: 28px;
  border: 2.5px solid var(--surface-3);
  border-top-color: var(--text);
  border-radius: 50%;
  animation: partyLoadingSpin 0.8s linear infinite;
}
@keyframes partyLoadingSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .party-loading-spinner { animation: none; }
}
/* The HTML `hidden` attribute should always win over class display rules. */
[hidden] { display: none !important; }

/* #832 OAuth-return overlay: full-screen opaque cover that suppresses the
   onboarding wizard flash while the native OAuth session hydrates. */
.oauth-return-overlay {
  position: fixed;
  inset: 0;
  z-index: 400;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
}
.oauth-return-overlay--dismissing {
  opacity: 0;
  transition: opacity .25s ease;
  pointer-events: none;
}
.oauth-return-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}
.oauth-return-spinner {
  width: 32px;
  height: 32px;
  border: 2.5px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .oauth-return-spinner { animation: none; }
}
.oauth-return-spinner--stalled {
  animation: none;
  opacity: .3;
}
.oauth-return-status {
  margin: 0;
  font-size: 14px;
  color: var(--text-dim);
  text-align: center;
}

.toast {
  position: fixed; bottom: 24px; left: 50%;
  transform: translateX(-50%) translateY(100px);
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 12px 20px;
  border-radius: 10px;
  z-index: 300;
  font-size: 14px;
  box-shadow: var(--shadow-2);
  opacity: 0;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s;
  /* A passive notification must never steal taps. Idle toasts (opacity:0 but
     still in the DOM) are centered bottom-anchored and land over the tab bar
     on mobile, invisibly blocking For You / Party / Wishlist after any toast
     fires. Interactive children (undo button, links) opt back in below. */
  pointer-events: none;
}
.toast button,
.toast a,
.toast-undo-btn {
  /* Re-enable hit-testing for interactive children. The container itself
     stays pointer-events:none so only deliberate controls are tappable. */
  pointer-events: auto;
}
.toast.visible {
  transform: translateX(-50%) translateY(0);
  opacity: 1;
}
.toast-undo { display: flex; align-items: center; gap: 12px; padding: 10px 14px 10px 16px; }
.toast-undo-btn {
  background: var(--accent);
  color: var(--on-accent);
  border: none;
  border-radius: 8px;
  padding: 6px 14px;
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
}
.toast-undo-btn:hover { filter: brightness(1.1); }

/* Lift toasts to the top of the viewport while a bottom-sheet modal is
   open (#494). Both `#toast` and `#toastUndo` are anchored to `bottom:
   24px`, which on mobile lands directly on top of any open bottom-sheet's
   primary action row — most painfully the rate-modal's Skip / Save buttons
   when the Watchlist→Watched quick action fires the move toast and the
   rate prompt at the same moment, leaving the user unable to finish
   rating. Re-anchoring to the top (and inverting the slide direction so
   the in/out animation still reads correctly) keeps the sheet's bottom
   actions tappable. The `body:has(...)` selector reacts whether the toast
   was triggered before or after the sheet opened. */
body:has(.modal-overlay.is-bottom-sheet.visible) .toast {
  top: calc(20px + env(safe-area-inset-top, 0px));
  bottom: auto;
  transform: translateX(-50%) translateY(-120%);
}
body:has(.modal-overlay.is-bottom-sheet.visible) .toast.visible {
  transform: translateX(-50%) translateY(0);
}

/* TV episode progress (#26) — modal section + per-card progress badge. */
.episodes-section {
  margin-top: 24px;
  padding: 0 24px;
}
/* #562 — wrap the whole section in a collapsible <details>; the <summary>
 * mirrors the old <h3> + progress bar so the section reads identically when
 * closed, and expands inline when the user wants the full season list. */
.episodes-disclosure { display: block; }
.episodes-summary {
  display: grid;
  grid-template-columns: 18px 1fr auto;
  align-items: center;
  gap: 10px;
  margin: 0 0 12px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
  list-style: none;
  user-select: none;
  min-height: 44px;
  transition: background 0.15s, border-color 0.15s;
}
.episodes-summary::-webkit-details-marker { display: none; }
.episodes-summary:hover { background: var(--surface-2); }
.episodes-disclosure[open] .episodes-summary {
  background: var(--surface-2);
  border-color: var(--accent);
}
.episodes-summary-chevron {
  display: inline-block;
  font-size: 18px;
  line-height: 1;
  color: var(--text-muted);
  transition: transform 0.18s;
  text-align: center;
}
.episodes-disclosure[open] .episodes-summary-chevron {
  transform: rotate(90deg);
  color: var(--accent);
}
.episodes-summary-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}
.episodes-summary .episodes-progress {
  font-size: 12px;
  color: var(--text-dim);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  justify-self: end;
}
.episodes-progress-bar {
  height: 4px;
  background: var(--surface-2);
  border-radius: 2px;
  overflow: hidden;
  margin-bottom: 12px;
}
.episodes-progress-bar > span {
  display: block;
  height: 100%;
  background: var(--accent);
  transition: width 0.2s;
}
/* Inline-progress flavor sits inside the summary, between the title row and
 * the count chip — full-width but tiny so the grid row stays compact. */
.episodes-progress-bar-inline {
  grid-column: 1 / -1;
  height: 3px;
  margin: 0;
  background: rgba(255, 255, 255, 0.06);
}
.seasons-list .season {
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 8px;
  overflow: hidden;
  background: var(--surface);
}
.season-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  cursor: pointer;
  user-select: none;
}
.season-header:hover, .season-header:focus-visible { background: var(--surface-2); }
.season-header:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.season-chevron {
  display: inline-block;
  width: 14px;
  font-size: 12px;
  color: var(--text-dim);
  transition: transform 0.15s;
}
.season-name { flex: 1; font-weight: 500; font-size: 14px; }
.season-count { font-size: 12px; color: var(--text-dim); min-width: 44px; text-align: right; }
.season-mark {
  background: transparent;
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 10px;
  font-size: 12px;
  cursor: pointer;
}
.season-mark:hover { background: var(--surface-2); color: var(--text); }
.season-episodes {
  border-top: 1px solid var(--border);
  padding: 4px 0;
  background: var(--surface-2);
}
.episode-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px 8px 12px;
  cursor: pointer;
  font-size: 13px;
  border-radius: 0;
}
.episode-row:hover { background: var(--surface-3, var(--surface)); }
.episode-row input[type="checkbox"] {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  cursor: pointer;
  accent-color: var(--accent);
}
.episode-row .episode-num {
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  min-width: 32px;
  font-weight: 500;
}
.episode-row .episode-name { flex: 1; color: var(--text); }
.episode-row .episode-aired { font-size: 11px; color: var(--text-muted); }
.episode-row.seen .episode-name { color: var(--text-dim); }
.episodes-empty, .episodes-error {
  padding: 12px;
  font-size: 13px;
  color: var(--text-dim);
}
.episodes-error { color: var(--accent); }

/* Card progress badge — top-right corner of TV poster. Sits above the .right
   corner-status badge by virtue of being rendered later (no z-index war).
   Geometry aligned with the rest of the .poster-badge family in #271
   (padding 4px 8px, border-radius 6px) so it reads as part of the same
   badge system instead of a stray pill. The translucent black background
   stays since the bar visualization needs more visual weight than a tinted
   chrome on top of poster art could provide. */
.poster-badge.tv-progress {
  right: 8px;
  display: flex;
  align-items: center;
  gap: 6px;
  background: rgba(0, 0, 0, 0.7);
  color: #fff;
  font-variant-numeric: tabular-nums;
  backdrop-filter: blur(4px);
}
.tv-progress-bar {
  display: inline-block;
  width: 32px;
  height: 3px;
  background: rgba(255, 255, 255, 0.25);
  border-radius: 2px;
  overflow: hidden;
}
.tv-progress-bar > span {
  display: block;
  height: 100%;
  background: var(--accent);
}

/* Rate modal */
.rate-modal {
  max-width: 420px;
  padding: 32px;
  text-align: center;
}
/* Rate modal is short and padded — skip the sticky/negative-margin layout
   the cinematic movie modal needs and just anchor the X to the card corner. */
.rate-modal .modal-close-floating {
  position: absolute;
  top: 14px;
  right: 14px;
  align-self: auto;
  margin: 0;
}
/* Title padding clears two overlapping affordances on the rate-modal sheet
   (#392 + follow-up):
   - Right + left 48px clears the absolute-positioned `.modal-close-floating`
     × button (top: 14px, right: 14px, ~36px square + breathing room).
     Symmetric padding keeps the `text-align: center` title visually
     centered.
   - Top 24px clears the `.bottom-sheet-handle` on mobile, which is sticky
     at the top of the modal with `margin-bottom: -22px` — that negative
     margin reclaims the handle's flow space, so without this padding the
     title's first line paints *behind* the 22px handle. The padding is
     small enough that on desktop (where the handle is `display: none`)
     it just adds a hair of breathing room above the title.
   The cinematic `.modal h2` uses sticky-position layout and doesn't share
   either overlap. */
.rate-modal h2 {
  font-size: 20px;
  margin-bottom: 6px;
  line-height: 1.3;
  padding: 24px 48px 0;
}
.rate-modal .subtitle { color: var(--text-dim); font-size: 13px; margin-bottom: 24px; }
.rate-stars {
  display: flex; justify-content: center; gap: 6px;
  margin-bottom: 24px;
  font-size: 40px;
}
.rate-star {
  color: var(--text-muted);
  cursor: pointer;
  transition: color 0.1s, transform 0.1s;
  user-select: none;
}
/* Gate :hover behind hover-capable devices — without this, mobile Safari/
   Chrome treat the first tap on a star as "fauxhover" (showing the hover
   state but suppressing the synthetic click), so users have to tap twice
   before pendingRating updates and Save enables. */
@media (hover: hover) and (pointer: fine) {
  .rate-star:hover { color: var(--warning); transform: scale(1.1); }
}
/* Mobile: :active gives instant tactile feedback; .active lands with a
   one-shot spring settle via the keyframe below. */
@media (hover: none) {
  .rate-star:active { transform: scale(1.25); }
}
@keyframes rate-star-settle {
  0%   { transform: scale(1.25); }
  55%  { transform: scale(0.92); }
  100% { transform: scale(1);    }
}
.rate-star.active {
  color: var(--warning);
  animation: rate-star-settle 0.25s ease-out both;
}
.rate-value-label {
  font-size: 13px;
  color: var(--text-dim);
  margin-bottom: 20px;
  height: 18px;
}
.rate-actions {
  display: flex; gap: 8px; justify-content: center;
}
.rate-actions button {
  padding: 10px 20px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-weight: 500;
  font-size: 14px;
  transition: background 0.15s;
}
.rate-actions button:hover { background: var(--surface-2); }
.rate-actions button.primary:disabled { background: var(--surface-3); border-color: var(--border); }
#rateModal { z-index: 250; }

/* PWA install bottom-sheet (#174). Reuses .modal-overlay / .is-bottom-sheet
   chrome; everything here is layout + chrome for the install affordance:
   hero icon, title, benefit line, optional iOS step-list, action row. */
.pwa-install-modal {
  max-width: 420px;
  padding: 28px 24px 24px;
  text-align: center;
}
.pwa-install-modal .modal-close-floating {
  position: absolute;
  top: 14px;
  right: 14px;
  align-self: auto;
  margin: 0;
}
.pwa-install-hero {
  width: 64px;
  height: 64px;
  margin: 8px auto 14px;
  border-radius: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.pwa-install-modal h2 {
  font-size: 20px;
  margin-bottom: 6px;
  line-height: 1.3;
  padding: 0 32px;
}
.pwa-install-benefit {
  color: var(--text-dim);
  font-size: 13.5px;
  line-height: 1.5;
  margin: 0 auto 18px;
  max-width: 320px;
}
.pwa-install-steps {
  list-style: none;
  padding: 0;
  margin: 0 0 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  text-align: left;
}
.pwa-install-steps li {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-2);
  font-size: 13.5px;
  line-height: 1.4;
  color: var(--text);
}
.pwa-step-icon {
  flex: 0 0 36px;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.pwa-step-text { flex: 1; }
.pwa-install-actions {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-top: 4px;
}
.pwa-install-actions button {
  flex: 1;
  max-width: 180px;
  padding: 11px 20px;
  border: 1px solid var(--border);
  border-radius: 10px;
  font-weight: 500;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.15s;
  background: transparent;
  color: var(--text);
}
.pwa-install-actions button:hover { background: var(--surface-2); }
.pwa-install-actions button.primary { border-radius: 10px; }
#pwaInstallModal { z-index: 250; }
.pwa-install-samsung-note {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-dim);
  background: var(--surface-2);
  border-radius: 8px;
  padding: 10px 12px;
  margin: 0 0 4px;
}

/* Store badge inside the PWA install sheet (#917). Ported from
   welcome.css so the badge renders correctly inside the app shell
   without loading the marketing page stylesheet. */
.pwa-install-badge {
  display: flex;
  justify-content: center;
  margin: 0 0 20px;
}
.pwa-install-badge .store-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 10px 18px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  min-height: 56px;
  text-decoration: none;
  font-family: inherit;
  transition: background 0.15s;
}
.pwa-install-badge .store-badge:hover { background: var(--surface-2); }
.pwa-install-badge .store-badge-icon {
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
}
.pwa-install-badge .store-badge-text {
  display: flex;
  flex-direction: column;
  text-align: left;
  line-height: 1.1;
}
.pwa-install-badge .store-badge-small {
  font-size: 11px;
  color: var(--text-dim);
}
.pwa-install-badge .store-badge-big {
  font-size: 18px;
  font-weight: 600;
}

/* Page-level footer (#1068). index.html had no footer before this; badge
   treatment mirrors welcome.css's .store-badge (and the #917 modal port
   above) so the same component reads consistently across the app, the
   marketing page, and the install sheet. */
.landing-footer {
  border-top: 1px solid var(--border);
  padding: 28px 24px calc(28px + env(safe-area-inset-bottom, 0px));
  text-align: center;
}
.landing-footer-badges {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
}
.landing-footer .store-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 10px 18px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  min-height: 56px;
  text-decoration: none;
  font-family: inherit;
  transition: background 0.15s;
}
.landing-footer .store-badge:hover { background: var(--surface-2); }
.landing-footer .store-badge-icon {
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
}
.landing-footer .store-badge-text {
  display: flex;
  flex-direction: column;
  text-align: left;
  line-height: 1.1;
}
.landing-footer .store-badge-small {
  font-size: 11px;
  color: var(--text-dim);
}
.landing-footer .store-badge-big {
  font-size: 18px;
  font-weight: 600;
}

/* Native-shell gate (#1073). The Capacitor iOS/Android apps load this same
   SPA live from https://todeo.app, so web-only affordances that invite the
   user to "download the app" (the store-badge footer above, and any future
   install prompt) are meaningless — and an App Store review risk — inside the
   native app. src/native-shell-gate.js stamps `is-native-shell` on <html> the
   moment Capacitor is detected (before <body> paints), and this rule hides
   every element marked `data-web-only` under it. Mark a surface web-only and
   it vanishes in the shell automatically; nothing else to remember. */
html.is-native-shell [data-web-only] { display: none !important; }

/* Party join name prompt — mandatory display-name modal that appears
   after the user enters a valid code, before /api/party/join is called.
   Reuses .modal-overlay/.modal for the backdrop + card chrome; just
   adds layout for the input + buttons row. (#new) */
.party-name-prompt-overlay { z-index: 260; }
.party-name-prompt-modal {
  max-width: 420px;
  padding: 28px 24px 24px;
  text-align: center;
}
.party-name-prompt-modal h2 {
  font-size: 20px;
  margin-bottom: 6px;
  line-height: 1.3;
}
.party-name-prompt-modal .subtitle {
  color: var(--text-dim);
  font-size: 13px;
  margin-bottom: 20px;
}
.party-name-prompt-modal .party-anon-name-input {
  width: 100%;
  margin-bottom: 12px;
  text-align: center;
}
.party-name-prompt-error {
  margin: -4px 0 12px;
  font-size: 13px;
  color: var(--danger);
}
.party-name-prompt-actions {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-top: 8px;
}
.party-name-prompt-actions button {
  padding: 10px 20px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-weight: 500;
  font-size: 14px;
  transition: background 0.15s;
  min-width: 96px;
}
.party-name-prompt-actions button:hover { background: var(--surface-2); }

/* Cold-start genre picker (#361). Replaces the rate-10 quiz (#84) — the
   .cold-start* outer wrapper + spacing rules below are reused; the chip
   grid + CTA row are this surface's specific UI. The legacy `.cold-start-card`
   styles further below are retained while the rate-10 surface area is
   trimmed in a follow-up so we don't churn CSS that's not in this PR's
   scope. */
.cold-start-genre {
  padding-top: 8px;
}
.cold-start-chip-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 4px 0 18px;
}
.cold-start-chip {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 10px 16px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  min-height: 40px;
  transition: background 0.12s, border-color 0.12s, color 0.12s, transform 0.08s;
}
.cold-start-chip:hover { border-color: var(--accent); }
.cold-start-chip.selected {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}
.cold-start-cta-row {
  display: flex;
  gap: 12px;
  margin: 20px 0 24px;
}
.cold-start-cta {
  flex: 1 1 0;
  border: none;
  padding: 14px 18px;
  border-radius: 10px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  min-height: 48px;
  transition: background 0.15s, color 0.15s;
}
.cold-start-cta-skip {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
}
.cold-start-cta-skip:hover { background: var(--surface-3); }
.cold-start-cta-continue {
  background: var(--accent);
  color: var(--on-accent);
}
.cold-start-cta-continue:hover { background: var(--accent-hover); }
.cold-start-cta-continue:disabled {
  background: var(--surface-3);
  color: var(--text-muted);
  cursor: not-allowed;
}
@media (prefers-reduced-motion: reduce) {
  .cold-start-chip,
  .cold-start-cta { transition: none; }
}

/* Day-0 taste picker tile (#711). Renders at the top of the Discover For-You
   surface while the whole library is under 5 items, feeding a synthetic seed
   into the recommender's trending fallback. Reuses the #361 chip language
   (.cold-start-chip) so it reads as one family; the taste-specific overrides
   below just give it a card frame, a wider 44px touch target (UX gate), and an
   era-band sub-label. */
.cold-start-taste {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 18px 18px 8px;
  margin: 0 0 20px;
  max-width: none;
}
.cold-start-taste .cold-start-header {
  align-items: center;
  margin-bottom: 2px;
}
.cold-start-taste .cold-start-subtitle {
  margin: 0 0 14px;
}
/* >=44pt hit area on every chip (UX gate). The base .cold-start-chip is 40px;
   bump it here so the taste tile clears the 44px accessibility floor. */
.taste-chip {
  min-height: 44px;
}
.cold-start-taste-era-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin: 2px 0 8px;
}
.cold-start-taste .taste-era-grid {
  margin-bottom: 14px;
}
.cold-start-taste .taste-dismiss {
  flex-shrink: 0;
}

/* Cold-start quiz (#84). Stacked card list shown on the For You tab the
   first time a fresh user lands there for the active category. */
.cold-start {
  max-width: 640px;
  margin: 0 auto;
}
.cold-start-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 4px;
}
.cold-start-header h2 {
  font-size: 22px;
  font-weight: 700;
  margin: 0;
  line-height: 1.25;
}
.cold-start-header .cold-start-skip {
  background: none;
  border: none;
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  padding: 6px 8px;
  flex-shrink: 0;
}
.cold-start-header .cold-start-skip:hover { text-decoration: underline; }
.cold-start-subtitle {
  color: var(--text-dim);
  font-size: 14px;
  margin: 0 0 10px;
}
.cold-start-progress {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: 16px;
}
.cold-start-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-bottom: 18px;
}
.cold-start-card {
  display: grid;
  grid-template-columns: 72px 1fr;
  gap: 14px;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  transition: opacity 0.18s, background 0.18s;
}
.cold-start-card.dismissed {
  opacity: 0.5;
}
.cold-start-card.rated {
  background: var(--surface-2);
}
.cold-start-card .cs-poster {
  width: 72px;
  height: 108px;
  border-radius: 8px;
  background: var(--surface-3);
  background-size: cover;
  background-position: center;
  flex-shrink: 0;
}
.cold-start-card .cs-poster.no-art {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  font-size: 22px;
}
.cold-start-card .cs-body {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-width: 0;
}
.cold-start-card .cs-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  margin: 0 0 2px;
  line-height: 1.3;
  word-wrap: break-word;
}
.cold-start-card .cs-meta {
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 8px;
}
.cold-start-card .cs-stars {
  display: flex;
  gap: 4px;
  font-size: 28px;
  line-height: 1;
  margin-bottom: 6px;
}
.cold-start-card .cs-star {
  color: var(--text-muted);
  cursor: pointer;
  user-select: none;
  background: none;
  border: none;
  padding: 4px 2px;
  min-width: 36px;
  min-height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.1s, transform 0.1s;
}
.cold-start-card .cs-star:hover { color: var(--warning); transform: scale(1.08); }
.cold-start-card .cs-star.active { color: var(--warning); }
.cold-start-card .cs-haventseen {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  padding: 6px 0;
  text-align: left;
  align-self: flex-start;
}
.cold-start-card .cs-haventseen:hover { color: var(--text-dim); }
.cold-start-card .cs-status {
  font-size: 13px;
  color: var(--text-dim);
  font-weight: 500;
}
.cold-start-card .cs-status .cs-status-stars { color: var(--warning); }
.cold-start-card .cs-undo {
  background: none;
  border: none;
  color: var(--accent);
  font-size: 12px;
  cursor: pointer;
  padding: 4px 0 0;
}
.cold-start-card .cs-undo:hover { text-decoration: underline; }
.cold-start-done-row {
  display: flex;
  justify-content: center;
  margin: 10px 0 24px;
}
.cold-start-done-btn {
  background: var(--accent);
  color: var(--on-accent);
  border: none;
  padding: 14px 28px;
  border-radius: 10px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  min-height: 48px;
  transition: background 0.15s;
}
.cold-start-done-btn:hover { background: var(--accent-hover); }
.cold-start-done-btn:disabled {
  background: var(--surface-3);
  color: var(--text-muted);
  cursor: not-allowed;
}
.cold-start-loading,
.cold-start-error {
  text-align: center;
  padding: 40px 20px;
  color: var(--text-dim);
  font-size: 14px;
}
.cold-start-error button {
  margin-top: 14px;
  background: var(--accent);
  color: var(--on-accent);
  border: none;
  padding: 10px 20px;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  font-size: 14px;
}

@media (max-width: 720px) {
  .cold-start-header h2 { font-size: 19px; }
  .cold-start-card { grid-template-columns: 60px 1fr; gap: 10px; padding: 10px; }
  .cold-start-card .cs-poster { width: 60px; height: 90px; }
  .cold-start-card .cs-stars { font-size: 26px; gap: 2px; }
  .cold-start-card .cs-star { min-width: 40px; min-height: 40px; }
  .cold-start-card .cs-title { font-size: 14px; }
  .cold-start-done-btn { width: 100%; }

  /* Bottom-sheet treatment for the movie / rate / settings modals (#91).
     The overlay anchors to the bottom edge, the modal becomes full-width
     with rounded top-only corners, and the slide animation runs on
     `transform`. Outside this query the desktop centered-dialog layout
     in `.modal-overlay` / `.modal` is untouched. */
  .modal-overlay.is-bottom-sheet {
    align-items: flex-end;
    padding: 0;
  }
  .modal-overlay.is-bottom-sheet > .modal {
    width: 100%;
    max-width: none;
    border-radius: 16px 16px 0 0;
    max-height: 90vh;
    transform: translateY(100%);
    transition: transform 0.25s cubic-bezier(0.32, 0.72, 0, 1),
                max-height 0.18s ease;
    box-shadow: var(--shadow-3);
  }
  .modal-overlay.is-bottom-sheet.visible > .modal {
    transform: translateY(0);
  }
  /* While the user is actively dragging the handle, JS writes
     `transform: translateY(<dy>px)` directly on the element — kill the
     CSS transition so the gesture tracks the finger 1:1. */
  .modal-overlay.is-bottom-sheet > .modal.dragging {
    transition: none;
  }
  /* Peek snap-point — half-height resting position (tap-on-handle toggle). */
  .modal-overlay.is-bottom-sheet > .modal.peek {
    max-height: 60vh;
  }

  .modal-overlay.is-bottom-sheet .bottom-sheet-handle {
    display: flex;
    position: sticky;
    top: 0;
    z-index: 12;
    height: 22px;
    margin-bottom: -22px;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    cursor: grab;
    /* The handle owns vertical pan gestures so drags translate to the
       sheet's transform rather than scrolling the inner content. The
       scrollable content area stays at the default `auto`. */
    touch-action: none;
    /* Faint top-fade so the pill is legible over the movie modal's
       backdrop hero image (the rate/settings surfaces don't strictly
       need it, but a barely-there fade reads as the same visual
       affordance across all three sheets). */
    background: linear-gradient(to bottom, rgba(20,20,31,0.18) 0%, rgba(20,20,31,0) 100%);
  }
  .modal-overlay.is-bottom-sheet .bottom-sheet-handle:active { cursor: grabbing; }

  /* Settings modal has its own inner-scroll wrapper (`.settings-modal-scroll`)
     and the outer `.modal` is `overflow: hidden`. The negative-margin
     reclaim trick used on movie/rate isn't needed here — the handle just
     sits as a flex child at the top of the column. Profile modal (#545)
     follows the same pattern — the float-row of action buttons sits as
     a flex child directly below the handle, no overlap. */
  .modal-overlay.is-bottom-sheet > .modal.settings-modal > .bottom-sheet-handle,
  .modal-overlay.is-bottom-sheet > .modal.profile-modal > .bottom-sheet-handle {
    margin-bottom: 0;
  }

  /* Respect reduced-motion: skip the slide-up entirely. The sheet
     simply appears at its resting position when `.visible` toggles. */
  @media (prefers-reduced-motion: reduce) {
    .modal-overlay.is-bottom-sheet > .modal {
      transition: none !important;
    }
  }

  /* Top-sheet override (#174 follow-up). The PWA install sheet adds
     `.is-top-sheet` when rendered on iOS Safari so it doesn't cover the
     bottom URL bar — that bar is where Safari's Share icon lives, and
     the iOS instruction copy points the user at it. On Android we keep
     the default bottom-sheet position; the URL bar is at the top. */
  .modal-overlay.is-bottom-sheet.is-top-sheet {
    align-items: flex-start;
  }
  .modal-overlay.is-bottom-sheet.is-top-sheet > .modal {
    border-radius: 0 0 16px 16px;
    transform: translateY(-100%);
  }
  .modal-overlay.is-bottom-sheet.is-top-sheet.visible > .modal {
    transform: translateY(0);
  }
  /* The drag handle visually points "grab me to dismiss this sheet" away
     from the screen edge it's anchored to. On a top-sheet it would point
     up off-screen, which is meaningless — hide it. The X button still
     dismisses. */
  .modal-overlay.is-bottom-sheet.is-top-sheet .bottom-sheet-handle {
    display: none;
  }
}

/* Discover */
.section-heading {
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  margin: 32px 0 14px;
  display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap;
}
.section-heading:first-child { margin-top: 0; }
.section-heading.trending {
  color: var(--text);
  gap: 8px;
}
.section-heading.trending::before {
  content: '';
  display: inline-block;
  width: 3px;
  height: 14px;
  border-radius: 2px;
  background: var(--accent);
  flex-shrink: 0;
}
.section-heading .meta {
  color: var(--text-muted);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  font-size: 12px;
}
.genre-chips {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-bottom: 20px;
}
.genre-chip {
  padding: 8px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
  color: var(--text-dim);
}
.genre-chip:hover { border-color: var(--accent); color: var(--text); }
.genre-chip.active {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}
/* Mood chips (#87) — visually adjacent to genre chips but lightly tinted so
   the user can tell at a glance the chip strip switched modes. The icon is
   a leading emoji rendered inside .mood-icon for spacing. */
.mood-chip {
  background: color-mix(in srgb, var(--accent) 8%, var(--surface-2));
  border-color: color-mix(in srgb, var(--accent) 22%, var(--border));
}
.mood-chip:hover {
  background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
}
.mood-chip.active {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}
.mood-chip .mood-icon {
  margin-right: 6px;
  font-size: 14px;
  line-height: 1;
  display: inline-block;
}
/* Genres / Moods pivot above the chip strip (#87). Uses the same pill
   shape as the segmented controls in .filter-bar but lives in its own row
   to make the toggle feel deliberate. */
.browse-pivot {
  display: inline-flex;
  gap: 4px;
  padding: 3px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  margin-bottom: 12px;
}
.browse-pivot-btn {
  padding: 6px 14px;
  background: transparent;
  border: none;
  border-radius: 999px;
  font-size: 13px;
  color: var(--text-dim);
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.browse-pivot-btn:hover { color: var(--text); }
.browse-pivot-btn.active {
  background: var(--accent);
  color: var(--on-accent);
}

/* Annual goals progress strip (#86). Sits above the For You toolbar; one
   row per non-zero goal in the current category. Compact — designed not
   to push the swipe deck below the fold on mobile. The fill is `--accent`
   when on-pace or ahead, `--warning` when behind. .hit adds a soft glow
   for the celebrating-good-job state. The strip itself is JS-built so the
   number of rows tracks state.goals dynamically. */
/* First-5 activation progress chip (#964) -------------------------------- */
.first5-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px 7px 14px;
  margin: 0 0 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 20px;
  width: fit-content;
  max-width: 100%;
}
.first5-chip-text {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}
.first5-chip-dismiss {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  padding: 2px;
  border-radius: 50%;
  /* 44pt tap floor via the #940 transparent-overlay pattern; the chip itself
     stays visually compact. (Reviewer suggested a .ghost class: none exists;
     .secondary is too heavy for an inline icon dismiss.) */
  position: relative;
  line-height: 1;
}
.first5-chip-dismiss::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 44px; height: 44px;
  transform: translate(-50%, -50%);
}
.first5-chip-dismiss:hover { color: var(--text); }
/* ------------------------------------------------------------------- */

.goals-strip {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 14px;
}

.goal-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--surface, rgba(255, 255, 255, 0.04));
  border: 1px solid var(--border);
}
.goal-row-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  color: var(--text);
  line-height: 1.3;
}
.goal-row-head strong {
  font-weight: 700;
}
.goal-row-pace {
  color: var(--text-dim);
  font-size: 12px;
  white-space: nowrap;
}
.goal-row-pace.behind {
  color: var(--warning, #f59e0b);
}
.goal-row-pace.hit {
  color: var(--accent);
  font-weight: 600;
}
.goal-bar {
  position: relative;
  height: 6px;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.08);
}
.goal-bar-fill {
  position: absolute;
  inset: 0;
  width: 0%;
  background: var(--accent);
  border-radius: inherit;
  transition: width 240ms ease-out;
}
.goal-row.behind .goal-bar-fill {
  background: var(--warning, #f59e0b);
}
.goal-row.hit {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 0 24px -6px var(--accent);
}
.goal-row.hit .goal-bar-fill {
  background: var(--accent);
}
@media (max-width: 720px) {
  .goals-strip { gap: 8px; margin-bottom: 10px; }
  .goal-row { padding: 8px 10px; }
  .goal-row-head { font-size: 12px; }
  .goal-row-pace { font-size: 11px; }
}

/* Yearly goals settings (#86) — labelled rows with right-aligned number
   inputs. The .movies-only / .books-only / .games-only modifiers reuse
   the same per-category visibility rules used elsewhere in the settings
   modal so a Watch user doesn't see a Books goal input. */
.goals-grid {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.goals-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 14px;
}
.goals-row-label {
  color: var(--text);
}
.goals-row input[type="number"] {
  width: 96px;
  height: 36px;
  padding: 6px 10px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface, transparent);
  color: var(--text);
  font-size: 14px;
  text-align: right;
}
.goals-help {
  margin-top: 12px;
  color: var(--text-dim);
  font-size: 12px;
}

/* Unified tab toolbar (#271) — single-row pattern shared by Discover,
   For You, Watchlist, and Watched. Leftmost slot hosts the sub-mode
   segmented or search bar; the rest of the row holds the + Filter pill,
   inline Sort, and any tab-specific action button (Pick-for-me, +New
   list). Replaces the previous mix of .discover-toolbar / .filter-bar /
   .watchlist-mode-row / .watched-view-toggle as separate row containers.
   Sort lives in the .sort-menu custom dropdown (1.7.31) — see the
   block further down for its chrome. */
.tab-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}
.tab-toolbar .search-bar {
  flex: 1 1 280px;
  margin-bottom: 0;
  min-width: 200px;
}
/* Push pure-action buttons (Pick-for-me, +New list) to the right edge so
   the row reads `[sub-mode] … [+Filter] [Sort] | [action]`. Only one of
   these should be visible at a time per tab/mode. */
.tab-toolbar .pick-inline,
.tab-toolbar .lists-create-btn {
  margin-left: auto;
}
/* In-toolbar item count — pill inside the active segmented tab button.
   Shows only in the items sub-mode (gated by .items-mode-only /
   .watched-items-only on the span itself). Hidden entirely when count is 0. */
.toolbar-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--surface-3, var(--border));
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  line-height: 1.4;
  flex-shrink: 0;
  margin-left: 5px;
}
.toolbar-count[hidden] { display: none; }
.tab-toolbar select {
  padding: 0 12px;
  height: 34px;
  font-size: 14px;
}
/* Custom sort dropdown — one trigger button + popup listbox. Replaces the
   prior <select> + ↑↓-toggle pair (1.7.31). The trigger shows the active
   metric with its direction in parens; tapping the active row inside the
   popup flips asc ↔ desc. */
.sort-menu {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-width: 0;
}
.sort-menu-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 8px 0 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-card, var(--surface, transparent));
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
  box-sizing: border-box;
  max-width: 100%;
}
.sort-menu-toggle:hover,
.sort-menu-toggle:focus-visible {
  border-color: var(--accent);
}
.sort-menu.open .sort-menu-toggle {
  border-color: var(--accent);
}
/* Compact sort glyph. Hidden on desktop (the text metric label carries the
   meaning there); shown on mobile, where the toolbar sort button collapses
   to an icon to fit the one-line toolbar. */
.sort-menu-icon {
  display: none;
  flex: 0 0 auto;
  color: var(--text-dim);
}
.sort-menu-current {
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  flex: 0 1 auto;
}
/* Fixed-slot direction glyph (↑ / ↓) that sits between the truncatable
   metric label and the chevron. Stays visible even when the parent
   toggle's max-width forces the label into an ellipsis. Hidden via
   [hidden] for direction-less metrics like 'relevance'. */
.sort-menu-dir-glyph {
  flex: 0 0 auto;
  font-size: 14px;
  line-height: 1;
  color: var(--accent);
}
.sort-menu-dir-glyph[hidden] { display: none; }
.sort-menu-chevron {
  flex-shrink: 0;
  transition: transform 0.15s;
  color: var(--text-dim);
}
.sort-menu.open .sort-menu-chevron {
  transform: rotate(180deg);
  color: var(--accent);
}
.sort-menu-popup {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 100%;
  max-width: 260px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 4px;
  margin: 0;
  list-style: none;
  z-index: 60;
  box-shadow: var(--shadow-2), inset 0 1px 0 var(--border);
}
.sort-menu-popup[hidden] {
  display: none;
}
.sort-menu-popup li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 12px;
  font-size: 13.5px;
  border-radius: 7px;
  cursor: pointer;
  color: var(--text);
  white-space: nowrap;
}
.sort-menu-popup li:hover,
.sort-menu-popup li:focus-visible {
  background: var(--surface-2);
}
.sort-menu-popup li.active {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
  font-weight: 600;
}
.sort-menu-option-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sort-menu-option-dir {
  flex: 0 0 auto;
  font-size: 15px;
  line-height: 1;
}
.sort-menu-option-dir[hidden] {
  display: none;
}
@media (max-width: 720px) {
  .tab-toolbar { gap: 6px; margin-bottom: 12px; row-gap: 8px; }
  /* Sort stays on the toolbar at every viewport. On phones it collapses to a
     compact icon button — the metric text label is hidden, leaving the sort
     glyph + the asc/desc direction indicator — so it fits the one-line
     toolbar without burying sort inside the filter sheet. (Supersedes #766,
     which moved sort into the sheet bottom: that put the most-frequent action
     behind Apply and opened the dropdown into the sticky footer.) */
  .tab-toolbar .sort-menu-icon { display: inline-block; }
  .tab-toolbar .sort-menu-current { display: none; }
  .tab-toolbar .sort-menu-chevron { display: none; }
  .tab-toolbar .sort-menu-toggle { gap: 5px; padding: 0 10px; }
  /* Keep [search] and [+ Filter] on the same row on mobile (#440) — the
     search bar grows to fill, the pill sits beside it. flex-basis:0 +
     min-width:0 lets the input shrink below its 280px desktop basis so
     the pill doesn't wrap to a second line on iPhone-class widths. */
  .tab-toolbar .search-bar { flex: 1 1 0; min-width: 0; }
}
/* Narrow-screen tightening: ≤430px (iPhone 14/15 standard + below).
   Toolbar holds [segmented] [+Filter] [compact sort] [action buttons].
   Reduce filter pill padding + font to keep everything on one line. */
@media (max-width: 430px) {
  #watchedPanel .tab-toolbar .watched-view-toggle button {
    padding: 0 6px;
    font-size: 11px;
    min-height: 28px;
    position: relative;
  }
  /* #940: extend watched-view segmented button tap height to 44pt.
     Width matches the button (left:0/right:0) so siblings are never hit. */
  #watchedPanel .tab-toolbar .watched-view-toggle button::after {
    content: '';
    position: absolute;
    top: 50%; left: 0; right: 0;
    transform: translateY(-50%);
    height: 44px;
  }
  #watchedPanel .tab-toolbar .filter-pill {
    padding: 5px 8px;
    font-size: 11.5px;
  }
  #watchedPanel .tab-toolbar { gap: 4px; }
  /* #947: slimmed to 2-way (All items / Upcoming); comfortable sizing since
     the removed availability buttons freed ~80px on the row. Tap target kept
     at >=44pt via the ::after overlay below. */
  #watchlistPanel .tab-toolbar .watchlist-mode-toggle button {
    padding: 0 10px;
    font-size: 13px;
    min-height: 32px;
    position: relative;
  }
  /* #940: extend watchlist-mode segmented button tap height to 44pt.
     Width matches the button (left:0/right:0) so siblings are never hit. */
  #watchlistPanel .tab-toolbar .watchlist-mode-toggle button::after {
    content: '';
    position: absolute;
    top: 50%; left: 0; right: 0;
    transform: translateY(-50%);
    height: 44px;
  }
  /* Segmented grows to fill; Filter/Sort/Pick stay at natural width. */
  #watchlistPanel .tab-toolbar .watchlist-mode-toggle {
    flex: 1 1 auto;
    min-width: 0;
  }
  #watchlistPanel .tab-toolbar .filter-pill,
  #watchlistPanel .tab-toolbar .sort-menu,
  #watchlistPanel .tab-toolbar .pick-inline {
    flex-shrink: 0;
  }
  #watchlistPanel .tab-toolbar { gap: 4px; }
  .sort-menu-popup { font-size: 13px; }
}

/* Active-filter chips row (#205, #227, #271) — chips-only after #271 hoisted
   the + Filter pill into .tab-toolbar. Each chip dismisses one filter. JS
   sets [hidden] when no chips render so the row claims no vertical space
   when filters are inactive. .filter-pill-wrap below keeps the first-launch
   hint anchored above the pill in the toolbar. */
.active-filter-chips-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 20px;
}
.filter-pill-wrap {
  position: relative;
  display: inline-flex;
}

/* Mode-gated visibility on Watchlist (Items vs Upcoming) and Watched (Items
   vs Stats) so the unified toolbar shows the right controls per sub-mode
   without re-rendering. setWatchlistMode toggles data-mode on
   #watchlistPanel; #watchedPanel already carries data-view. */
#watchlistPanel:not([data-mode="items"]) .items-mode-only { display: none !important; }
#watchedPanel:not([data-view="items"]) .watched-items-only { display: none !important; }

/* Backwards-compatible aliases so legacy usage of .discover-toolbar still
   gets the same flex behavior without duplicating rules. */
.discover-toolbar { /* shape comes from .tab-toolbar */ }

/* Active-filter chip row + + Filter pill (#205). Lives just below the
   toolbar in idle/browse mode. Each chip dismisses one filter (×); the
   pill at the end opens the bottom sheet. The container is flex-wrap so
   on desktop more chips fit inline before truncation, and on mobile they
   wrap to additional rows naturally. */
.discover-filter-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 20px;
  position: relative;
}
.active-filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 6px 6px 12px;
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid var(--accent);
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
  line-height: 1;
  cursor: default;
}
.active-filter-chip-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px; height: 20px;
  border-radius: 999px;
  background: rgba(255,255,255,0.18);
  color: inherit;
  font-size: 12px;
  cursor: pointer;
  border: 0;
  padding: 0;
  transition: background 0.12s;
}
.active-filter-chip-remove:hover { background: rgba(255,255,255,0.32); }
.filter-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  background: var(--surface-2);
  border: 1px dashed var(--border);
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.filter-pill:hover {
  border-color: var(--accent);
  color: var(--text);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface-2));
}
.filter-pill svg { flex-shrink: 0; }

/* First-launch hint that floats above the pill on first idle render. Fades
   after the pill is tapped or after 2 page visits (logic in src/main.js).
   prefers-reduced-motion strips the fade. */
.filter-pill-hint {
  position: absolute;
  top: -34px;
  right: 0;
  background: var(--accent);
  color: var(--on-accent);
  padding: 5px 10px;
  border-radius: 8px;
  font-size: 12px;
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 4px 12px rgba(0,0,0,0.4);
  animation: filterHintBob 2.4s ease-in-out infinite;
  max-width: calc(100vw - 48px);
  overflow: hidden;
  text-overflow: ellipsis;
}
.filter-pill-hint::after {
  content: '';
  position: absolute;
  bottom: -5px;
  right: 18px;
  width: 0; height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 5px solid var(--accent);
}
@keyframes filterHintBob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-3px); }
}
/* On mobile (#440) the + Filter pill sits at the right edge of the toolbar,
   directly under the sticky header — the default top:-34px would clip the
   hint vertically against the header. Flip it below the pill instead and
   flip the arrow to point up. (Right-anchoring is already the default.)
   Must come AFTER the base rule above so the same-specificity selectors
   win on source order. */
@media (max-width: 720px) {
  .filter-pill-hint {
    top: auto;
    bottom: -34px;
  }
  .filter-pill-hint::after {
    bottom: auto;
    top: -5px;
    border-top: 0;
    border-bottom: 5px solid var(--accent);
  }
}
@media (prefers-reduced-motion: reduce) {
  .filter-pill-hint { animation: none; }
}

/* Discover filter sheet body (#205). Reuses .is-bottom-sheet primitive on
   mobile (≤720px); on desktop it falls back to the centered-dialog layout
   shared with the other modals. */
.discover-filter-sheet {
  max-width: 520px;
}
.discover-filter-sheet-scroll {
  overflow-y: auto;
  padding: 24px 24px 16px;
  flex: 1 1 auto;
}
.filter-sheet-title {
  font-size: 22px;
  font-weight: 700;
  margin: 0 0 4px;
}
.filter-sheet-subtitle {
  margin: 0 0 20px;
  font-size: 13px;
  color: var(--text-dim);
}
.filter-sheet-section {
  margin-bottom: 20px;
}
.filter-section-hint {
  margin: -2px 0 8px;
  font-size: 12px;
  color: var(--text-dim);
}
.filter-sheet-section h3 {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  margin: 0 0 10px;
}
.filter-sheet-section .segmented,
.filter-sheet-section .browse-pivot {
  display: inline-flex;
}
.filter-sheet-section select {
  padding: 8px 12px;
  font-size: 14px;
  width: 100%;
  max-width: 280px;
}
.filter-sheet-meta {
  margin: 8px 0 0;
  font-size: 12px;
  color: var(--text-muted);
  min-height: 1em;
}
/* The chip grid now lives inside the sheet — keep its own margin-bottom
   collapsed since the sheet section already provides spacing. */
.filter-sheet-section .genre-chips {
  margin-bottom: 0;
  max-height: none;
}
.filter-sheet-footer {
  display: flex;
  gap: 10px;
  padding: 12px 24px 20px;
  border-top: 1px solid var(--border);
  background: var(--surface);
  flex-shrink: 0;
}
.filter-sheet-footer button {
  flex: 1 1 0;
  padding: 12px 16px;
  font-size: 14px;
  font-weight: 600;
  border-radius: 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.filter-sheet-footer button:hover {
  border-color: var(--accent);
}
.discover-filter-sheet { display: flex; flex-direction: column; padding: 0; overflow: hidden; }

/* When the discover panel is in idle (browse) mode the sort section inside
   the sheet is irrelevant. JS mirrors the panel mode onto the sheet via
   data-discover-mode so we can hide irrelevant sections cleanly. */
#discoverFilterSheet[data-discover-mode="idle"] .searching-only {
  display: none !important;
}

/* Watchlist + Watched chips rows — chips-only after #271 hoisted their
   + Filter pills into .tab-toolbar. Geometry is shared with
   .active-filter-chips-row above; these aliases let the legacy IDs keep
   working. */
.watchlist-filter-row,
.watched-filter-row {
  /* shape comes from .active-filter-chips-row */
}

/* Filter sheet sections that only apply to Watch (movies + TV) — Year, Rating,
   Runtime, Provider, My rating, Year watched, in-sheet Type & Genre (#239).
   Scoped to .filter-sheet-section so we don't collide with Settings's
   .movies-only (the streaming-subs picker, which deliberately stays visible
   in Read so the user can manage subs without category-switching). */
body.cat-books .filter-sheet-section.movies-only,
body.cat-games .filter-sheet-section.movies-only {
  display: none !important;
}
/* Watched filter row + pill — visible in all three categories now. The
   sheet body shows the matching subset via the .movies-only / .books-only
   / .games-only rules. (Pre-#241 it was hidden in Play because Play had
   no Watched filter dimensions; #241 added Genre / Platform / Year /
   My-rating sections so the row is unhidden everywhere.) */

/* Year / rating / runtime / provider / my-rating / year-watched chips reuse
   the .genre-chips visual treatment so the sheet feels coherent across
   sections — same shape, same active state. The trailing modifier classes
   are kept on the markup so JS can target each grid for re-render without
   walking the DOM. */
.filter-sheet-section .year-chips,
.filter-sheet-section .rating-chips,
.filter-sheet-section .runtime-chips,
.filter-sheet-section .provider-chips,
.filter-sheet-section .my-rating-chips,
.filter-sheet-section .year-watched-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* "My ★" chips on Watched (#239) — visually distinct from the TMDB rating
   threshold above so the two are unambiguous. Active state borrows the
   accent fill but with a subtle inner ring to read as "your rating". */
.my-rating-chips .genre-chip.active {
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.32);
}

.liked-chips {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin-bottom: 24px;
}
.liked-chip {
  padding: 5px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 12px;
  color: var(--text-dim);
}
.liked-chip-more {
  opacity: 0.5;
  letter-spacing: 0.05em;
}

/* Clickable provider chips — deep links */
a.provider-chip {
  text-decoration: none;
  color: var(--text);
  cursor: pointer;
  transition: transform 0.1s, border-color 0.15s, background 0.15s;
}
a.provider-chip:hover {
  transform: translateY(-1px);
  border-color: var(--accent);
}
a.provider-chip.subscribed:hover { border-color: var(--success); }
a.provider-chip.vpn:hover { border-color: var(--vpn); }
a.provider-chip .provider-chip-arrow {
  margin-left: 4px;
  color: var(--text-muted);
  font-size: 11px;
  opacity: 0.7;
  transition: opacity 0.15s, transform 0.15s;
}
a.provider-chip:hover .provider-chip-arrow { opacity: 1; transform: translateX(2px); }

/* VPN reliability indicator */
.rel-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  vertical-align: middle;
  margin: 0 2px;
}
.rel-dot.high { background: var(--success); box-shadow: 0 0 0 2px rgba(74,222,128,0.18); }
.rel-dot.medium { background: var(--warning); box-shadow: 0 0 0 2px rgba(251,191,36,0.18); }
.rel-dot.low { background: #ef4444; box-shadow: 0 0 0 2px rgba(239,68,68,0.18); }
.poster-badge .rel-dot { width: 6px; height: 6px; box-shadow: none; margin-left: 4px; }
.h3-tag .rel-dot { margin-left: 4px; vertical-align: 1px; }
.provider-chip .vpn-regions {
  font-size: 13px;
  letter-spacing: 1px;
  color: var(--text-dim);
  margin-left: 2px;
}

/* Available abroad — per-provider rows. Stacks providers vertically so a
   100%-coverage Netflix doesn't blow out into a multi-line wall of flags
   next to a <80% provider. Issue #218. */
.vpn-providers-list { flex-direction: column; align-items: stretch; gap: 8px; }
.vpn-provider-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
}
.vpn-provider-row > .provider-chip { flex: 0 1 auto; min-width: 0; }
.provider-chip .vpn-summary-text {
  font-size: 13px;
  color: var(--text-dim);
  margin-left: 2px;
  white-space: normal;
}
.vpn-show-toggle {
  background: transparent;
  border: 0;
  color: var(--accent);
  font-size: 12px;
  padding: 4px 8px;
  cursor: pointer;
  border-radius: 6px;
  white-space: nowrap;
}
.vpn-show-toggle:hover { background: var(--surface-2); }
.vpn-flags-expand {
  flex-basis: 100%;
  font-size: 13px;
  letter-spacing: 1px;
  color: var(--text-dim);
  padding: 0 4px 4px 40px;
}

/* Notification banners — availability, upcoming releases, friend inbox.
   --banner-color drives border, title, pill, and icon tint.
   Modifier classes override --banner-color / --banner-color-dim per type. */
.alerts-banner {
  --banner-color: var(--success);
  --banner-color-dim: var(--success-dim);
  background: linear-gradient(135deg, var(--banner-color-dim), transparent);
  border: 1px solid var(--banner-color);
  border-radius: 12px;
  margin: 18px 24px 0;
  padding: 16px 20px;
  display: flex; gap: 16px; align-items: flex-start;
  max-width: 1352px; margin-left: auto; margin-right: auto;
}
.alerts-banner--upcoming {
  --banner-color: var(--warning);
  --banner-color-dim: var(--warning-dim);
}
.alerts-banner--friends {
  --banner-color: var(--vpn);
  --banner-color-dim: var(--vpn-dim);
}
.alerts-banner-icon { flex-shrink: 0; line-height: 1; color: var(--banner-color); }
.alerts-banner-icon svg { width: 28px; height: 28px; }
.alerts-banner-content { flex: 1; }
.alerts-banner-title {
  font-weight: 600; margin-bottom: 6px;
  color: var(--banner-color); font-size: 15px;
}
.alerts-banner-list {
  font-size: 13px;
  color: var(--text);
  display: flex; flex-direction: column; gap: 4px;
}
.alerts-banner-row {
  display: flex; align-items: center; gap: 8px;
  cursor: pointer;
  padding: 4px 8px;
  margin: -4px -8px;
  border-radius: 6px;
  transition: background 0.15s;
  /* Element is now a <button> (was a <div>) for keyboard activation —
     reset the default UA button styling so it looks identical to the
     prior row. */
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  background: transparent;
  border: 0;
}
.alerts-banner-row:hover { background: rgba(255,255,255,0.04); }
.alerts-banner-row strong { font-weight: 600; }
.alerts-banner-row .pill {
  font-size: 11px; font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--banner-color);
  color: var(--on-accent);
}
.alerts-banner-dismiss {
  width: 30px; height: 30px;
  border-radius: 6px;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-dim);
  flex-shrink: 0;
  font-size: 16px;
}
.alerts-banner-dismiss:hover { background: rgba(255,255,255,0.05); color: var(--text); }

/* Upcoming-releases calendar (#90) — agenda-style sectioned list of
   watchlist items by release date, accessed via the "Upcoming" segment
   of the watchlist mode toggle (folded in from the retired inner
   Grid|Upcoming view-toggle row). Reuses .pill / .segmented patterns. */
.upcoming-section {
  margin-bottom: 28px;
}
.upcoming-section-heading {
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  margin: 0 0 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.upcoming-section.upcoming-this-week .upcoming-section-heading {
  color: var(--accent);
  border-bottom-color: rgba(255, 77, 109, 0.35);
}
.upcoming-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* #177 — wrap holds the main row button plus the per-row .ics export button
   so they're flex-aligned without nesting an interactive element inside
   another button (which would break a11y + click semantics). */
.upcoming-row-wrap {
  display: flex;
  align-items: stretch;
  gap: 6px;
}
.upcoming-row-wrap > .upcoming-row { flex: 1 1 auto; min-width: 0; }
.upcoming-row-ics {
  flex-shrink: 0;
  width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  color: var(--text-dim);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.upcoming-row-ics:hover,
.upcoming-row-ics:focus-visible {
  background: var(--surface-2);
  color: var(--accent);
  border-color: var(--accent);
}
.upcoming-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  cursor: pointer;
  text-align: left;
  width: 100%;
  font: inherit;
  color: inherit;
  transition: background 0.15s, border-color 0.15s, transform 0.1s;
}
.upcoming-row:hover {
  background: var(--surface-2);
  border-color: var(--accent);
}
.upcoming-row:active { transform: scale(0.997); }
.upcoming-row-poster {
  width: 44px;
  height: 66px;
  flex-shrink: 0;
  border-radius: 6px;
  overflow: hidden;
  background: var(--surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
}
.upcoming-row-poster img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.upcoming-row-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.upcoming-row-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.upcoming-row-meta {
  font-size: 12px;
  color: var(--text-dim);
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.upcoming-row-meta .pill {
  font-size: 10px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-dim);
  border: 1px solid var(--border);
}
.upcoming-countdown {
  flex-shrink: 0;
  font-size: 12px;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text);
  white-space: nowrap;
}
.upcoming-countdown.is-today {
  background: var(--accent);
  color: var(--on-accent);
}
.upcoming-countdown.is-tomorrow {
  background: rgba(255, 77, 109, 0.18);
  color: var(--accent);
  border: 1px solid rgba(255, 77, 109, 0.4);
}
.upcoming-countdown.is-soon {
  background: rgba(56, 189, 248, 0.14);
  color: #38BDF8;
  border: 1px solid rgba(56, 189, 248, 0.35);
}
.upcoming-empty {
  text-align: center;
  color: var(--text-dim);
  padding: 36px 16px;
  font-size: 14px;
}
@media (max-width: 600px) {
  .upcoming-row { gap: 10px; padding: 8px 10px; }
  .upcoming-row-poster { width: 38px; height: 57px; }
  .upcoming-row-title { font-size: 14px; }
  .upcoming-countdown { font-size: 11px; padding: 5px 10px; }
}

/* ===== Onboarding wizard (#193) =======================================
   First-launch 4-step guided flow. Replaces the old in-Settings welcome
   banner. Centered modal on desktop, full-screen on mobile. The user can
   only exit via Skip All (top-right) or completing step 4 — backdrop
   clicks and Esc are intentionally ignored. Steps 2 + 3 reuse the
   existing region/language/provider pickers wired into dedicated wizard
   slots so changes flow into state via the same code paths Settings uses. */
.wizard-modal {
  max-width: 560px;
  width: 100%;
  padding: 28px 32px 24px;
  position: relative;
}
.wizard-skip-all {
  position: absolute;
  top: 12px;
  right: 14px;
  background: transparent;
  border: 0;
  color: var(--text-dim);
  font-size: 12px;
  padding: 6px 8px;
  cursor: pointer;
  border-radius: 6px;
}
.wizard-skip-all:hover { color: var(--text); background: var(--surface-2); }
.wizard-progress {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin: 4px 0 22px;
}
.wizard-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--surface-3, var(--border));
  transition: background 0.15s, transform 0.15s;
}
.wizard-dot.done { background: var(--accent); opacity: 0.55; }
.wizard-dot.active {
  background: var(--accent);
  transform: scale(1.4);
}
.wizard-step[hidden] { display: none; }
.wizard-step h2 {
  font-size: 22px;
  font-weight: 600;
  margin: 0 0 8px;
  color: var(--text);
  text-align: center;
}
.wizard-step .wizard-tagline {
  text-align: center;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--text-dim);
  margin: 0 0 24px;
}
.wizard-brand {
  text-align: center;
  font-size: 32px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--accent);
  margin: 22px 0 10px;
}
.wizard-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 14px;
}
.wizard-field label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.wizard-field select,
.wizard-field input[type="email"] {
  padding: 10px 12px;
  font-size: 14px;
  border-radius: 8px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  background: var(--surface-2);
  color: var(--text);
}
.wizard-providers-help {
  font-size: 13px;
  color: var(--text-dim);
  margin: 0 0 10px;
}
.wizard-providers {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  max-height: 320px;
  overflow-y: auto;
  padding: 4px 0 4px;
}
.wizard-providers .provider-pill { margin: 0; }
.wizard-providers-loading,
.wizard-providers-empty {
  font-size: 13px;
  color: var(--text-dim);
  padding: 18px 0;
  text-align: center;
}
.wizard-signin-status {
  font-size: 13px;
  margin: 8px 0 0;
  min-height: 1.4em;
}
.wizard-signin-status.err { color: #f87171; }
.wizard-signin-status.ok { color: #34d399; }
.wizard-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}
.wizard-actions [data-wizard-back] {
  margin-right: auto;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  padding: 10px 14px;
  font-size: 14px;
  cursor: pointer;
}
.wizard-actions [data-wizard-skip] {
  background: transparent;
  border: 0;
  color: var(--text-dim);
  font-size: 13px;
  cursor: pointer;
  padding: 8px 10px;
}
.wizard-actions [data-wizard-skip]:hover { color: var(--text); }
.wizard-actions .primary { font-weight: 600; }
.wizard-actions .primary:disabled { opacity: 0.55; cursor: not-allowed; }
.wizard-step-welcome { text-align: center; }
.wizard-step-welcome .wizard-actions {
  border-top: 0;
  justify-content: center;
}
.wizard-step-welcome .wizard-actions .primary {
  padding: 14px 38px;
  font-size: 15px;
  border-radius: 999px;
  box-shadow: 0 10px 28px -12px rgba(212, 175, 55, 0.55);
}
/* "Already have an account? Sign in" shortcut on the welcome step (#538).
 * Tertiary text-button under the primary CTA — visually quiet so it doesn't
 * compete with "Get started" for first-time users, but discoverable enough
 * that returning OAuth users see it and jump straight to step 5. */
.wizard-have-account {
  display: block;
  margin: 14px auto 0;
  padding: 8px 14px;
  background: transparent;
  border: 0;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  border-radius: 8px;
}
.wizard-have-account:hover { color: var(--text); background: var(--surface-2); text-decoration: none; }
.wizard-have-account:focus-visible { outline: 2px solid var(--accent, #f6c026); outline-offset: 2px; }

.wizard-legal-links {
  margin: 14px 0 0;
  text-align: center;
  font-size: 12px;
  color: var(--muted);
}
.wizard-legal-links a { color: var(--muted); text-decoration: underline; text-underline-offset: 3px; }
.wizard-legal-links a:hover { color: var(--text); }
.wizard-legal-links span { margin: 0 8px; opacity: 0.6; }

/* Welcome hero (#576) — the real Todeo app icon cross-fades between the
   three mode variants (watch / read / play, same SVGs the header uses)
   while the bullets below highlight in lockstep, so first-run users see
   what the three modes are before reading the heading. Reduced-motion
   keeps the watch icon and gives every bullet equal weight. */
.wizard-hero {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 8px;
  margin: 18px auto 12px;
}
.wizard-hero-glow {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 140px;
  height: 110px;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 42%, transparent), transparent 70%);
  filter: blur(8px);
  pointer-events: none;
}
.wizard-hero-stage {
  position: relative;
  width: 96px;
  height: 96px;
  display: block;
}
.wizard-hero-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: 22px;
  opacity: 0;
  transform: scale(0.94);
  animation: wizardHeroCycle 7.2s ease-in-out infinite;
  filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.28));
}
.wizard-hero-img[data-mode="watch"] { animation-delay: 0s; }
.wizard-hero-img[data-mode="read"]  { animation-delay: -2.4s; }
.wizard-hero-img[data-mode="play"]  { animation-delay: -4.8s; }
@keyframes wizardHeroCycle {
  0%, 28%      { opacity: 1; transform: scale(1); }
  33.33%, 95%  { opacity: 0; transform: scale(0.94); }
  100%         { opacity: 1; transform: scale(1); }
}
/* Rotating wordmark (#578) — three Todeo variants cross-fade in the same
   stage, in lockstep with the icon above. Each variant mirrors the per-
   category wordmark treatment used in the app header (.wordmark + body.cat-X
   .wordmark, ~line 261–284) so first-run users see the brand in all three
   modes' typographic identities before they pick one. */
.wizard-hero-wordmark-stage {
  position: relative;
  display: inline-block;
  height: 1.25em;
  min-width: 5ch;
  line-height: 1.25;
}
.wizard-hero-wordmark {
  position: absolute;
  inset: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  font-size: 30px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--text);
  opacity: 0;
  transform: scale(0.96);
  animation: wizardHeroCycle 7.2s ease-in-out infinite;
}
.wizard-hero-wordmark-accent { color: var(--accent); }
/* Watch — clean sans-serif, accent-gold "e" (mirrors body.cat-watch default) */
.wizard-hero-wordmark[data-mode="watch"] {
  animation-delay: 0s;
  font-weight: 800;
}
/* Read — italic serif, mirrors body.cat-books .wordmark */
.wizard-hero-wordmark[data-mode="read"] {
  animation-delay: -2.4s;
  font-family: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Georgia, serif;
  font-weight: 700;
  font-style: italic;
  letter-spacing: 0;
}
.wizard-hero-wordmark[data-mode="read"] .wizard-hero-wordmark-accent {
  color: #ec4848;
}
/* Play — arcade-marquee monospace, mirrors body.cat-games .wordmark */
.wizard-hero-wordmark[data-mode="play"] {
  animation-delay: -4.8s;
  font-family: 'JetBrains Mono', 'SF Mono', 'Source Code Pro', 'Menlo', 'Consolas', ui-monospace, monospace;
  font-weight: 700;
  letter-spacing: -0.04em;
}
.wizard-hero-wordmark[data-mode="play"] .wizard-hero-wordmark-accent {
  color: #b373ff;
  text-shadow: 0 0 12px rgba(168, 85, 247, 0.45);
}
.wizard-welcome-bullets {
  list-style: none;
  margin: 6px 0 26px;
  padding: 0;
  display: flex;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
}
.wizard-welcome-bullets li {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-size: 12.5px;
  color: var(--text-dim);
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
  animation: wizardBulletPulse 7.2s ease-in-out infinite;
}
.wizard-welcome-bullets li[data-mode="watch"] { animation-delay: 0s; }
.wizard-welcome-bullets li[data-mode="read"]  { animation-delay: -2.4s; }
.wizard-welcome-bullets li[data-mode="play"]  { animation-delay: -4.8s; }
@keyframes wizardBulletPulse {
  0%, 28% {
    color: var(--text);
    background: color-mix(in srgb, var(--accent) 18%, var(--surface-2));
    border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
    transform: translateY(-1px);
  }
  33.33%, 95% {
    color: var(--text-dim);
    background: var(--surface-2);
    border-color: var(--border);
    transform: translateY(0);
  }
  100% {
    color: var(--text);
    background: color-mix(in srgb, var(--accent) 18%, var(--surface-2));
    border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
    transform: translateY(-1px);
  }
}
.wizard-bullet-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: 0 0 auto;
}
.wizard-bullet-watch { background: #e5c547; }
.wizard-bullet-read  { background: #ec4848; }
.wizard-bullet-play  { background: #b373ff; }
@media (prefers-reduced-motion: reduce) {
  .wizard-hero-img { animation: none; }
  .wizard-hero-img[data-mode="watch"] { opacity: 1; transform: scale(1); }
  .wizard-hero-img[data-mode="read"],
  .wizard-hero-img[data-mode="play"] { display: none; }
  .wizard-hero-wordmark { animation: none; }
  .wizard-hero-wordmark[data-mode="watch"] { opacity: 1; transform: scale(1); }
  .wizard-hero-wordmark[data-mode="read"],
  .wizard-hero-wordmark[data-mode="play"] { display: none; }
  .wizard-welcome-bullets li {
    animation: none;
    color: var(--text);
  }
}

/* Import step (wizard step 4) — compact 2x2 grid of source cards.
   Visually mirrors the Settings ▸ Data import cards but slimmer so
   the wizard fits within one viewport at typical heights. */
.wizard-import-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin: 6px 0 4px;
}
@media (max-width: 480px) {
  .wizard-import-grid { grid-template-columns: 1fr; }
}
.wizard-import-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  font: inherit;
  transition: background 0.15s, border-color 0.15s, transform 0.05s;
}
.wizard-import-card:hover {
  background: var(--surface-3);
  border-color: var(--accent);
}
.wizard-import-card:active { transform: scale(0.99); }
.wizard-import-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.wizard-import-icon {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 12px;
  color: #fff;
  background: linear-gradient(135deg, #3b82f6, #6366f1);
}
.wizard-import-icon-letterboxd { background: #14181c; gap: 3px; }
.wizard-import-icon-letterboxd span {
  width: 7px; height: 7px; border-radius: 50%; display: inline-block;
}
.wizard-import-icon-letterboxd span:nth-child(1) { background: #00c030; }
.wizard-import-icon-letterboxd span:nth-child(2) { background: #40bcf4; }
.wizard-import-icon-letterboxd span:nth-child(3) { background: #ff8000; }
.wizard-import-icon-goodreads { background: #6b4f33; }
.wizard-import-icon-goodreads svg { color: #f5e8d3; }
.wizard-import-icon-imdb { background: #f5c518; color: #000; }
.wizard-import-icon-steam { background: linear-gradient(135deg, #1b2838, #2a475e); }
.wizard-import-icon-steam svg { color: #66c0f4; }
.wizard-import-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.wizard-import-title {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
}
.wizard-import-desc {
  font-size: 11.5px;
  color: var(--text-dim);
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wizard-import-success {
  margin: 14px 0 0;
  padding: 10px 14px;
  border-radius: 10px;
  background: color-mix(in srgb, #34d399 12%, transparent);
  border: 1px solid color-mix(in srgb, #34d399 35%, transparent);
  color: #34d399;
  font-size: 13px;
  text-align: center;
}

/* Slightly tighter step transition (was 180ms fade-only — now adds a
   small upward slide so each step feels like a step, not a swap). */
@media (prefers-reduced-motion: no-preference) {
  .wizard-step:not([hidden]) {
    animation: wizardStepIn 220ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  @keyframes wizardStepIn {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
  }
}
.wizard-dot {
  cursor: default;
}
.wizard-dot.active { box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 22%, transparent); }
@media (max-width: 720px) {
  .wizard-modal {
    max-width: 100%;
    width: 100%;
    height: 100vh;
    height: 100dvh; /* dynamic viewport — shrinks with Android browser chrome */
    max-height: 100vh;
    max-height: 100dvh;
    border-radius: 0;
    /* iOS standalone PWA: viewport extends under the status-bar/notch.
       Inset the top padding so the wizard content clears it. */
    padding: calc(24px + env(safe-area-inset-top)) 20px 20px;
  }
  .wizard-modal .wizard-skip-all {
    top: calc(10px + env(safe-area-inset-top));
    right: 12px;
    font-size: 13px;
    padding: 7px 14px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    color: var(--text-dim);
    border-radius: 20px;
    line-height: 1;
  }
  /* Pin modal to top so skip button is never pushed above the fold. */
  .modal-overlay#onboardingWizard { padding: 0; align-items: flex-start; }
}

/* ===== First-run spotlight tour (#594) =================================
   A single fixed overlay; the dim layer is painted by the spotlight's
   `box-shadow: 0 0 0 9999px` extending past the viewport. This keeps the
   hole rectangular with rounded corners and tracks the target element's
   bounding rect cleanly. Tooltip is positioned by JS into the spot with
   the most space (above or below the target) and clamped to viewport. */
.tutorial-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  pointer-events: auto;
}
.tutorial-spotlight {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  border-radius: 10px;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.68);
  outline: 2px solid rgba(255, 255, 255, 0.55);
  outline-offset: 2px;
  transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease;
  pointer-events: none;
}
.tutorial-tooltip {
  position: fixed;
  max-width: 320px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 16px 18px;
  box-shadow: var(--shadow-3);
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: top 0.2s ease, left 0.2s ease;
  z-index: 1001;
  pointer-events: auto;
}
.tutorial-tooltip-meta {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.tutorial-tooltip-title {
  font-size: 16px;
  font-weight: 600;
  margin: 0;
  outline: none;
}
.tutorial-tooltip-body {
  margin: 0;
  font-size: 14px;
  line-height: 1.45;
  color: var(--text-dim);
}
.tutorial-tooltip-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 6px;
}
.tutorial-btn {
  border-radius: 8px;
  padding: 8px 14px;
  font-size: 13px;
  cursor: pointer;
  font-family: inherit;
}
.tutorial-btn-secondary {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
}
.tutorial-btn-secondary:hover { color: var(--text); background: var(--surface-2); }
.tutorial-btn-primary {
  background: var(--accent);
  color: #161821;
  border: 1px solid var(--accent);
  font-weight: 600;
}
.tutorial-btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
@media (max-width: 480px) {
  .tutorial-tooltip { max-width: calc(100vw - 32px); }
}

/* ===== Settings IA structure (#192) ====================================
   The 12 flat sections are wrapped into 5 navigable groups (Preferences /
   Account / Notifications / Data / About). Mobile (≤720px) shows a
   top-level list of group entries and drills into one at a time, driven
   by `data-active-group` on the wrapper. Desktop (>720px) renders the
   same nav as a sticky left sidebar with the active group on the right.
   Hash routing in main.js (#settings/<group>) keeps URL + back-button in
   sync. */
.settings-groups { display: block; }
.settings-group-nav {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 12px;
}
.settings-group-nav button {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.settings-group-nav button:hover {
  background: var(--surface-3);
  border-color: var(--accent);
}
/* Leading icon squircle (#559) — gives each group a recognizable glyph
   rather than a wall of identical text rows. */
.settings-group-nav-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border-radius: 10px;
  background: var(--surface-3);
  color: var(--accent);
}
.settings-group-nav button:hover .settings-group-nav-icon {
  background: color-mix(in srgb, var(--accent) 18%, var(--surface-3));
}
.settings-group-nav-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1 1 auto;
}
.settings-group-nav-title {
  font-size: 15px;
  font-weight: 600;
}
.settings-group-nav-desc {
  font-size: 12px;
  color: var(--text-dim);
}
.settings-group-nav-chevron {
  flex-shrink: 0;
  color: var(--text-dim);
}
.settings-group-content { display: block; }
.settings-group { display: none; }
/* Active group: flex-column with consistent gap stacks the new card-style
   sections (#558) with even spacing — replaces per-section border-top
   separators. */
.settings-groups[data-active-group="preferences"] .settings-group[data-group="preferences"],
.settings-groups[data-active-group="profile"] .settings-group[data-group="profile"],
.settings-groups[data-active-group="account"] .settings-group[data-group="account"],
.settings-groups[data-active-group="notifications"] .settings-group[data-group="notifications"],
.settings-groups[data-active-group="data"] .settings-group[data-group="data"],
.settings-groups[data-active-group="about"] .settings-group[data-group="about"] {
  display: flex;
  flex-direction: column;
  /* #684: bump from 14 to 20px for clearer visual separation between
     section cards — the old gap read as "wall of rows" once Settings
     accumulated 6+ sections per group. */
  gap: 20px;
}
/* #684: destructive treatment for sections containing dangerous actions
   (sign out, delete account, wipe local data). Subtle red-tinted border +
   slightly cooler surface tone, so the user reads them as "be careful"
   without screaming at them. Applied via class="settings-section is-destructive". */
.settings-section.is-destructive {
  border-color: color-mix(in srgb, var(--danger) 35%, var(--border));
  background: color-mix(in srgb, var(--danger) 4%, var(--surface-2));
}
.settings-section.is-destructive h3 {
  color: var(--danger);
}
/* Mobile top-level (data-active-group=""): nav visible, content hidden */
.settings-groups[data-active-group=""] .settings-group-content { display: none; }
.settings-groups:not([data-active-group=""]) .settings-group-nav { display: none; }

.settings-group-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.settings-group-back {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px 6px 6px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 14px;
  cursor: pointer;
  transition: background 0.15s;
}
.settings-group-back:hover { background: var(--surface-2); }
.settings-group-title {
  font-size: 18px;
  font-weight: 700;
  margin: 0;
}

/* Advanced disclosure (TMDB key, #192 IA audit). Hosted app uses a
   server-side proxy key, so this surface is legacy/self-hoster only —
   collapsed by default keeps it out of the default Preferences view. */
/* Advanced disclosure (#558): card-styled to match the rest of the
   sections. Summary doubles as the card heading; nested sections sit inline
   with subtle dividers when expanded. */
details.settings-section-advanced {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 14px 18px;
}
details.settings-section-advanced > summary {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-dim);
  cursor: pointer;
  padding: 2px 0;
  list-style: revert;
  user-select: none;
}
details.settings-section-advanced > summary:hover { color: var(--text); }
details.settings-section-advanced[open] > summary { color: var(--text); margin-bottom: 10px; }
details.settings-section-advanced .settings-section {
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 14px 16px;
  border-radius: 10px;
}
details.settings-section-advanced .settings-section + .settings-section {
  margin-top: 10px;
}
/* #215: Games data source picker — sits below the TMDB key inside the
   Advanced disclosure. Stacks two radio rows with a hairline border between
   them, matching the rest of the Settings panel's quiet density. */
.games-backend-section { margin-top: 12px; }
.games-backend-options {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 8px;
}
.games-backend-option {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  cursor: pointer;
  font-size: 14px;
  color: var(--text);
}
.games-backend-option:hover { background: var(--surface-2); }
.games-backend-option input[type="radio"] { accent-color: var(--accent); }

/* Desktop layout (>720px): 200px sidebar + content panel.
   minmax(0, 1fr) instead of plain 1fr so the content column can shrink
   below its min-content. Plain 1fr lets greedy children (icon picker
   category tabs, language <select>, theme segmented buttons) blow out the
   column and push content past the modal's `overflow-x: hidden`, clipping
   the right edge (#504). Modal max-width bumped from 640px → 880px so
   the content column has comfortable breathing room. */
@media (min-width: 721px) {
  .settings-modal { max-width: 880px; }
  .settings-groups {
    display: grid;
    grid-template-columns: 200px minmax(0, 1fr);
    gap: 24px;
    align-items: start;
  }
  /* Both nav and content always visible on desktop, regardless of active group.
     Selector specificity must match the mobile-drill rule
     `.settings-groups:not([data-active-group=""]) .settings-group-nav { display: none }`
     (0,3,0) — otherwise that rule wins on desktop too and the sidebar
     collapses to zero width inside the grid (#269). `[data-active-group]`
     matches whenever the attribute is present, which is always (set in
     HTML and re-set by setSettingsActiveGroup).
     Sticky pins the sidebar to the top of `.settings-modal-scroll` while the
     right content panel scrolls (#279) — standard desktop settings UX.
     `align-self: start` so the grid row doesn't stretch the sidebar to match
     the (much taller) content column, which would leave nothing for sticky
     to do. No nested scroll on the sidebar itself, preserving the
     one-scroll-axis invariant from #225. */
  .settings-groups[data-active-group] .settings-group-nav {
    display: flex;
    position: sticky;
    top: 0;
    align-self: start;
  }
  .settings-groups[data-active-group=""] .settings-group-content {
    display: block;
  }
  /* Default-empty data-active-group on desktop falls back to Preferences,
     so users without JS (or before hash sync runs) still see content. */
  .settings-groups[data-active-group=""] .settings-group[data-group="preferences"] {
    display: block;
  }
  .settings-groups[data-active-group=""] .settings-group-nav button[data-target="preferences"] {
    background: var(--surface-2);
    border-color: var(--border);
    color: var(--accent);
  }
  /* Sidebar entries: compact, transparent until active/hover */
  .settings-group-nav { gap: 2px; margin-bottom: 0; }
  .settings-group-nav button {
    padding: 10px 12px;
    border-radius: 8px;
    background: transparent;
    border: 1px solid transparent;
  }
  .settings-group-nav button:hover {
    background: var(--surface-2);
    border-color: var(--border);
  }
  .settings-group-nav-desc,
  .settings-group-nav-chevron { display: none; }
  .settings-group-nav-title { font-size: 14px; font-weight: 500; }
  /* Desktop sidebar: shrink icon squircle so the row stays compact (#559) */
  .settings-group-nav-icon {
    width: 28px;
    height: 28px;
    border-radius: 7px;
  }
  .settings-group-nav-icon svg {
    width: 18px;
    height: 18px;
  }
  /* Active sidebar entry */
  .settings-groups[data-active-group="preferences"] .settings-group-nav button[data-target="preferences"],
  .settings-groups[data-active-group="profile"] .settings-group-nav button[data-target="profile"],
  .settings-groups[data-active-group="account"] .settings-group-nav button[data-target="account"],
  .settings-groups[data-active-group="notifications"] .settings-group-nav button[data-target="notifications"],
  .settings-groups[data-active-group="data"] .settings-group-nav button[data-target="data"],
  .settings-groups[data-active-group="about"] .settings-group-nav button[data-target="about"] {
    background: var(--surface-2);
    border-color: var(--border);
    color: var(--accent);
  }
  /* Sidebar handles navigation on desktop — drop the per-group back button
     and the duplicate group title (the section h3s already title each card). */
  .settings-group-header { display: none; }
}

/* Platform picker: grouped by console family (PlayStation, Xbox, …) so the
   ~30 specific platforms read as a tidy list rather than a wall of pills. */
.platform-group + .platform-group { margin-top: 12px; }
.platform-group-heading {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: 6px;
}

/* Game modal: horizontal-scrolling screenshots strip */
.game-screenshots {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-bottom: 4px;
}
.game-screenshots::-webkit-scrollbar { display: none; }
.game-screenshots img {
  flex: 0 0 auto;
  height: 180px;
  width: auto;
  border-radius: 8px;
  background: var(--surface-2);
  object-fit: cover;
}
@media (max-width: 600px) {
  .game-screenshots img { height: 130px; border-radius: 6px; }
}

/* Game modal: deals section (CheapShark integration, #27) */
.deals-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.deal-row {
  display: flex; align-items: center; gap: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px 12px;
  text-decoration: none;
  color: var(--text);
  transition: border-color 0.15s, transform 0.1s;
}
.deal-row:hover { border-color: var(--success); transform: translateY(-1px); }
.deal-store { flex: 1; min-width: 0; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.deal-price-group { display: flex; align-items: baseline; gap: 5px; flex-shrink: 0; }
.deal-price { font-size: 14px; font-weight: 700; color: var(--success); }
.deal-retail { font-size: 12px; color: var(--text-muted); text-decoration: line-through; }
.deal-savings {
  font-size: 11px; font-weight: 700;
  color: var(--on-accent); background: var(--success);
  padding: 2px 5px; border-radius: 4px;
  white-space: nowrap; flex-shrink: 0;
}
.deal-savings.deal-hl { background: var(--accent); }
.deals-attribution { font-size: 11px; color: var(--text-muted); margin: 0; }
.deals-attribution a { color: var(--text-muted); }
/* On-sale badge on game cards */
.poster-badge.on-sale { background: var(--success); color: var(--on-accent); }

/* Book / game placeholders when no cover is available */
.movie-card[data-type="book"] .movie-poster.no-image::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M12%206.042A8.967%208.967%200%200%200%206%203.75c-1.052%200-2.062.18-3%20.512v14.25A8.987%208.987%200%200%201%206%2018c2.305%200%204.408.867%206%202.292m0-14.25a8.966%208.966%200%200%201%206-2.292c1.052%200%202.062.18%203%20.512v14.25A8.987%208.987%200%200%200%2018%2018a8.967%208.967%200%200%200-6%202.292m0-14.25v14.25%22%2F%3E%3C%2Fsvg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M12%206.042A8.967%208.967%200%200%200%206%203.75c-1.052%200-2.062.18-3%20.512v14.25A8.987%208.987%200%200%201%206%2018c2.305%200%204.408.867%206%202.292m0-14.25a8.966%208.966%200%200%201%206-2.292c1.052%200%202.062.18%203%20.512v14.25A8.987%208.987%200%200%200%2018%2018a8.967%208.967%200%200%200-6%202.292m0-14.25v14.25%22%2F%3E%3C%2Fsvg%3E");
}
.movie-card[data-type="game"] .movie-poster.no-image::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M6%206.878V6a2.25%202.25%200%200%201%202.25-2.25h7.5A2.25%202.25%200%200%201%2018%206v.878m-12%200c.235-.083.487-.128.75-.128h10.5c.263%200%20.515.045.75.128m-12%200A2.25%202.25%200%200%200%204.5%209v.878m13.5-3A2.25%202.25%200%200%201%2019.5%209v.878m0%200a2.246%202.246%200%200%200-.75-.128H5.25c-.263%200-.515.045-.75.128m15%200A2.25%202.25%200%200%201%2021%2012v6a2.25%202.25%200%200%201-2.25%202.25H5.25A2.25%202.25%200%200%201%203%2018v-6c0-.98.626-1.813%201.5-2.122%22%2F%3E%3C%2Fsvg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M6%206.878V6a2.25%202.25%200%200%201%202.25-2.25h7.5A2.25%202.25%200%200%201%2018%206v.878m-12%200c.235-.083.487-.128.75-.128h10.5c.263%200%20.515.045.75.128m-12%200A2.25%202.25%200%200%200%204.5%209v.878m13.5-3A2.25%202.25%200%200%201%2019.5%209v.878m0%200a2.246%202.246%200%200%200-.75-.128H5.25c-.263%200-.515.045-.75.128m15%200A2.25%202.25%200%200%201%2021%2012v6a2.25%202.25%200%200%201-2.25%202.25H5.25A2.25%202.25%200%200%201%203%2018v-6c0-.98.626-1.813%201.5-2.122%22%2F%3E%3C%2Fsvg%3E");
}

/* Procedural book-cover placeholder (#146). Used in every book-card
   surface (grid, swipe deck, modal hero, cold-start quiz) when no
   cover image URL can be resolved through any of the OL fallback keys.
   Background gradient is computed in JS from a hash of the book id so
   the same book always gets the same color across reloads.
   Container-query sizing keeps typography legible at every poster
   width — grid thumbnail through modal hero. */
.book-placeholder {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  padding: 12% 9%;
  color: #F5F2EA;
  font-family: Georgia, 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', serif;
  /* Inner double-ring evokes a printed cover; opaque enough to read on
     every gradient in the palette without dominating it. */
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.10),
    inset 0 0 0 4px rgba(0, 0, 0, 0.14);
  container-type: inline-size;
  overflow: hidden;
}
.book-placeholder-title {
  font-size: clamp(11px, 9cqw, 26px);
  font-weight: 600;
  line-height: 1.18;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-bottom: 0.5em;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
  word-break: break-word;
  hyphens: auto;
}
.book-placeholder-author {
  font-size: clamp(9px, 5.5cqw, 15px);
  font-weight: 400;
  line-height: 1.3;
  opacity: 0.82;
  font-style: italic;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}

/* #909 - Shared layout class for converted cover surfaces (modal, profile, party,
   pick overlay, recap). Fills the positioned parent exactly like .swipe-cover-img
   does on the swipe surface. All per-type reveal classes (.tv-cover-fade,
   .game-cover-fade, .book-cover-fade) are also valid on .cover-img. */
.cover-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  background-color: var(--surface-2);
}

/* #862 - book cover bold page-turn flip reveal (replaces #860 subtle settle) */
.poster-img.book-cover-fade,
.cover-img.book-cover-fade {
  opacity: 0;
  transform: rotateY(-88deg);
  transform-origin: left center;
  backface-visibility: hidden;
  transition: opacity 0.15s linear, transform 0.82s cubic-bezier(0.16, 1, 0.3, 1);
}
.poster-img.book-cover-fade.loaded,
.cover-img.book-cover-fade.loaded { opacity: 1; transform: rotateY(0deg); }
.swipe-book-cover {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; background-color: var(--surface-2); display: block;
  opacity: 0;
  transform: rotateY(-88deg);
  transform-origin: left center;
  backface-visibility: hidden;
  transition: opacity 0.15s linear, transform 0.82s cubic-bezier(0.16, 1, 0.3, 1);
}
.swipe-book-cover.loaded { opacity: 1; transform: rotateY(0deg); }

/* #899 - Watch (movie+tv) and Game grid card placeholders + reveal animations.
   Derived from #897; durations bumped so the reveal is clearly visible at normal
   scroll speed. All transitions use clip-path + opacity only (GPU-compositable). */

/* Watch placeholder: scanline-textured gradient shown behind the cover during load. */
.watch-placeholder {
  position: absolute;
  inset: 0;
  overflow: hidden;
  container-type: inline-size;
  background:
    repeating-linear-gradient(
      180deg,
      rgba(0,0,0,0.07) 0px,
      rgba(0,0,0,0.07) 1px,
      transparent 1px,
      transparent 3px
    ),
    linear-gradient(180deg, var(--surface-3) 0%, var(--surface-2) 100%);
}
.watch-placeholder::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: clamp(22px, 5cqw, 36px); height: clamp(22px, 5cqw, 36px);
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M6%2020.25h12m-7.5-3v3m3-3v3m-10.125-3h17.25c.621%200%201.125-.504%201.125-1.125V4.875c0-.621-.504-1.125-1.125-1.125H3.375c-.621%200-1.125.504-1.125%201.125v11.25c0%20.621.504%201.125%201.125%201.125Z%22%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M6%2020.25h12m-7.5-3v3m3-3v3m-10.125-3h17.25c.621%200%201.125-.504%201.125-1.125V4.875c0-.621-.504-1.125-1.125-1.125H3.375c-.621%200-1.125.504-1.125%201.125v11.25c0%20.621.504%201.125%201.125%201.125Z%22%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
  opacity: 0.18;
}

/* Game placeholder: pixel-grid on a flat surface shown behind the cover during load. */
.game-placeholder {
  position: absolute;
  inset: 0;
  overflow: hidden;
  container-type: inline-size;
  background:
    repeating-linear-gradient(
      90deg,
      rgba(212,175,55,0.06) 0px,
      rgba(212,175,55,0.06) 1px,
      transparent 1px,
      transparent 8px
    ),
    repeating-linear-gradient(
      180deg,
      rgba(212,175,55,0.06) 0px,
      rgba(212,175,55,0.06) 1px,
      transparent 1px,
      transparent 8px
    ),
    var(--surface-2);
}
@media (prefers-color-scheme: light) {
  .game-placeholder {
    background:
      repeating-linear-gradient(
        90deg,
        rgba(123,44,44,0.06) 0px,
        rgba(123,44,44,0.06) 1px,
        transparent 1px,
        transparent 8px
      ),
      repeating-linear-gradient(
        180deg,
        rgba(123,44,44,0.06) 0px,
        rgba(123,44,44,0.06) 1px,
        transparent 1px,
        transparent 8px
      ),
      var(--surface-2);
  }
}
.game-placeholder::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: clamp(22px, 5cqw, 36px); height: clamp(22px, 5cqw, 36px);
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M6%206.878V6a2.25%202.25%200%200%201%202.25-2.25h7.5A2.25%202.25%200%200%201%2018%206v.878m-12%200c.235-.083.487-.128.75-.128h10.5c.263%200%20.515.045.75.128m-12%200A2.25%202.25%200%200%200%204.5%209v.878m13.5-3A2.25%202.25%200%200%201%2019.5%209v.878m0%200a2.246%202.246%200%200%200-.75-.128H5.25c-.263%200-.515.045-.75.128m15%200A2.25%202.25%200%200%201%2021%2012v6a2.25%202.25%200%200%201-2.25%202.25H5.25A2.25%202.25%200%200%201%203%2018v-6c0-.98.626-1.813%201.5-2.122%22%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M6%206.878V6a2.25%202.25%200%200%201%202.25-2.25h7.5A2.25%202.25%200%200%201%2018%206v.878m-12%200c.235-.083.487-.128.75-.128h10.5c.263%200%20.515.045.75.128m-12%200A2.25%202.25%200%200%200%204.5%209v.878m13.5-3A2.25%202.25%200%200%201%2019.5%209v.878m0%200a2.246%202.246%200%200%200-.75-.128H5.25c-.263%200-.515.045-.75.128m15%200A2.25%202.25%200%200%201%2021%2012v6a2.25%202.25%200%200%201-2.25%202.25H5.25A2.25%202.25%200%200%201%203%2018v-6c0-.98.626-1.813%201.5-2.122%22%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
  opacity: 0.22;
}

/* #900 - TV/movie CRT center-expand reveal. opacity leads slightly then the clip-path expands.
   Duration: 0.54s (clip-path) and 0.15s (opacity) — halved from 1.4s for snappier feel. */
.poster-img.tv-cover-fade,
.swipe-cover-img.tv-cover-fade,
.cover-img.tv-cover-fade {
  opacity: 0;
  clip-path: inset(50% 0 50% 0);
}
/* #907 - Keyframe (not transition) so the cover stays clipped to the centre line
   while the white dot->line forms, then SPLITS OPEN in sync with the line opening
   — the dot/line lead, the picture follows. (A transition delay would be clobbered
   by the stagger transition-delay wireBookCoverFade sets inline.) */
.poster-img.tv-cover-fade.loaded,
.swipe-cover-img.tv-cover-fade.loaded,
.cover-img.tv-cover-fade.loaded {
  opacity: 1;
  animation: tvImgReveal 0.54s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes tvImgReveal {
  0%   { clip-path: inset(50% 0 50% 0); }  /* centre line — picture hidden */
  44%  { clip-path: inset(50% 0 50% 0); }  /* hold while the white dot->line forms */
  100% { clip-path: inset(0% 0 0% 0); }    /* split open, in sync with the line */
}

/* #907 - Game cover reveal: the image pops in and GLITCH-SHAKES slightly on load
   (choppy translate/skew jumps) while the pixel overlay twinkles over it, then
   settles sharp. Keyframe + steps() for the glitchy, non-smooth motion. */
.poster-img.game-cover-fade,
.swipe-cover-img.game-cover-fade,
.cover-img.game-cover-fade {
  opacity: 0;
}
.poster-img.game-cover-fade.loaded,
.swipe-cover-img.game-cover-fade.loaded,
.cover-img.game-cover-fade.loaded {
  opacity: 1;
  animation: gameImgIn 0.45s steps(1, end) forwards;
}
@keyframes gameImgIn {
  0%   { opacity: 0; transform: translate(0, 0) skewX(0deg);   filter: contrast(1.4) saturate(1.5); }
  12%  { opacity: 1; transform: translateX(-3%) skewX(-2deg);  filter: contrast(1.3) saturate(1.4); }
  28%  { transform: translate(4%, -1%) skewX(1.5deg);          filter: contrast(1.2); }
  44%  { transform: translateX(-2%) skewX(-1deg); }
  62%  { transform: translate(2%, 1%) skewX(0.5deg); }
  80%  { transform: translateX(-1%);                           filter: none; }
  100% { opacity: 1; transform: translate(0, 0) skewX(0deg);   filter: none; }
}

/* #908 - Game cover "pixel glitch" overlay: scattered pixels covering the WHOLE
   card, appearing/disappearing at DIFFERENT times and in seemingly-RANDOM colours.
   Three sparse pixel layers (element + ::before + ::after), each a single-quadrant
   conic grid (3/4 of every cell transparent so the cover shows through) at
   different sizes/offsets. Each layer flickers, shifts and HUE-ROTATES on its own
   rhythm (per-frame hue jumps => random-looking colours, compounded across the
   three layers). The whole field also tilts in step with the image (gameGlitchTilt
   mirrors gameImgIn). Self-destructing (all end opacity:0, pointer-events:none). */
/* #910 - WAITING state (image not yet loaded): the pixel field LOOPS so games
   "keep glitching until we have the image". When the cover img gains .loaded the
   sibling selectors below switch each layer to a one-shot resolve (gpxA/B/C end
   at opacity:0) plus a tilt synced with the image shake. */
.game-glitch {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background-image: repeating-conic-gradient(#ff2d55 0deg 90deg, transparent 90deg 360deg);
  background-size: 34px 34px;
  opacity: 0;
  animation: gpxLoopA 0.42s steps(1, end) infinite;
}
.game-glitch::before,
.game-glitch::after {
  content: '';
  position: absolute;
  inset: 0;
  mix-blend-mode: screen;
}
.game-glitch::before {
  background-image: repeating-conic-gradient(#0a84ff 0deg 90deg, transparent 90deg 360deg);
  background-size: 28px 28px;
  background-position: 11px 6px;
  opacity: 0;
  animation: gpxLoopB 0.52s steps(1, end) infinite;
}
.game-glitch::after {
  background-image: repeating-conic-gradient(#30d158 0deg 90deg, transparent 90deg 360deg);
  background-size: 42px 42px;
  background-position: 6px 17px;
  opacity: 0;
  animation: gpxLoopC 0.48s steps(1, end) infinite;
}
/* Loaded => stop looping, final flicker + resolve to transparent, tilt w/ image. */
.game-cover-fade.loaded ~ .game-glitch {
  animation: gpxA 0.45s steps(1, end) forwards, gameGlitchTilt 0.45s steps(1, end) forwards;
}
.game-cover-fade.loaded ~ .game-glitch::before {
  animation: gpxB 0.45s steps(1, end) forwards;
}
.game-cover-fade.loaded ~ .game-glitch::after {
  animation: gpxC 0.45s steps(1, end) forwards;
}
/* Resolve flickers (one-shot, end transparent) — used once the image loads. */
@keyframes gpxA {
  0% { opacity: .5; filter: hue-rotate(0deg); } 18% { opacity: 0; }
  33% { opacity: .5; background-position: 9px 4px; filter: hue-rotate(140deg); }
  52% { opacity: .12; } 70% { opacity: .45; background-position: -7px 12px; filter: hue-rotate(285deg); }
  88% { opacity: .1; filter: hue-rotate(60deg); } 100% { opacity: 0; }
}
@keyframes gpxB {
  0% { opacity: 0; } 14% { opacity: .5; background-position: 11px 6px; filter: hue-rotate(205deg); }
  38% { opacity: .1; } 58% { opacity: .45; background-position: 24px -6px; filter: hue-rotate(40deg); }
  82% { opacity: .18; filter: hue-rotate(320deg); } 100% { opacity: 0; }
}
@keyframes gpxC {
  0% { opacity: .4; filter: hue-rotate(95deg); } 24% { opacity: .12; background-position: 6px 17px; filter: hue-rotate(250deg); }
  48% { opacity: .5; background-position: -13px 9px; filter: hue-rotate(10deg); }
  74% { opacity: .1; filter: hue-rotate(175deg); } 100% { opacity: 0; }
}
/* Waiting loops (seamless: 0% == 100%, stay visible) — run until the image loads. */
@keyframes gpxLoopA {
  0% { opacity: .42; background-position: 0 0; filter: hue-rotate(0deg); }
  25% { opacity: .08; } 50% { opacity: .42; background-position: 9px 5px; filter: hue-rotate(180deg); }
  75% { opacity: .1; } 100% { opacity: .42; background-position: 0 0; filter: hue-rotate(360deg); }
}
@keyframes gpxLoopB {
  0% { opacity: .1; background-position: 11px 6px; filter: hue-rotate(200deg); }
  30% { opacity: .42; background-position: 20px -4px; } 55% { opacity: .08; filter: hue-rotate(40deg); }
  80% { opacity: .4; background-position: 4px 12px; } 100% { opacity: .1; background-position: 11px 6px; filter: hue-rotate(560deg); }
}
@keyframes gpxLoopC {
  0% { opacity: .38; background-position: 6px 17px; filter: hue-rotate(95deg); }
  35% { opacity: .1; background-position: -10px 8px; } 65% { opacity: .42; background-position: 14px -6px; filter: hue-rotate(280deg); }
  100% { opacity: .38; background-position: 6px 17px; filter: hue-rotate(455deg); }
}
/* Tilt/jitter the whole pixel field in step with the image (mirrors gameImgIn). */
@keyframes gameGlitchTilt {
  0%   { transform: translate(0, 0) skewX(0deg); }
  12%  { transform: translateX(-3%) skewX(-2deg); }
  28%  { transform: translate(4%, -1%) skewX(1.5deg); }
  44%  { transform: translateX(-2%) skewX(-1deg); }
  62%  { transform: translate(2%, 1%) skewX(0.5deg); }
  80%  { transform: translateX(-1%); }
  100% { transform: translate(0, 0) skewX(0deg); }
}

/* #900 - CRT "tube powering on" flash for TV reveals.
   A ::before pseudo-element sits over the placeholder at vertical center.
   When the img gets .loaded the ::before fades from 0 -> 0.85 -> 0 over 120ms.
   This keyframe runs on the placeholder sibling (which stays visible until the
   img itself covers it). We target .watch-placeholder::before because the
   .watch-placeholder is always present when a tv-cover-fade img exists.
   Implemented as a GPU-cheap opacity animation on a centered white bar. */
/* #905 - CRT "power on" sequence: a white dot appears at center, stretches into a
   thin horizontal line, then opens vertically while fading as the picture unfolds.
   Hosted on a .tv-crt overlay placed AFTER the cover <img> (so the general-sibling
   trigger fires on grid + swipe). Self-destructing (ends opacity:0,
   pointer-events:none) so it can never hide the cover. Triggered by the img
   gaining .loaded, so it syncs with image load. */
.tv-crt {
  position: absolute; inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 2;
}
.tv-crt::before {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 100%; height: 4px;
  background: #fff;
  box-shadow: 0 0 12px 3px rgba(255, 255, 255, 0.9);
  transform: translate(-50%, -50%) scale(0.04, 1);
  opacity: 0;
  /* #910 - WAITING (image not yet loaded): a small white dot pulses in the
     middle as a "still loading" flare. Replaced by the power-on once .loaded. */
  animation: crtDotFlare 1.2s ease-in-out infinite;
}
@keyframes crtDotFlare {
  0%, 100% { opacity: .45; transform: translate(-50%, -50%) scale(0.04, 1); }
  50%      { opacity: 1;   transform: translate(-50%, -50%) scale(0.07, 1.5); }
}
.tv-cover-fade.loaded ~ .tv-crt::before {
  animation: crtPowerOn 0.54s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes crtPowerOn {
  0%   { opacity: 1; transform: translate(-50%, -50%) scale(0.04, 1); }        /* white dot */
  14%  { opacity: 1; transform: translate(-50%, -50%) scale(0.04, 1); }        /* hold the dot */
  44%  { opacity: 1; transform: translate(-50%, -50%) scaleX(1) scaleY(1); }   /* stretch to a thin line */
  56%  { opacity: 0.7; transform: translate(-50%, -50%) scaleX(1) scaleY(2.5); } /* line begins to open */
  100% { opacity: 0; transform: translate(-50%, -50%) scaleX(1) scaleY(40); }  /* opens + fades, revealing the picture */
}

/* #912 - Cover already available (cached/decoded, or already revealed this
   session): show it instantly with NO reveal animation, and suppress the
   loading/reveal overlay. wireBookCoverFade adds .cover-instant in that case.
   Applies to every type (book flip / tv CRT / game glitch) on every surface. */
.book-cover-fade.cover-instant,
.tv-cover-fade.cover-instant,
.game-cover-fade.cover-instant {
  animation: none !important;
  transition: none !important;
  opacity: 1 !important;
  clip-path: none !important;
  filter: none !important;
  transform: none !important;
}
.tv-cover-fade.cover-instant ~ .tv-crt,
.game-cover-fade.cover-instant ~ .game-glitch {
  display: none !important;
}

/* Reduced-motion: skip all animation for both watch and game reveals, and suppress flash. */
@media (prefers-reduced-motion: reduce) {
  .poster-img.tv-cover-fade,
  .poster-img.game-cover-fade,
  .swipe-cover-img.tv-cover-fade,
  .swipe-cover-img.game-cover-fade,
  .cover-img.tv-cover-fade,
  .cover-img.game-cover-fade {
    animation: none !important;
    transition: none;
    clip-path: none;
    filter: none;
    transform: none;
    opacity: 1;
  }
  .poster-img.book-cover-fade,
  .cover-img.book-cover-fade {
    animation: none !important;
    transition: none;
    transform: none;
    opacity: 1;
  }
  .tv-crt,
  .game-glitch {
    display: none;
  }
}

/* #901b - Background-image div reveal via a pure-CSS ENTRANCE ANIMATION.
   The background-image is set synchronously by applyDataStyles (the proven
   pre-#900 behavior), and the element's DEFAULT state is fully VISIBLE
   (opacity:1, no clip-path/filter). The reveal is a one-shot keyframe entrance
   that runs on render — there is NO `.loaded` class and NO JS handshake, so the
   cover can never be left hidden.
   --tv and --game variants removed in #909: every cover surface now uses a real
   <img> with the typed reveal class (tv-cover-fade / game-cover-fade) wired via
   wireBookCoverFade. --plain stays for large hero backdrops (modal, party) that
   are not card covers and do not use the img-reveal model. */
.cover-reveal-bg--plain {
  animation: cover-bg-plain 0.45s ease;
}

/* Plain fade: large backdrops + mixed/unknown-type surfaces. */
@keyframes cover-bg-plain {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .cover-reveal-bg--plain {
    animation: none !important;
  }
}

/* In Read and Play modes, hide streaming/movie-specific filters that don't
   apply to books or games. The For You mode toggle (Grid/Swipe) STAYS visible
   — swipe works for books and games too. */
body.cat-books .type-toggle,
body.cat-books #discoverType,
body.cat-books .filter-bar #discoverFilter,
body.cat-books .filter-bar > .meta,
body.cat-games .type-toggle,
body.cat-games #discoverType,
body.cat-games .filter-bar #discoverFilter,
body.cat-games .filter-bar > .meta {
  display: none !important;
}
/* watchlistFilter is also hidden in books (books have no availability concept)
   but in Play mode it stays visible — repurposed as the "On my platforms"
   filter against the user's owned-platforms list in Settings. */
body.cat-books #watchlistFilter { display: none !important; }
/* In books, availability is not modelled — hide the Availability filter
   sheet section (#947 moved it from the segmented into the sheet). The
   + Filter pill stays visible because the sheet carries Subjects/Year/
   Pages/Author for the Read category. */
body.cat-books .availability-section { display: none !important; }

/* `.books-only` filter sections appear inside the shared filter sheets
   (#discoverFilterSheet, #watchlistFilterSheet) but should only render
   when the Read category is active. The companion `.movies-only` /
   `.games-only` rules above already handle the inverse (Watch- and Play-
   only sections). #240. */
body:not(.cat-books) .books-only { display: none !important; }

/* #529 — list-detail scope reuses the Watchlist sheet but hides sections
   that don't apply inside a single custom list: the Lists chip strip
   (already inside one list), the Provider strip, and the Availability
   segmented. Selected by the `data-scope="list-detail"` attribute the
   sheet wears while opened from the list-detail surface. */
#watchlistFilterSheet[data-scope="list-detail"] #watchlistListFilterSection,
#watchlistFilterSheet[data-scope="list-detail"] #watchlistProviderSection,
#watchlistFilterSheet[data-scope="list-detail"] .availability-section {
  display: none !important;
}

/* Watched-side + Filter pill — books-only in #240, extended to movies in
   #239, and to games in #241. The row's visibility is now unconditional;
   the sheet body shows the right subset of sections via the
   .movies-only / .books-only / .games-only class rules. */
/* The +travel button is meaningless for games (no travel-region concept) */
body.cat-games #watchlistFilter button[data-filter="any"] { display: none !important; }

/* Settings sections that only apply to specific categories. .movies-only is
   the streaming-subs picker — useless in Play mode. .games-only is the new
   owned-platforms picker — only relevant in Play mode. */
body.cat-games .movies-only,
body:not(.cat-games) .games-only {
  display: none !important;
}

/* Discover panel mode driver (#181). The panel root carries data-mode="idle"
   when the search input is empty (browse view) or "searching" when it has a
   query. Use ":is" so a control flagged both .idle-only and .games-only still
   collapses correctly when either rule fires. */
#discoverPanel[data-mode="idle"] .searching-only,
#discoverPanel[data-mode="searching"] .idle-only {
  display: none !important;
}

/* Danger zone (#558): card uses a faintly red-tinted border so it reads as
   destructive without screaming. Heading red stays. */
.danger-section {
  border-color: color-mix(in srgb, var(--danger) 35%, var(--border));
  background: color-mix(in srgb, var(--danger) 5%, var(--surface-2));
}
.danger-section h3 { color: var(--danger); }

.app-version {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  font-weight: 500;
  color: var(--text-dim);
  margin-left: 4px;
}

/* #682 offline banner. Pinned to the top so it does not collide with the
   centred bottom update-banner. Same surface / border-radius vocabulary as
   the update-banner for visual consistency. */
/* #642 What's New toast — slides in from the top of the viewport, non-
   blocking. Sits above the offline banner (z 330) and below modals (400+)
   so a What's-New toast can coexist with the version-update banner. */
.whats-new-toast {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 12px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--surface);
  border: 1px solid var(--accent);
  padding: 10px 14px;
  border-radius: 12px;
  box-shadow: var(--shadow-2);
  font-size: 13px;
  z-index: 350;
  max-width: calc(100vw - 32px);
  animation: whatsNewToastIn 220ms ease-out;
}
@keyframes whatsNewToastIn {
  from { transform: translate(-50%, -16px); opacity: 0; }
  to   { transform: translate(-50%, 0);     opacity: 1; }
}
.whats-new-toast-icon { display: inline-flex; color: var(--accent); }
.whats-new-toast-icon svg { width: 18px; height: 18px; }
.whats-new-toast-msg { flex: 1 1 auto; }
.whats-new-toast-btn {
  padding: 4px 10px;
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  flex-shrink: 0;
}
.whats-new-toast-btn:hover { background: var(--accent-hover); }
.whats-new-toast-close {
  background: transparent;
  border: 0;
  color: var(--text-dim);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
  flex-shrink: 0;
}
.whats-new-toast-close:hover { color: inherit; }
/* What's New modal — opt-in card list of versions × highlights. */
.whats-new-modal {
  max-width: 520px;
  padding: 24px 24px 28px;
}
.whats-new-modal h2 {
  font-size: 20px;
  font-weight: 700;
  margin: 0 0 16px;
}
.whats-new-list {
  display: flex;
  flex-direction: column;
  gap: 18px;
  max-height: 60vh;
  overflow-y: auto;
}
.whats-new-version {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-2);
  padding: 14px 16px;
}
.whats-new-version-title {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-dim);
  margin: 0 0 10px;
  letter-spacing: 0.4px;
}
.whats-new-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.whats-new-step {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 13px;
  line-height: 1.4;
}
.whats-new-step strong {
  font-weight: 600;
  color: var(--text);
}
.whats-new-step span {
  color: var(--text-dim);
}

.offline-banner {
  position: fixed;
  top: env(safe-area-inset-top, 0px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--surface);
  border: 1px solid var(--accent);
  padding: 8px 12px;
  border-radius: 10px;
  margin-top: 8px;
  z-index: 330;
  font-size: 13px;
  box-shadow: var(--shadow-2);
  max-width: calc(100vw - 32px);
}
.offline-banner-btn {
  padding: 4px 10px;
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  flex-shrink: 0;
}
.offline-banner-btn:hover { background: var(--accent-hover); }

.update-banner {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--surface);
  border: 1px solid var(--accent);
  padding: 10px 14px;
  border-radius: 10px;
  z-index: 320;
  font-size: 14px;
  box-shadow: var(--shadow-2);
  max-width: calc(100vw - 32px);
}
.update-banner > span { flex: 1 1 auto; min-width: 0; }
.update-banner-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.update-banner-main > span { display: block; }
.update-banner-changelog-title {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.4px;
}
.update-banner-changelog-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-dim);
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 110px;
  overflow-y: auto;
}
.update-banner-changelog-list li { margin: 0; }
.update-banner-changelog-list strong {
  color: var(--text);
  font-weight: 600;
  margin-right: 4px;
}
.update-banner-btn {
  padding: 6px 12px;
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  flex-shrink: 0;
}
.update-banner-btn:hover { background: var(--accent-hover); }
.update-banner-close {
  background: transparent;
  border: 0;
  color: var(--text-dim);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
  flex-shrink: 0;
}
.update-banner-close:hover { color: inherit; }

/* Pull-to-refresh indicator (#406 Layer 4). Sits fixed above the top edge,
   slides down as the user pulls. Default state is invisible (opacity: 0,
   translateY(0)); src/ui/pull-to-refresh.js writes transform + opacity
   inline as the drag progresses. .ready highlights once the threshold has
   been crossed; .spinning runs a continuous rotation while the reload
   commits so the indicator doesn't visually freeze in the gap before the
   navigation tears the DOM down. .animating opts into a spring-back
   transition for the cancel path. */
.pull-to-refresh-indicator {
  position: fixed;
  top: -56px;
  left: 50%;
  margin-left: -22px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--surface-3);
  color: var(--text-dim);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  z-index: 320;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
}
.pull-to-refresh-indicator.ready {
  color: var(--accent);
  border-color: var(--accent);
}
.pull-to-refresh-indicator.animating {
  transition: transform 0.22s ease-out, opacity 0.22s ease-out;
}
.pull-to-refresh-indicator.spinning svg {
  animation: pull-to-refresh-spin 0.9s linear infinite;
}
@keyframes pull-to-refresh-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .pull-to-refresh-indicator.animating { transition: none; }
  .pull-to-refresh-indicator.spinning svg { animation: none; }
}
/* Desktop: pull-to-refresh is mobile-only (F5 / Cmd-R covers desktop), but
   defensive — make absolutely sure the indicator never paints on > 720px. */
@media (min-width: 721px) {
  .pull-to-refresh-indicator { display: none; }
}

.danger-btn {
  padding: 10px 20px;
  background: var(--danger-dim);
  border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent);
  color: var(--danger);
  border-radius: 8px;
  font-weight: 500;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.danger-btn:hover {
  background: color-mix(in srgb, var(--danger) 22%, transparent);
  border-color: var(--danger);
}

button.primary { display:inline-flex; align-items:center; justify-content:center; min-height:44px; padding:10px 18px; background:var(--accent); border:1px solid var(--accent); color:white; border-radius:8px; font-weight:500; font-size:14px; cursor:pointer; transition:background 0.15s, border-color 0.15s; }
button.primary:hover { background:var(--accent-hover); border-color:var(--accent-hover); }
button.primary:active { background:var(--accent-hover); }
button.primary:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
button.primary:disabled { opacity:0.5; cursor:default; pointer-events:none; }

.secondary { display:inline-flex; align-items:center; justify-content:center; min-height:44px; padding:10px 18px; background:var(--surface-2); border:1px solid var(--border); color:var(--text); border-radius:8px; font-weight:500; font-size:14px; cursor:pointer; transition:background 0.15s, border-color 0.15s; }
.secondary:hover { background:var(--surface-3); border-color:var(--accent); }
.secondary:active { background:var(--surface-3); }
.secondary:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.secondary:disabled { opacity:0.5; cursor:default; pointer-events:none; }

.account-section .api-row input[type="email"] {
  flex: 1;
  font-family: inherit;
  font-size: 14px;
}
.account-info {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
}
.account-info-label {
  font-size: 12px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.account-info-email {
  font-size: 15px;
  font-weight: 600;
  margin-top: 2px;
  word-break: break-all;
}
.account-info-meta {
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 4px;
}
#accountSignOutBtn {
  padding: 8px 14px;
  background: var(--surface-3);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  font-weight: 500;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
#accountSignOutBtn:hover {
  background: var(--surface);
  border-color: #ef4444;
  color: #ef4444;
}

.backup-actions {
  display: flex; gap: 8px; flex-wrap: wrap;
}

/* #615.1 push notifications: manual "Scan now" trigger + live result text. */
.push-test-row {
  display: flex; gap: 10px; align-items: center; margin-top: 12px;
  flex-wrap: wrap;
}
.push-test-row button {
  padding: 8px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  font-weight: 500;
  font-size: 13px;
  cursor: pointer;
}
.push-test-row button:hover { background: var(--surface-3); border-color: var(--accent); }
.push-test-row button:disabled { opacity: 0.5; cursor: default; }
.push-scan-status {
  font-size: 12px; color: var(--text-dim);
}
.backup-actions button {
  padding: 10px 18px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  font-weight: 500;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.backup-actions button:hover {
  background: var(--surface-3);
  border-color: var(--accent);
}

/* Danger zone rows (#560): stacked text + action layout for the
   consolidated Reset data + Delete account card in Data settings. */
.danger-section .danger-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 0;
  flex-wrap: wrap;
}
.danger-section .danger-row + .danger-row {
  border-top: 1px solid color-mix(in srgb, #f87171 18%, var(--border));
}
.danger-row-text {
  flex: 1 1 220px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.danger-row-text strong {
  font-size: 14px;
  color: var(--text);
}
.danger-row-text span {
  font-size: 12.5px;
  color: var(--text-dim);
  line-height: 1.4;
}
.danger-section .danger-row .danger-btn { flex-shrink: 0; }

/* Profile: signed-out hint sits in place of the form so the section is
   discoverable even before the user signs in. */
.profile-signin-hint {
  font-size: 13px;
  color: var(--text-dim);
  background: var(--surface-2);
  border: 1px dashed var(--border);
  border-radius: 8px;
  padding: 12px 14px;
}

.profile-form {
  display: flex; flex-direction: column; gap: 14px;
}
.profile-row {
  display: flex; flex-direction: column; gap: 6px;
}
.profile-row label {
  font-size: 12px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.profile-row input[type="text"],
.profile-row textarea,
.profile-row select {
  width: 100%;
  padding: 9px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
}
.profile-row textarea {
  min-height: 72px;
  resize: vertical;
}
.profile-row input:focus,
.profile-row textarea:focus,
.profile-row select:focus {
  outline: none; border-color: var(--accent);
}
.profile-username-row {
  display: flex; align-items: stretch; gap: 0;
}
.profile-username-prefix {
  display: inline-flex; align-items: center;
  padding: 0 10px;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-right: 0;
  border-radius: 8px 0 0 8px;
  color: var(--text-dim);
  font-size: 13px;
}
.profile-username-row input {
  border-radius: 0 8px 8px 0 !important;
  flex: 1;
}
.profile-field-status {
  font-size: 12px;
  min-height: 14px;
}
.profile-field-status.ok { color: #22c55e; }
.profile-field-status.err { color: #f87171; }
.profile-field-status.pending { color: var(--text-dim); }

.profile-username-auto-hint {
  font-size: 12px;
  color: var(--text-dim);
}

.profile-bio-counter {
  font-size: 11px;
  color: var(--text-dim);
  text-align: right;
}

/* Profile avatar: prominent on top of the editing form (#345) — shown
   above username/display-name/bio so the user can see what they're
   editing without flipping to header / public profile to confirm.
   Stack vertically (preview centered, action buttons row beneath) so
   the preview gets the full row width's visual prominence. The wider
   .desktop-only-stack utility breaks layout in narrow viewports, so
   this stays column-flex everywhere. */
.profile-avatar-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.profile-avatar-preview {
  width: 96px; height: 96px;
  border-radius: 50%;
  background: var(--surface-3);
  border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  color: var(--text-dim);
  font-size: 36px; font-weight: 600;
  flex-shrink: 0;
  cursor: pointer; /* whole disc is now a tap target into the file picker */
}
.profile-avatar-preview:hover { border-color: var(--accent); }
.profile-avatar-preview:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.profile-avatar-preview img {
  width: 100%; height: 100%; object-fit: cover;
}
@media (min-width: 720px) {
  .profile-avatar-preview {
    width: 120px; height: 120px;
    font-size: 44px;
  }
}
.profile-avatar-actions {
  display: flex; gap: 8px; flex-wrap: wrap;
}
.profile-avatar-actions button {
  padding: 8px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  font-size: 13px; font-weight: 500;
  cursor: pointer;
}
.profile-avatar-actions button:hover {
  background: var(--surface-3);
  border-color: var(--accent);
}

.profile-actions {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 4px;
}
#profileSaveBtn {
  padding: 9px 16px;
  background: var(--accent);
  border: 1px solid var(--accent);
  color: #fff;
  border-radius: 8px;
  font-weight: 500;
  font-size: 14px;
  cursor: pointer;
}
.profile-save-status {
  font-size: 12px;
}
.profile-save-status.ok { color: #22c55e; }
.profile-save-status.err { color: #f87171; }
.profile-save-status.pending { color: var(--text-dim); }

.profile-public-lists {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 14px 10px;
  margin: 0;
}
.profile-public-lists legend {
  font-size: 12px; font-weight: 600;
  color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em;
  padding: 0 6px;
}
.profile-public-lists-hint {
  margin: 0 0 8px;
  font-size: 12px; color: var(--text-muted);
}
.profile-toggle {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 0;
  font-size: 13.5px; color: var(--text);
  cursor: pointer;
}
.profile-public-lists.disabled .profile-toggle {
  color: var(--text-dim); cursor: not-allowed;
}
.profile-toggle input[type="checkbox"] {
  width: 16px; height: 16px; cursor: inherit;
}

/* Friends 29a — "Add friend" modal */
.add-friend-modal { max-width: 520px; padding: 24px 28px 20px; }
.add-friend-modal h2 { margin: 0 0 14px; font-size: 20px; }
.add-friend-modal #friendSearchInput {
  width: 100%;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
  margin-bottom: 8px;
}
.add-friend-modal #friendSearchInput:focus {
  outline: none; border-color: var(--accent);
}
.friend-search-status {
  font-size: 12px; color: var(--text-dim);
  min-height: 16px;
  margin-bottom: 8px;
}
.friend-search-status.err { color: #f87171; }
.friend-search-status.pending { color: var(--text-dim); }
.friend-search-results {
  list-style: none; margin: 0; padding: 0;
  max-height: 320px; overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.friend-search-results li {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}
.friend-search-results li:last-child { border-bottom: 0; }
.friend-result-avatar {
  width: 40px; height: 40px; border-radius: 50%;
  background: var(--surface-3); background-size: cover; background-position: center;
  border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center;
  font-size: 16px; font-weight: 600; color: var(--text-dim);
  flex-shrink: 0;
}
.friend-result-meta { flex: 1; min-width: 0; }
.friend-result-name {
  font-size: 14px; font-weight: 500; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.friend-result-handle {
  font-size: 12px; color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.friend-result-action {
  padding: 7px 12px;
  background: var(--accent);
  color: #fff;
  border: 1px solid var(--accent);
  border-radius: 8px;
  font-size: 13px; font-weight: 500;
  cursor: pointer;
  flex-shrink: 0;
}
.friend-result-action:hover:not(:disabled) { background: var(--accent-hover); }
.friend-result-action:disabled {
  background: var(--surface-3); border-color: var(--border);
  color: var(--text-dim); cursor: default;
}
.friend-result-action.sent {
  background: var(--surface-3); border-color: var(--border); color: #22c55e;
}
.friend-result-actions { display: flex; gap: 6px; flex-shrink: 0; }
.friend-result-action.danger {
  background: transparent; color: var(--danger); border-color: var(--border);
}
.friend-result-action.danger:hover:not(:disabled) { background: var(--surface-3); }

/* Friends 29b (#105) — pending-requests inbox in Settings + banner rows. */
.friends-subhead {
  font-size: 13px; font-weight: 600; color: var(--text-dim);
  text-transform: uppercase; letter-spacing: 0.04em;
  margin: 0 0 6px;
}
.friend-inbox-list {
  list-style: none; margin: 8px 0 12px; padding: 0;
  border: 1px solid var(--border); border-radius: 8px;
}
.friend-inbox-list li {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}
.friend-inbox-list li:last-child { border-bottom: 0; }
.friend-inbox-actions { display: flex; gap: 6px; flex-shrink: 0; }
.friend-inbox-action {
  padding: 6px 10px;
  border-radius: 8px;
  font-size: 13px; font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--border);
}
.friend-inbox-action.accept {
  background: var(--accent); color: #fff; border-color: var(--accent);
}
.friend-inbox-action.accept:hover:not(:disabled) { background: var(--accent-hover); }
.friend-inbox-action.decline {
  background: transparent; color: var(--text);
}
.friend-inbox-action.decline:hover:not(:disabled) { background: var(--surface-3); }
.friend-inbox-action.danger {
  background: transparent; color: var(--danger);
}
.friend-inbox-action.danger:hover:not(:disabled) { background: var(--surface-3); }
.friend-inbox-action:disabled { opacity: 0.6; cursor: default; }
.friend-inbox-empty { font-size: 13px; color: var(--text-dim); margin: 6px 0 12px; }

/* People-you-may-know carousel inside the friends modal (#718). Horizontal
 * scroll strip rendered between the pending-requests inbox (#654) and the
 * main friends list. Cards are clickable and send a friend request via the
 * existing flow; the chip below the name surfaces the ranking signal
 * (mutual count first, then shared-rating fallback). */
.friends-suggestions-section { margin: 8px 0 12px; }
.friends-suggestions-section[hidden] { display: none; }
.friends-suggestions-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 4px 2px 8px;
  display: flex;
  gap: 10px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
}
.friends-suggestions-list li {
  flex: 0 0 120px;
  scroll-snap-align: start;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  text-align: center;
  min-height: 168px;
}
.friends-suggestion-card {
  appearance: none;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  width: 100%;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  color: inherit;
  font: inherit;
}
.friends-suggestion-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 8px;
}
.friends-suggestion-card .friend-result-avatar {
  width: 56px;
  height: 56px;
  font-size: 22px;
}
.friends-suggestion-name {
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
  width: 100%;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}
.friends-suggestion-chip {
  font-size: 11px;
  color: var(--text-dim);
  background: var(--surface-2);
  border-radius: 999px;
  padding: 3px 8px;
  line-height: 1.2;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.friends-suggestion-action {
  appearance: none;
  margin-top: 4px;
  padding: 7px 10px;
  min-height: 44px;
  border-radius: 999px;
  border: 1px solid var(--accent);
  background: var(--accent);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  width: 100%;
}
.friends-suggestion-action:hover:not(:disabled) { background: var(--accent-hover); }
.friends-suggestion-action.sent {
  background: transparent;
  color: var(--accent);
  border-color: var(--accent);
  cursor: default;
}
.friends-suggestion-action:disabled { opacity: 0.8; cursor: default; }

.friend-banner-list { list-style: none; margin: 4px 0 0; padding: 0; }
.friend-banner-row {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 6px 0;
  border-top: 1px solid rgba(255,255,255,0.06);
}
.friend-banner-row:first-child { border-top: 0; padding-top: 2px; }
.friend-banner-text { flex: 1 1 auto; min-width: 0; font-size: 13px; }
.friend-banner-action {
  padding: 5px 10px;
  border-radius: 8px;
  font-size: 12px; font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--border);
  flex-shrink: 0;
}
.friend-banner-action.accept {
  background: var(--accent); color: #fff; border-color: var(--accent);
}
.friend-banner-action.accept:hover:not(:disabled) { background: var(--accent-hover); }
.friend-banner-action.decline {
  background: transparent; color: var(--text);
}
.friend-banner-action.decline:hover:not(:disabled) { background: rgba(255,255,255,0.05); }
.friend-banner-action:disabled { opacity: 0.6; cursor: default; }

/* Friends 29c (#106) — header icon. Visual treatment lives on the shared
   .header-icon-btn base (#550). */

/* Notification inbox bell + panel (#175). Lives inside .header-right; the
 * slot wraps the bell so the panel can anchor below it via the slot's
 * `position: relative`. Bell shape is inherited from .header-icon-btn
 * (#550); the .notification-bell rule only carries badge anchoring. */
.notification-bell-slot {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* The badge at top:-4px;right:-4px sticks past the bell's right edge,
     so the 4px .header-right gap leaves the badge visually touching the
     avatar pill. Add breathing room when the avatar follows. (#665) */
  margin-right: 6px;
}
.notification-bell-slot[hidden] { display: none; }
.notification-bell { position: relative; }
.notification-bell-badge {
  position: absolute;
  top: -4px; right: -4px;
  min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 8px;
  background: var(--accent);
  color: var(--bg);
  font-size: 10px; font-weight: 700; line-height: 16px;
  text-align: center;
  pointer-events: none;
}
.notification-bell-badge[hidden] { display: none; }
.notification-inbox-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: min(360px, 92vw);
  max-height: min(480px, 80vh);
  overflow: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-2);
  z-index: 1000;
  padding: 8px 0 4px;
}
.notification-inbox-panel[hidden] { display: none; }
.inbox-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 6px 12px 10px; border-bottom: 1px solid var(--border);
}
.inbox-title { margin: 0; font-size: 14px; font-weight: 600; }
.inbox-actions { display: inline-flex; align-items: center; gap: 4px; }
.inbox-action {
  background: transparent; border: none; color: var(--text-dim);
  font-size: 11px; cursor: pointer; padding: 4px 6px; border-radius: 6px;
}
.inbox-action:hover { color: var(--accent); background: rgba(255,255,255,0.05); }
.inbox-close {
  background: transparent; border: none; color: var(--text-dim);
  font-size: 16px; cursor: pointer; padding: 2px 6px; border-radius: 6px;
  line-height: 1;
}
.inbox-close:hover { color: var(--text); background: rgba(255,255,255,0.05); }
.inbox-empty {
  padding: 24px 16px; text-align: center; color: var(--text-dim); font-size: 13px;
}
.inbox-list { display: flex; flex-direction: column; }
.inbox-row {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 10px 12px;
  background: transparent; border: none; color: var(--text);
  text-align: left; cursor: pointer;
  border-bottom: 1px solid rgba(255,255,255,0.04);
  position: relative;
}
.inbox-row:last-child { border-bottom: none; }
.inbox-row:hover { background: rgba(255,255,255,0.04); }
.inbox-row-icon {
  flex-shrink: 0; width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--accent);
}
.inbox-row-content {
  display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0;
}
.inbox-row-title { font-size: 13px; font-weight: 600; line-height: 1.3; }
.inbox-row-body { font-size: 12px; color: var(--text-dim); line-height: 1.3; }
.inbox-row-age { font-size: 10px; color: var(--text-dim); margin-top: 2px; }
.inbox-row-dot {
  position: absolute; top: 14px; right: 12px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent);
}
.inbox-row--unread { background: rgba(255, 215, 90, 0.04); }
@media (max-width: 600px) {
  .notification-bell { width: 28px; height: 28px; }
  .notification-bell svg { width: 18px; height: 18px; }
  .notification-inbox-panel {
    position: fixed;
    top: 56px; right: 8px; left: 8px;
    width: auto;
    max-height: min(80vh, calc(100vh - 80px));
  }
}
@media (max-width: 380px) {
  .notification-bell { width: 26px; height: 26px; border-width: 1.5px; }
  .notification-bell svg { width: 16px; height: 16px; }
}

/* Friends list modal — list of confirmed friends with View profile + kebab. */
.friends-modal { max-width: 540px; padding: 22px 24px 18px; }
.friends-modal-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 14px;
}
.friends-modal-header h2 { margin: 0; font-size: 20px; }
.friends-modal-add {
  padding: 7px 12px;
  background: var(--accent); color: #fff;
  border: 1px solid var(--accent);
  border-radius: 8px;
  font-size: 13px; font-weight: 500;
  cursor: pointer;
  flex-shrink: 0;
}
.friends-modal-add:hover { background: var(--accent-hover); }
.friends-modal-status {
  font-size: 13px; color: var(--text-dim);
  margin: 4px 0 8px;
}
.friends-modal-status.err { color: #f87171; }
.friends-list {
  list-style: none; margin: 0; padding: 0;
  max-height: 60vh; overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.friends-list li {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  position: relative;
}
.friends-list li:last-child { border-bottom: 0; }
.friend-row-link {
  display: flex; align-items: center; gap: 12px;
  flex: 1; min-width: 0;
  text-decoration: none; color: inherit;
  cursor: pointer;
}
.friend-row-link:hover .friend-result-name { color: var(--accent); }
.friend-row-arrow {
  color: var(--text-dim);
  flex-shrink: 0;
  transition: transform 0.12s, color 0.12s;
}
.friend-row-link:hover .friend-row-arrow {
  color: var(--accent);
  transform: translateX(2px);
}
.friend-row-kebab-wrap { position: relative; flex-shrink: 0; }
.friend-row-kebab {
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--text-dim);
  cursor: pointer;
}
.friend-row-kebab:hover, .friend-row-kebab[aria-expanded="true"] {
  background: var(--surface-3);
  color: var(--text);
}
.friend-row-menu {
  position: absolute;
  top: 100%; right: 0;
  margin-top: 4px;
  min-width: 140px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 18px rgba(0,0,0,0.28);
  z-index: 10;
  padding: 4px;
}
.friend-row-menu[hidden] { display: none; }
.friend-row-menu-item {
  display: block;
  width: 100%;
  padding: 8px 10px;
  background: transparent;
  border: 0;
  color: var(--text);
  font-size: 13px;
  text-align: left;
  border-radius: 6px;
  cursor: pointer;
}
.friend-row-menu-item:hover { background: var(--surface-3); }
.friend-row-menu-item.danger { color: var(--danger); }
.friends-empty {
  padding: 28px 16px;
  text-align: center;
  color: var(--text-dim);
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: var(--surface-2);
}
.friends-empty-title {
  display: block;
  color: var(--text); font-weight: 600; font-size: 15px;
  margin-bottom: 4px;
}
.friends-empty-cta {
  margin-top: 12px;
  padding: 8px 14px;
  background: var(--accent); color: #fff;
  border: 1px solid var(--accent);
  border-radius: 8px;
  font-size: 13px; font-weight: 500;
  cursor: pointer;
}
.friends-empty-cta:hover { background: var(--accent-hover); }

/* #723 Friend activity feed — tabs + combined timeline inside the Friends modal. */
.friends-modal-tabs {
  display: flex; gap: 4px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.friends-modal-tab {
  appearance: none;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: 8px 4px 10px;
  margin-bottom: -1px;
  min-height: 44px;
  font-size: 14px; font-weight: 500;
  color: var(--text-dim);
  cursor: pointer;
  flex: 1;
}
.friends-modal-tab:hover { color: var(--text); }
.friends-modal-tab[aria-selected="true"] {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
.friends-tab-panel[hidden] { display: none; }

.friends-activity-list {
  list-style: none; margin: 0; padding: 0;
  max-height: 60vh; overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.friends-activity-list[hidden] { display: none; }
.friends-activity-item {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}
.friends-activity-item:last-child { border-bottom: 0; }
.friends-activity-main {
  display: flex; align-items: center; gap: 12px;
  flex: 1; min-width: 0;
  text-decoration: none; color: inherit;
  cursor: pointer;
}
.friends-activity-avatar { width: 36px; height: 36px; font-size: 14px; }
.friends-activity-body { flex: 1; min-width: 0; }
.friends-activity-action {
  display: block;
  font-size: 14px; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.friends-activity-name { font-weight: 600; color: var(--text); }
.friends-activity-main:hover .friends-activity-action { color: var(--accent); }
.friends-activity-age {
  display: block;
  font-size: 12px; color: var(--text-dim);
  margin-top: 1px;
}
.friends-activity-cover {
  width: 34px; height: 50px;
  border-radius: 5px;
  background: var(--surface-3); background-size: cover; background-position: center;
  border: 1px solid var(--border);
  flex-shrink: 0;
}
.friends-activity-reactions {
  display: flex; align-items: center; gap: 4px;
  flex-shrink: 0;
}
.friends-activity-react {
  appearance: none;
  display: inline-flex; align-items: center; gap: 3px;
  min-width: 44px; min-height: 44px;
  justify-content: center;
  padding: 0 6px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--text-dim);
  font-size: 12px; font-weight: 500;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.friends-activity-react:hover { background: var(--surface-3); color: var(--text); }
.friends-activity-react.active {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-color: var(--accent);
  color: var(--accent);
}
.friends-activity-react-count { font-variant-numeric: tabular-nums; }

/* Narrow viewports: the single flex row can't fit avatar + body + cover +
   three 44pt reaction buttons, so the title ellipsis-truncates to a few
   characters. Stack the three reactions onto a second row beneath the body,
   giving the name + title the full width. Tap targets stay at 44pt. */
@media (max-width: 520px) {
  .friends-activity-item { align-items: stretch; flex-wrap: wrap; padding: 10px 12px 12px; }
  .friends-activity-main { width: 100%; }
  /* Let the name + title wrap to two lines instead of truncating after a few
     characters: with reactions on their own row the body now has the full
     width to use. */
  .friends-activity-action {
    white-space: normal;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    line-height: 1.35;
  }
  .friends-activity-age { margin-top: 4px; }
  .friends-activity-reactions {
    width: 100%;
    justify-content: flex-start;
    /* Align buttons under the body text: 36px avatar + 12px gap. */
    padding-left: 48px;
    margin-top: -4px;
  }
}

/* Unfriend confirm modal — small, two-button. */
.unfriend-confirm-modal { max-width: 420px; padding: 22px 24px; }
.unfriend-confirm-modal h2 { margin: 0 0 10px; font-size: 18px; }
.unfriend-confirm-modal p { margin: 0 0 14px; font-size: 14px; line-height: 1.5; color: var(--text); }

/* Public profile modal (route: /u/<username>) — 28b */
.profile-modal { max-width: 640px; padding: 28px 32px 24px; }
/* Block + share + close right-cluster behaviour now comes from the base
 * `.modal-float-row` rule (see #571) — this overlay-specific override was
 * removed when the base rule adopted `justify-content: flex-end; gap: 8px`. */
/* #542 — block icon stays neutral at rest (matches share/close), turns red
   on hover/focus/active so the destructive intent reads clearly without
   alarming the user with a permanently-red button. */
.modal-block-floating:hover,
.modal-block-floating:focus-visible {
  background: rgba(208, 72, 72, 0.95) !important;
  border-color: rgba(208, 72, 72, 0.6) !important;
}
.profile-header {
  display: flex; align-items: center; gap: 16px; margin-bottom: 18px;
}
.profile-avatar {
  width: 88px; height: 88px; border-radius: 50%;
  flex-shrink: 0;
  background: var(--surface-2);
  background-size: cover; background-position: center;
  border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center;
  font-size: 36px; font-weight: 600;
  color: var(--text-dim);
  overflow: hidden;
}
.profile-avatar.profile-avatar-placeholder {
  background-image: none;
}
.profile-name-block { min-width: 0; flex: 1; }
.profile-display-name {
  font-size: 22px; font-weight: 600; color: var(--text);
  margin: 0 0 2px;
  word-break: break-word;
}
.profile-handle {
  font-size: 13px; color: var(--text-dim);
  word-break: break-all;
}
.profile-bio {
  margin: 14px 0 4px;
  font-size: 14px; line-height: 1.55; color: var(--text);
  white-space: pre-wrap; word-wrap: break-word;
}
.profile-bio-empty {
  margin: 14px 0 4px;
  font-size: 13px; color: var(--text-muted); font-style: italic;
}
.profile-lists-placeholder {
  margin-top: 22px; padding: 18px;
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-dim);
  font-size: 13px;
  text-align: center;
}
.profile-lists-placeholder strong {
  display: block; color: var(--text); font-weight: 600; font-size: 14px; margin-bottom: 4px;
}
.profile-lists { margin-top: 22px; display: flex; flex-direction: column; gap: 14px; }
.profile-list-section h3 {
  margin: 0 0 10px;
  font-size: 14px; font-weight: 600;
  color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em;
}

/* Category tabs (Watch / Read / Play) on the public profile.
   Replaces the older "render only categories with items" stack so the
   modal always presents all three buckets, with empty states for those
   the friend hasn't tracked. */
.profile-tabs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  padding: 4px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  margin-bottom: 14px;
}
.profile-tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 10px;
  background: transparent;
  border: 0;
  border-radius: 8px;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
  font-family: inherit;
  min-width: 0;
}
.profile-tab svg { flex: 0 0 auto; }
.profile-tab span:not(.profile-tab-count) {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.profile-tab:hover { color: var(--text); }
.profile-tab.active {
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
}
.profile-tab-count {
  font-size: 11px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--surface-3, var(--border));
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.profile-tab.active .profile-tab-count {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--accent);
}
/* The active segmented button is a SOLID accent fill (gold in dark, burgundy
   in light) with a white label, so the at-rest accent-tinted pill would be
   invisible on it. On the active button the count flips to a dark translucent
   chip with white text — readable on any saturated accent, matching the
   button's own white label. */
.segmented button.active .toolbar-count {
  background: rgba(0, 0, 0, 0.24);
  color: #fff;
}

.profile-tab-panel { display: flex; flex-direction: column; gap: 16px; }
.profile-list-block { display: flex; flex-direction: column; }
.profile-list-block-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px; font-weight: 600;
  color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em;
  margin: 0 0 8px;
}
.profile-list-block-count {
  font-size: 11px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-dim);
  letter-spacing: 0;
  text-transform: none;
  font-weight: 500;
}
.profile-list-block-empty {
  padding: 16px 14px;
  border-radius: 10px;
  background: var(--surface-2);
  border: 1px dashed var(--border);
  text-align: center;
}
.profile-list-block-empty .profile-list-block-label {
  justify-content: center;
  margin-bottom: 4px;
}
.profile-list-block-empty-body {
  font-size: 12.5px;
  color: var(--text-dim);
  font-style: italic;
}

/* Legacy classes kept for any in-flight CSS targeting older renders. */
.profile-list-cat { margin-bottom: 14px; }
.profile-list-cat:last-child { margin-bottom: 0; }
.profile-list-cat-label {
  font-size: 12px; font-weight: 600;
  color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em;
  margin: 0 0 6px;
}
.profile-list-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 10px;
}
.profile-list-card {
  display: flex; flex-direction: column;
  border-radius: 8px; overflow: hidden;
  background: var(--surface-2); border: 1px solid var(--border);
  text-decoration: none; color: var(--text);
  transition: transform 120ms ease, border-color 120ms ease;
}
.profile-list-card:hover, .profile-list-card:focus-visible {
  transform: translateY(-2px);
  border-color: var(--accent);
  outline: none;
}
.profile-list-cover {
  width: 100%; aspect-ratio: 2 / 3;
  background-color: var(--surface-3, var(--surface-2));
  background-size: cover; background-position: center;
  position: relative; overflow: hidden;
  perspective: 1100px;
}
/* #858: absolute cover img fills the positioned parent (book branch only). */
.profile-list-cover-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.profile-list-cat[data-cat="games"] .profile-list-cover,
.profile-list-grid[data-cat="games"] .profile-list-cover { aspect-ratio: 16 / 9; }
.profile-list-card .profile-list-title {
  padding: 6px 8px;
  font-size: 11.5px; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Public item note (#179.2) — rendered under the title on a public profile
 * list card when the owner opted in. Small italic block, line-clamped to 3
 * so a long note doesn't dominate the grid. */
.profile-list-card .profile-list-note {
  padding: 0 8px 8px;
  font-size: 11px; line-height: 1.35;
  font-style: italic;
  color: var(--text-dim);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Mobile: profile modal is a bottom sheet (#545) — slides up from the
   bottom edge with a drag handle, matching settings / movie / rate. The
   `.modal-overlay.is-bottom-sheet > .modal` rules in the bottom-sheet
   primitive give max-height: 90vh + the slide animation; we only need
   the profile-specific layout tweaks below. */
@media (max-width: 560px) {
  .profile-modal {
    /* 90vh from the bottom-sheet primitive leaves ~10vh of viewport above
       the sheet — the status bar / notch sits in that gap, so no extra
       safe-area-top padding is needed. */
    padding: 8px 16px 16px;
    display: flex;
    flex-direction: column;
  }
  /* The floating action row sits below the drag handle. Drop sticky
     positioning so the row doesn't compete with the handle for top:0
     (both are position: sticky; top: 0 by default — the handle's higher
     z-index would cover the buttons). Negative margin-top eats the
     handle's -22px reclaim so buttons start cleanly at the top. */
  #profileModal .modal-float-row {
    position: relative;
    margin: 0 0 8px;
    padding: 0;
    min-height: 40px;
    z-index: 11;
  }
  .profile-modal #profileOk {
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    flex: 1 1 auto;
  }
  .profile-tabs { position: sticky; top: 0; z-index: 1; }
  .profile-tab { padding: 8px 6px; font-size: 12px; gap: 4px; }
  .profile-tab span:not(.profile-tab-count) { display: none; }
  .profile-tab.active span:not(.profile-tab-count) {
    display: inline;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .profile-list-grid {
    grid-template-columns: repeat(auto-fill, minmax(82px, 1fr));
    gap: 8px;
  }
}

.profile-state {
  text-align: center; padding: 32px 12px;
}
.profile-state h2 {
  font-size: 18px; font-weight: 600; margin: 0 0 6px; color: var(--text);
}
.profile-state p {
  font-size: 13.5px; color: var(--text-dim); margin: 0;
}
.profile-loading {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  padding: 40px 0;
  color: var(--text-dim); font-size: 13.5px;
}
/* Privacy policy modal */
.privacy-modal { max-width: 720px; padding: 28px 32px 24px; }
.privacy-modal h2 { margin: 0 0 4px; font-size: 22px; font-weight: 600; }
.privacy-modal .privacy-updated {
  font-size: 12px; color: var(--text-dim); margin: 0 0 20px;
}
.privacy-modal h3 {
  margin: 22px 0 6px; font-size: 15px; font-weight: 600; color: var(--text);
}
.privacy-modal p, .privacy-modal li {
  font-size: 13.5px; line-height: 1.55; color: var(--text);
}
.privacy-modal p { margin: 6px 0; }
.privacy-modal a { color: var(--accent); }
.privacy-modal-footer {
  margin-top: 24px;
  display: flex; justify-content: flex-end; align-items: center;
  gap: 12px;
}
.privacy-modal-report-link { margin-right: auto; }

.privacy-link {
  display: inline-block;
  margin-top: 6px;
  color: var(--accent);
  font-size: 12px;
  background: none; border: 0; padding: 0;
  cursor: pointer;
  text-decoration: underline;
}
.privacy-link:hover { color: var(--accent-hover); }

/* About section — beautified (#543) */
.about-section { padding: 0 !important; }

.about-hero {
  text-align: center;
  padding: 28px 24px 20px;
  border-bottom: 1px solid var(--border);
}
.about-hero-wordmark {
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -0.5px;
  color: var(--text);
  line-height: 1;
}
.about-hero-version {
  margin-top: 4px;
  font-size: 12px;
  color: var(--text-dim);
  letter-spacing: 0.2px;
}
.about-hero-tagline {
  margin: 10px 0 0;
  font-size: 13px;
  color: var(--text-dim);
  line-height: 1.4;
}

.about-attribution {
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.about-attr-card {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 8px;
  border-radius: 8px;
  text-decoration: none;
  color: var(--text);
  font-size: 13px;
  transition: background 0.12s;
}
.about-attr-card:hover { background: var(--surface-3); }
.about-attr-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--text-dim);
  flex-shrink: 0;
}
.about-attr-label { flex: 1; }
.about-attr-chevron { color: var(--text-dim); opacity: 0.5; flex-shrink: 0; }

.about-actions {
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.about-action-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 8px;
  border-radius: 8px;
  background: none;
  border: 0;
  text-decoration: none;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
  cursor: pointer;
  text-align: left;
  width: 100%;
  transition: background 0.12s;
}
.about-action-row:hover { background: var(--surface-3); }
.about-action-row svg:first-child { color: var(--text-dim); flex-shrink: 0; }
.about-action-row span { flex: 1; }
.about-action-chevron { color: var(--text-dim); opacity: 0.5; flex-shrink: 0; }

/* Donate modal (#548) — Settings › About › Support Todeo + monthly soft-prompt. */
.donate-modal {
  max-width: 420px;
  padding: 0;
  overflow: hidden;
  border-radius: 18px;
}
.donate-hero {
  text-align: center;
  padding: 32px 28px 18px;
  background: linear-gradient(180deg, rgba(255, 221, 0, 0.10) 0%, transparent 100%);
  border-bottom: 1px solid var(--border);
}
.donate-hero-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: linear-gradient(135deg, #ffdd00 0%, #ffb800 100%);
  color: #1a1a1a;
  margin-bottom: 14px;
  box-shadow: 0 8px 24px rgba(255, 188, 0, 0.28), 0 2px 6px rgba(0, 0, 0, 0.18);
}
.donate-title {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.3px;
  color: var(--text);
}
.donate-thanks {
  margin: 6px 0 0;
  font-size: 13px;
  color: var(--text-dim);
  font-style: italic;
}
.donate-body-wrap {
  padding: 18px 28px 4px;
  text-align: center;
}
.donate-body {
  margin: 0 0 14px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text);
}
.donate-cta {
  margin: 0;
  padding: 12px 14px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text);
  background: var(--surface-2);
  border-radius: 10px;
  font-weight: 500;
}
.donate-actions {
  padding: 18px 28px 24px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.donate-bmc-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 13px 18px;
  background: #ffdd00;
  color: #1a1a1a;
  border: 0;
  border-radius: 10px;
  font-size: 14.5px;
  font-weight: 600;
  font-family: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.donate-bmc-btn:hover {
  background: #ffd500;
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(255, 188, 0, 0.36);
}
.donate-bmc-btn:active { transform: translateY(0); }
.donate-bmc-btn svg { color: #1a1a1a; }
.donate-cancel-btn {
  padding: 10px 18px;
  background: none;
  border: 0;
  color: var(--text-dim);
  font-size: 13.5px;
  font-family: inherit;
  cursor: pointer;
  border-radius: 8px;
  transition: background 0.12s, color 0.12s;
}
.donate-cancel-btn:hover { background: var(--surface-2); color: var(--text); }

/* Header heart button (#548c, unified in #550) — shape lives on the shared
   .header-icon-btn base. No per-rule overrides needed. */

/* Bug report modal (#543) */
.bug-report-modal h2 { color: var(--text) !important; }
.bug-report-label {
  display: block;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-dim);
  margin: 14px 0 5px;
}
.bug-report-label:first-child { margin-top: 0; }
.bug-report-label-body { margin-top: 12px; }
.bug-report-textarea {
  width: 100%;
  padding: 9px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 13.5px;
  font-family: inherit;
  resize: vertical;
  min-height: 88px;
  box-sizing: border-box;
}
.bug-report-textarea:focus { outline: none; border-color: var(--accent); }

/* Delete-account confirm modal */
.delete-account-modal { max-width: 460px; padding: 24px 28px; }
.delete-account-modal h2 {
  margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #f87171;
}
.delete-account-modal p {
  margin: 6px 0 14px; font-size: 13.5px; line-height: 1.5; color: var(--text);
}
.delete-account-modal p.delete-account-export-hint {
  color: var(--text-dim); font-size: 12.5px;
}
.delete-account-modal .email-input {
  width: 100%;
  padding: 9px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
}
.delete-account-modal .email-input:focus {
  outline: none; border-color: var(--accent);
}
.delete-account-modal .modal-actions {
  display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;
}
.delete-account-modal .modal-actions button {
  padding: 9px 16px;
  border-radius: 8px;
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
}
.delete-account-modal .modal-actions button:hover { background: var(--surface-3); }
.delete-account-modal .modal-actions button.danger-btn {
  background: var(--danger-dim);
  border-color: color-mix(in srgb, var(--danger) 40%, transparent);
  color: var(--danger);
}
.delete-account-modal .modal-actions button.danger-btn:hover {
  background: color-mix(in srgb, var(--danger) 22%, transparent);
  border-color: var(--danger);
}
.delete-account-modal .modal-actions button:disabled {
  opacity: 0.5; cursor: not-allowed;
}
/* Reuse delete-account-modal layout for the list create + delete modals
   (#174). They share the same shape (title + body + textfield/inline + actions),
   so applying the class keeps the visual language consistent without forking. */
.delete-account-modal input[type="text"] {
  width: 100%;
  padding: 9px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
  box-sizing: border-box;
}
.delete-account-modal input[type="text"]:focus {
  outline: none; border-color: var(--accent);
}
.delete-account-modal .modal-actions button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}
.delete-account-modal .modal-actions button.primary:hover {
  background: var(--accent-hover);
}
/* #315 removed the top-left .swipe-card-list-btn ("+ list" bookmark
   shortcut from #175); the slot is now used by the .provider-cluster.
   top-left from #311. */

/* Watchlist mode sub-segment (#260, hoisted into .tab-toolbar in #271;
   slimmed to 2-way All items / Upcoming in #947 — availability moved into
   the filter sheet). The toggle keeps .watchlist-mode-toggle so JS can
   target it without changes to the click-handler lookup. */
.watchlist-mode-toggle { display: inline-flex; flex-wrap: nowrap; max-width: 100%; }

/* Mobile (<=720px): tighten button padding on the 2-segment mode toggle. */
@media (max-width: 720px) {
  .watchlist-mode-toggle button {
    padding: 6px 9px;
    font-size: 12.5px;
  }
  /* #947: 2-way toggle fits comfortably on one row — no overflow-x needed.
     The segmented grows to fill (flex: 1 1 auto set in the <=430px block
     above); Filter/Sort/Pick are flex-shrink:0 so they never collapse. */
  #watchlistPanel .tab-toolbar .watchlist-mode-toggle {
    flex: 1 1 auto;
    min-width: 0;
    overflow-x: visible;
  }
  #watchlistPanel .tab-toolbar .watchlist-mode-toggle button {
    flex: 0 0 auto;
  }
}

/* Custom lists (#174). The Lists tab renders a stack of cards, one per
   list, plus a "+ New list" toolbar button above. Cards reuse the
   surface palette of `.movie-card` / settings sections (border + soft bg)
   without re-implementing the poster/grid layout — these are list-of-lists
   rows, not item cards. The standalone .lists-toolbar wrapper retired in
   #271 — the +New list button now lives directly in the unified
   .tab-toolbar above the panel. */
.lists-create-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 8px;
  color: white;
  font-size: 13.5px; font-weight: 500;
  cursor: pointer;
  transition: background 0.15s;
}
.lists-create-btn:hover { background: var(--accent-hover); }
.lists-create-btn svg { stroke: white; }
.lists-grid { display: flex; flex-direction: column; gap: 10px; }
.list-card {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 8px 14px;
  align-items: center;
  padding: 14px 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  transition: background 0.15s, border-color 0.15s;
}
.list-card:hover { background: var(--surface-3); border-color: var(--accent); }
.list-card-title {
  font-size: 15px; font-weight: 600; color: var(--text);
  cursor: text;
  padding: 4px 6px;
  margin: -4px -6px;
  border-radius: 6px;
  border: 1px solid transparent;
  min-width: 0;
  word-break: break-word;
}
.list-card-title:hover { background: var(--surface-3); border-color: var(--border); }
.list-card-title-input {
  font: inherit; color: inherit;
  background: var(--surface-3);
  border: 1px solid var(--accent);
  border-radius: 6px;
  padding: 4px 6px;
  margin: -4px -6px;
  width: 100%;
  box-sizing: border-box;
  outline: none;
}
.list-card-meta {
  grid-column: 1 / 2;
  font-size: 12.5px; color: var(--text-dim);
  display: flex; gap: 10px; flex-wrap: wrap;
}
.list-card-meta span + span::before {
  content: '·'; margin-right: 10px; color: var(--text-dim);
}
.list-card-actions {
  grid-row: 1 / span 2;
  grid-column: 2 / 3;
  display: inline-flex; gap: 4px; align-self: start;
}
.list-card-action-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--text-dim);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.list-card-action-btn:hover {
  background: var(--surface-3);
  color: var(--text);
  border-color: var(--border);
}
.list-card-action-btn.danger:hover {
  color: #ef4444;
  border-color: rgba(239, 68, 68, 0.4);
  background: rgba(239, 68, 68, 0.08);
}
@media (max-width: 720px) {
  .lists-create-btn { padding: 9px 14px; font-size: 14px; }
  .list-card { padding: 12px 14px; gap: 6px 10px; }
  .list-card-title { font-size: 14.5px; }
  .list-card-meta { font-size: 12px; gap: 8px; }
  .list-card-action-btn { width: 36px; height: 36px; }
}

/* Embedded thumbnail row on list cards (#395). Lives in a third grid row
   spanning both columns so the existing actions column (rows 1–2) is
   undisturbed. Horizontal scroll on overflow keeps the row workable below
   ~360pt where 5 × 48px tiles + the +N tile + their gaps would otherwise
   wrap. The whole card is clickable (per #395 AC) — tapping a tile opens
   the item modal, anywhere else on the card opens the list-detail view. */
.list-card { cursor: pointer; }
.list-card-thumbnails {
  grid-column: 1 / -1;
  display: flex; gap: 6px;
  overflow-x: auto;
  padding: 4px 0 2px;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.list-card-thumbnails::-webkit-scrollbar { display: none; }
.list-card-thumb {
  flex: 0 0 auto;
  width: 48px; height: 72px;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
  cursor: pointer;
  padding: 0;
  position: relative;
  transition: border-color 0.15s, transform 0.15s;
}
.list-card-thumb:hover, .list-card-thumb:focus-visible {
  border-color: var(--accent);
  transform: translateY(-1px);
  outline: none;
}
.list-card-thumb img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.list-card-thumb-placeholder {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%;
  font-size: 22px;
}
.list-card-thumb.overflow {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  background: var(--surface-2);
  color: var(--text-dim);
  gap: 1px;
}
.list-card-thumb.overflow:hover, .list-card-thumb.overflow:focus-visible {
  color: var(--text);
}
.list-card-thumb-more { font-size: 14px; font-weight: 700; color: var(--text); line-height: 1; }
.list-card-thumb-more-label { font-size: 9.5px; letter-spacing: 0.02em; text-transform: uppercase; }
.list-card-thumbnails.empty {
  grid-column: 1 / -1;
  display: flex; align-items: center; justify-content: flex-start;
  height: 56px;
  background: transparent;
  border: 1px dashed var(--border);
  border-radius: 8px;
  padding: 0 12px;
  color: var(--text-dim);
  font-size: 12.5px; font-weight: 500;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.list-card-thumbnails.empty:hover, .list-card-thumbnails.empty:focus-visible {
  border-color: var(--accent);
  color: var(--text);
  background: var(--surface-3);
  outline: none;
}
.list-card-thumb-empty-text { display: inline-flex; align-items: center; gap: 6px; }
@media (max-width: 720px) {
  .list-card-thumb { width: 44px; height: 66px; }
  .list-card-thumbnails.empty { height: 50px; font-size: 12px; }
}

/* List detail view (#176). Sits inside #watchlistListsView alongside the
   list-of-lists container; renderListDetail toggles which is visible. The
   header is a 2-row stack: Back button on top, then a baseline-aligned row
   with the (rename-able) list title and the item count. The grid below
   reuses the global .movie-grid layout so cards match Watchlist density. */
.list-detail-header {
  display: flex; flex-direction: column; gap: 8px;
  margin: 4px 0 14px;
}
.list-detail-back {
  display: inline-flex; align-items: center; gap: 4px;
  align-self: flex-start;
  background: transparent;
  border: 1px solid transparent;
  padding: 6px 10px 6px 6px;
  margin-left: -6px;
  border-radius: 8px;
  color: var(--text-dim);
  font-size: 13.5px; font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.list-detail-back:hover, .list-detail-back:focus-visible {
  background: var(--surface-3);
  color: var(--text);
  border-color: var(--border);
}
.list-detail-title-row {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
}
.list-detail-title {
  background: transparent;
  border: 1px solid transparent;
  padding: 4px 8px;
  margin: -4px -8px;
  border-radius: 8px;
  color: var(--text);
  font-size: 22px; font-weight: 700;
  cursor: text;
  text-align: left;
  min-width: 0;
  word-break: break-word;
  transition: background 0.15s, border-color 0.15s;
}
.list-detail-title:hover, .list-detail-title:focus-visible {
  background: var(--surface-2);
  border-color: var(--border);
}
.list-detail-title-input {
  font: 700 22px/1.2 inherit;
  color: var(--text);
  background: var(--surface-3);
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 4px 8px;
  margin: -4px -8px;
  outline: none;
  min-width: 200px;
  max-width: 100%;
}
.list-detail-count {
  font-size: 13px; color: var(--text-dim);
}
/* #395 — last-updated timestamp beside the count, in the same dim weight.
   Separator dot mirrors the meta row on the My Lists card. */
.list-detail-updated {
  font-size: 13px; color: var(--text-dim);
}
.list-detail-count + .list-detail-updated::before {
  content: '·'; margin: 0 8px 0 0; color: var(--text-dim);
}
/* #19.1 — Share button + per-list public/private chip in the list-detail
   header. The share button is an icon-only affordance to keep the header
   compact; the visibility chip only renders when the list is actually
   public so it doubles as the "you've shared this" indicator. */
.list-detail-share {
  margin-left: auto; display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; padding: 0;
  background: transparent; border: 1px solid transparent; color: var(--text-dim);
  border-radius: 10px; cursor: pointer;
}
.list-detail-share:hover, .list-detail-share:focus-visible {
  background: var(--bg-elev); color: var(--text); border-color: var(--border);
  outline: none;
}
.list-detail-visibility {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; height: 28px;
  background: transparent; border: 1px solid var(--border);
  color: var(--text-dim); font-size: 12px; font-weight: 500;
  border-radius: 999px; cursor: pointer;
}
.list-detail-visibility-public { color: var(--text); border-color: rgba(80, 200, 120, 0.45); }
.list-detail-visibility-dot {
  width: 8px; height: 8px; border-radius: 999px; background: rgb(80, 200, 120);
  box-shadow: 0 0 0 3px rgba(80, 200, 120, 0.15);
}
.list-detail-visibility-action { color: var(--text-dim); }
.list-detail-visibility:hover .list-detail-visibility-action,
.list-detail-visibility:focus-visible .list-detail-visibility-action {
  color: var(--text);
}
.list-detail-visibility:focus-visible { outline: none; border-color: var(--text); }
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0);
  white-space: nowrap; border: 0;
}
@media (max-width: 720px) {
  .list-detail-title { font-size: 19px; }
  .list-detail-title-input { font-size: 19px; }
  .list-detail-header { margin: 0 0 10px; }
  .list-detail-share { width: 32px; height: 32px; }
}

/* #19.1 — Public read-only list view inside the profile modal. Used when a
   viewer lands on /u/<username>/list/<listId>. The grid reuses
   .profile-list-grid + .profile-list-card from the bare-profile view. */
.profile-list-view {
  display: flex; flex-direction: column; gap: 12px;
}
.profile-list-back-link {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--text-dim); text-decoration: none; font-size: 13px;
  align-self: flex-start;
}
.profile-list-back-link:hover, .profile-list-back-link:focus-visible {
  color: var(--text); text-decoration: underline; outline: none;
}
.profile-list-view-title {
  margin: 0; font-size: 22px; font-weight: 600;
}
.profile-list-view-meta {
  font-size: 13px; color: var(--text-dim);
}
.profile-list-signup-cta {
  display: flex; flex-direction: column; gap: 6px;
  padding: 14px 16px; margin-top: 12px;
  background: var(--bg-elev); border: 1px solid var(--border); border-radius: 12px;
  text-align: center;
}
.profile-list-signup-cta strong { font-size: 15px; color: var(--text); }
.profile-list-signup-cta span { font-size: 13px; color: var(--text-dim); }
.profile-list-signup-link {
  display: inline-block; margin-top: 6px; padding: 8px 16px;
  background: var(--accent, #e5b66b); color: #1a1a1a;
  border-radius: 999px; text-decoration: none; font-weight: 600; font-size: 14px;
  align-self: center;
}
.profile-list-signup-link:hover, .profile-list-signup-link:focus-visible {
  filter: brightness(1.08); outline: none;
}

/* #19.3 — sticky sign-up bar on /u/<username> and /u/<username>/list/<id>
   for signed-out viewers. Sits inside the profile bottom-sheet modal,
   pinned to the bottom of the scroll area so it stays visible while the
   user scrolls through the owner's lists. Replaces the previous in-line
   chip-style CTA which only appeared at the very bottom of the list view
   and was easy to miss. Inherits the modal's safe-area-inset padding so
   it clears the iOS home indicator. */
.profile-signup-sticky {
  position: sticky;
  bottom: 0;
  left: 0;
  right: 0;
  margin: 12px -16px -16px;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--bg-elev);
  border-top: 1px solid var(--border);
  z-index: 2;
  box-shadow: 0 -6px 16px rgba(0, 0, 0, 0.18);
}
.profile-signup-sticky-copy {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.profile-signup-sticky-copy strong {
  font-size: 15px;
  color: var(--text);
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.profile-signup-sticky-copy span {
  font-size: 12.5px;
  color: var(--text-dim);
  line-height: 1.3;
}
.profile-signup-sticky-cta {
  flex: 0 0 auto;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 10px 18px;
  background: var(--accent, #e5b66b);
  color: #1a1a1a;
  border-radius: 999px;
  text-decoration: none;
  font-weight: 600;
  font-size: 14px;
}
.profile-signup-sticky-cta:hover, .profile-signup-sticky-cta:focus-visible {
  filter: brightness(1.08);
  outline: none;
}

/* Remove-from-list overlay (#176). Mirrors the watchlist/watched
   .poster-quick-action treatment but uses an x-mark icon and a danger
   tint so the action reads as destructive at a glance. The .remove
   modifier overrides the default green hover with red. */
.poster-quick-action.remove {
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
}
.poster-quick-action.remove:hover, .poster-quick-action.remove:focus-visible {
  background: rgba(239, 68, 68, 0.9);
  color: #fff;
}

/* Inline CTA button on the empty list-detail state. Reuses the .empty-state
   container for icon + title + text but adds a small CTA button below — the
   ticket calls for a "Browse" CTA and the existing emptyState() helper
   doesn't render buttons. */
.empty-state-cta {
  display: inline-block;
  margin-top: 12px;
  padding: 8px 16px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 14px; font-weight: 600;
  cursor: pointer;
  transition: filter 0.15s;
}
.empty-state-cta:hover, .empty-state-cta:focus-visible { filter: brightness(1.1); }

/* Pick-for-me — random watchlist picker (#64).
   Inline button sits in the watchlist filter bar on desktop; the FAB
   replaces it on mobile (bottom-right, above the fixed bottom-tab bar).
   The modal is intentionally narrow + image-forward — the whole point is
   to dramatize a single chosen item, not to look like another grid. */
.pick-inline {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  cursor: pointer;
  font-size: 13.5px; font-weight: 500;
  white-space: nowrap;
}
.pick-inline:hover { background: var(--surface-3); border-color: var(--accent); color: var(--text); }
.pick-inline svg { color: var(--accent); }

.pick-fab {
  position: fixed;
  right: 16px;
  bottom: calc(76px + env(safe-area-inset-bottom));
  width: 56px; height: 56px;
  border-radius: 50%;
  background: var(--accent);
  color: white;
  border: 1px solid var(--accent);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  z-index: 90;
  cursor: pointer;
  transition: transform 0.12s ease, filter 0.12s ease;
}
.pick-fab:hover { filter: brightness(1.08); }
.pick-fab:active { transform: scale(0.94); }
.pick-fab[hidden] { display: none; }
/* FAB only on mobile; desktop uses the inline button instead. */
@media (min-width: 601px) {
  .pick-fab { display: none !important; }
}
/* Inline pick button hides on mobile in favour of the FAB. */
@media (max-width: 600px) {
  .pick-inline { display: none; }
}

.pick-modal {
  max-width: 460px;
  padding: 0;
  overflow: hidden;
}
.pick-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 36px 28px 24px;
}
.pick-poster {
  width: 220px;
  aspect-ratio: 2 / 3;
  border-radius: 14px;
  overflow: hidden;
  background: var(--surface-2);
  position: relative;
  box-shadow: var(--shadow-3);
  transition: transform 0.18s ease, filter 0.18s ease;
}
.pick-poster img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Book covers render as an <img> layered over the absolute .book-placeholder
   gradient; the cover must itself be absolute so it paints above the
   placeholder (same as the grid card's .movie-poster .poster-img). Without
   this the in-flow img sits behind the absolute placeholder and never shows
   (#861). Movie/game picks use a plain <img> with no placeholder sibling. */
.pick-poster .poster-img { position: absolute; inset: 0; }
.pick-poster .pick-poster-placeholder {
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-dim);
  font-size: 36px;
}
.pick-stage.rolling .pick-poster {
  animation: pickShake 0.16s ease-in-out infinite alternate;
}
@keyframes pickShake {
  from { transform: translateY(-3px) rotate(-1.6deg) scale(0.97); filter: blur(0.5px) saturate(1.1); }
  to   { transform: translateY(3px)  rotate(1.6deg)  scale(0.97); filter: blur(0.5px) saturate(1.1); }
}
.pick-stage.landed .pick-poster {
  animation: pickLand 0.5s cubic-bezier(0.22, 1.2, 0.36, 1) 1;
}
@keyframes pickLand {
  0%   { transform: scale(0.9); }
  60%  { transform: scale(1.05); }
  100% { transform: scale(1); }
}
.pick-title {
  margin: 20px 0 4px;
  font-size: 22px; font-weight: 700;
  color: var(--text);
  line-height: 1.25;
}
.pick-subtitle {
  font-size: 13px; color: var(--text-muted);
  margin-bottom: 22px;
}
.pick-actions {
  display: flex; flex-direction: column; gap: 9px;
  width: 100%;
  max-width: 340px;
}
.pick-actions button {
  width: 100%;
  padding: 13px 16px;
  border-radius: 11px;
  font-size: 15px; font-weight: 600;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.pick-actions button:hover { background: var(--surface-3); }
.pick-actions button:active { transform: translateY(1px); }
/* Primary: accept the pick */
.pick-actions .pick-watch-it {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}
.pick-actions .pick-watch-it:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}
/* Tertiary: quiet ghost for the edge "already seen it" action */
.pick-actions .pick-already-watched {
  background: transparent;
  border-color: transparent;
  color: var(--text-dim);
  font-weight: 500;
}
.pick-actions .pick-already-watched:hover {
  background: var(--surface-2);
  color: var(--text);
}
.pick-actions button svg { width: 16px; height: 16px; flex-shrink: 0; }

.pick-empty {
  padding: 44px 28px 32px;
  text-align: center;
}
.pick-empty .empty-state-icon { margin-bottom: 10px; color: var(--text-dim); }
.pick-empty .empty-state-icon svg { width: 48px; height: 48px; }
.pick-empty-title { font-size: 17px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
.pick-empty-text { color: var(--text-muted); font-size: 14px; }

@media (prefers-reduced-motion: reduce) {
  .pick-stage.rolling .pick-poster,
  .pick-stage.landed .pick-poster {
    animation: none !important;
    transform: none !important;
    filter: none !important;
  }
}

@media (max-width: 480px) {
  .pick-modal { max-width: 100%; }
  .pick-stage { padding: 28px 20px 20px; }
  .pick-poster { width: 180px; }
  .pick-title { font-size: 19px; }
}

/* Action button sitting below a toggle-row inside a settings section. */
.settings-section-action { margin-top: 12px; }

/* Settings toggle */
.toggle-row {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 0;
}
.toggle-row label { flex: 1; font-size: 13px; color: var(--text); margin: 0; }
.toggle-row label span { display: block; font-size: 12px; color: var(--text-dim); margin-top: 2px; }
.toggle {
  position: relative;
  width: 40px; height: 22px;
  background: var(--surface-3);
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.15s;
  flex-shrink: 0;
}
.toggle::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: var(--text);
  transition: transform 0.15s;
}
.toggle.on { background: var(--accent); }
.toggle.disabled { opacity: 0.4; pointer-events: none; }
.toggle.on::after { transform: translateX(18px); }

/* #766: slider rows in settings (push-notification "Minimum discount").
   Without these rules the .slider-row scaffold had no styling, so the two
   <span>s inside <label> rendered inline ("Minimum discountOnly alert me…")
   and the range input inherited unthemed OS defaults. Mirrors .toggle-row
   typography so this row visually aligns with the toggle rows above it. */
.slider-row {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 8px 0;
}
.slider-row[hidden] { display: none; }
.slider-row > label {
  font-size: 13px;
  color: var(--text);
  margin: 0;
  cursor: pointer;
}
.slider-row > label > span {
  display: block;
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 2px;
}
.slider-row > label > span:first-child { margin-top: 0; color: var(--text); }
.slider-control {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
}
.slider-control input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  height: 44px; /* 44pt tap target; the visual track is the ::-*-runnable-track inside */
  background: transparent;
  cursor: pointer;
  margin: 0;
  padding: 0;
}
.slider-control input[type="range"]:focus { outline: none; }
.slider-control input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 35%, transparent);
}
.slider-control input[type="range"]:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 35%, transparent);
}
.slider-control input[type="range"]::-webkit-slider-runnable-track {
  height: 4px;
  background: var(--surface-3);
  border-radius: 999px;
}
.slider-control input[type="range"]::-moz-range-track {
  height: 4px;
  background: var(--surface-3);
  border-radius: 999px;
}
.slider-control input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface);
  margin-top: -8px; /* center 20px thumb on 4px track */
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.slider-control input[type="range"]::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.slider-control input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.1); }
.slider-control input[type="range"]:active::-moz-range-thumb { transform: scale(1.1); }
.slider-control output {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  min-width: 44px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Tab icon spring: scale 1 -> 1.18 -> 1, triggered once per activation.
   Used inside the mobile @media block on .tab.active svg.tab-icon. */
@keyframes tab-icon-spring {
  0%   { transform: scale(1);    }
  50%  { transform: scale(1.18); }
  100% { transform: scale(1);    }
}

@media (max-width: 600px) {
  /* Belt-and-braces zoom prevention on mobile: the viewport meta blocks
     pinch-zoom; this 16px minimum on form controls blocks iOS Safari's
     auto-zoom on input focus (which fires whenever the focused field is
     under 16px). */
  input, select, textarea { font-size: 16px; }

  /* Safari/iOS bug: position:sticky creates a containing block for
     position:fixed descendants, so the fixed bottom tabs track #topbar
     instead of the viewport during scroll. Removing sticky on mobile is
     correct anyway — the header scrolls away, only the bottom bar stays. */
  #topbar { position: static; }

  /* The `padding:` shorthand re-asserts all four sides, so we have to
     re-apply the safe-area-inset-top trick from the base `header` rule
     above (#283) — otherwise iOS Home Screen PWA loses the inset and the
     status bar overlaps the wordmark again (#293). */
  header {
    padding: 10px 10px;
    padding-top: calc(10px + env(safe-area-inset-top));
    gap: 6px;
  }
  /* Bottom padding reserves room for the fixed-position translucent tab bar
     (~56px content + safe-area inset). Without this the last row of cards
     would scroll behind the bar and never become readable. */
  main {
    padding: 14px;
    padding-bottom: calc(72px + env(safe-area-inset-bottom));
  }

  /* Mobile region picker — same flag-chip as desktop but slightly shorter. */
  .region-picker { height: 30px; }
  /* Category toggle stays compact in the header on mobile, doesn't stretch.
     min-height (not height) so the touch-target override below can grow the
     wrapper instead of having buttons overflow it. The standalone selectors
     guard the (now-unused) bare-segmented case; the .brand-selector variant
     overrides the inner-pill sizing inside the unified container. */
  .segmented.category-toggle { width: auto; min-height: 30px; }
  .segmented.category-toggle button { flex: initial; padding: 0 8px; font-size: 11px; }
  .brand-selector { height: 34px; padding: 2px 3px 2px 12px; }
  .brand-selector .logo { padding-right: 8px; }
  .brand-selector .segmented.category-toggle button { height: 26px; padding: 0 8px; font-size: 11px; }
  /* Profile button shrinks slightly so the row fits without overflow. */
  .header-avatar { width: 28px; height: 28px; }
  .header-avatar-icon { width: 18px; height: 18px; }
}

@media (max-width: 480px) {
  /* iPhone-class viewports (≤390pt) with longer Romance-language category
     labels ("Guardare / Leggere / Giocare" in Italian, "Regarder / Lire /
     Jouer" in French) push the header past 100vw at the ≤600px values. Pack
     the row tighter so all three tabs stay visible — see issue #118.
     padding-top longhand preserves the safe-area inset (#283/#293) which
     the `padding:` shorthand would otherwise reset to 8px. */
  header {
    padding: 8px 10px;
    padding-top: calc(8px + env(safe-area-inset-top));
    gap: 6px;
  }
  .wordmark { font-size: 19px; }
  .region-picker { height: 28px; width: 34px; min-width: 34px; gap: 0; }
  .region-flag-display { font-size: 15px; }
  .segmented.category-toggle { min-height: 28px; padding: 1px; }
  .segmented.category-toggle button { padding: 0 6px; font-size: 10.5px; }
  /* Brand selector at iPhone-class widths — swap full localized pill labels
     ("Watch" / "Guardare" / "Regarder") for SVG icons so the wordmark stays
     visible alongside the right-side cluster (Region · Avatar). */
  .brand-selector { height: 30px; padding: 2px 2px 2px 10px; gap: 4px; border-radius: 10px; }
  .brand-selector .logo { padding-right: 7px; }
  .brand-selector .segmented.category-toggle { gap: 1px; }
  .brand-selector .segmented.category-toggle button {
    height: 24px;
    padding: 0;
    min-width: 28px;
    justify-content: center;
    position: relative;
  }
  /* #940: extend category pill tap height to 44pt. Width stays at 100% of
     the button so adjacent pills never receive each other's events. */
  .brand-selector .segmented.category-toggle button::after {
    content: '';
    position: absolute;
    top: 50%; left: 0; right: 0;
    transform: translateY(-50%);
    height: 44px;
  }
  .cat-pill-full { display: none; }
  .cat-pill-short { display: inline-flex; align-items: center; }
  /* iPhone-class viewports: shrink the single profile button so the row
     fits in 100vw alongside the long Romance-language category labels. */
  .header-avatar { width: 26px; height: 26px; border-width: 1.5px; overflow: visible; }
  .header-avatar-icon { width: 16px; height: 16px; }
  /* #940: extend avatar tap target to 44pt at 480px and below (26px visual).
     overflow:hidden is overridden to visible so the ::after pseudo-element is
     not clipped (the img is already constrained to 100%/100% of the button, so
     no actual image overflow occurs). Absolute, no z-index raise, transparent.
     No layout shift: ::after is absolute, layout box stays 26px. */
  .header-avatar::after {
    content: '';
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    min-width: 44px; min-height: 44px;
  }

  /* Toast (#145): raise above the fixed bottom nav so it doesn't cover tab buttons. */
  .toast {
    bottom: calc(64px + env(safe-area-inset-bottom) + 8px);
  }

  /* Update banner: full-width with edge insets, sat above the bottom
     tabs bar (which is position:fixed at bottom on mobile). */
  .update-banner {
    left: 12px;
    right: 12px;
    bottom: calc(64px + env(safe-area-inset-bottom));
    transform: none;
    max-width: none;
    font-size: 13px;
    gap: 8px;
    padding: 10px 12px;
  }
  .update-banner-btn { padding: 8px 14px; font-size: 13px; }
  .update-banner-close { font-size: 22px; padding: 0 6px; }

  /* Tabs: native mobile pattern — fixed bar at the bottom of the viewport.
     Even though the markup keeps them as a child of #topbar, position:fixed
     pulls them to the viewport edge. Active state uses color (no underline)
     since a border-bottom at the screen edge looks awkward.
     Translucent + backdrop-blur (#587) so content scrolls UNDER the bar with
     an iOS-native frosted-glass effect. Previously the bar was fully opaque
     and a JS scroll listener hid it during scrolls (#223 + #164), which was
     jittery; the listener is removed (see main.js). The blur "leak" #164
     called out was a real iOS bug under specific conditions — if it recurs,
     the fix is `transform: translateZ(0)` here to force a new compositing
     layer, not removing the blur. */
  .tabs {
    position: fixed;
    bottom: 0; left: 0; right: 0;
    background: color-mix(in srgb, var(--bg) 75%, transparent);
    backdrop-filter: blur(24px) saturate(180%);
    -webkit-backdrop-filter: blur(24px) saturate(180%);
    /* Force its own compositing layer — sidesteps the iOS-Safari blur-leak
       (#164) where backdrop-filter on a position:fixed element occasionally
       renders without the blur, exposing the content behind. */
    transform: translateZ(0);
    border-top: 1px solid var(--border);
    border-bottom: none;
    padding: 4px 4px;
    padding-bottom: max(6px, env(safe-area-inset-bottom));
    z-index: 100;
    overflow: hidden;
    gap: 0;
    margin: 0;
  }
  .tab {
    flex: 1 1 0;
    min-width: 0;
    flex-direction: column;
    padding: 8px 2px 6px;
    gap: 3px;
    font-size: 11px;
    line-height: 1.2;
    border-bottom: none;
    margin-bottom: 0;
    border-radius: 10px;
    transition: color 0.15s, background 0.15s;
  }
  .tab.active {
    border-bottom: none;
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    font-weight: 600;
  }
  /* Top indicator pill on active tab */
  .tab.active::before {
    content: '';
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 28px;
    height: 3px;
    background: var(--accent);
    border-radius: 0 0 3px 3px;
  }
  /* Slightly bolder icon stroke on active; one-shot spring on tab selection. */
  .tab.active svg.tab-icon { stroke-width: 2.2; animation: tab-icon-spring 0.25s ease-out both; }
  svg.tab-icon { width: 24px; height: 24px; }
  .tab-label {
    font-size: 10.5px;
    gap: 4px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
  }
  .tab { position: relative; }
  /* Reserve room at the bottom of scrollable content so the tab bar
     doesn't cover the last cards / actions. */
  main {
    padding-bottom: calc(80px + env(safe-area-inset-bottom));
  }
  /* Alerts banner sits below header — give it a little less side margin */
  .alerts-banner { margin: 12px 14px 0; }

  .modal-body {
    grid-template-columns: 110px 1fr;
    gap: 10px 14px;
    padding: 16px;
    margin-top: -70px;
    align-items: start;
  }
  .modal-poster {
    width: 110px;
    margin: 0;
    grid-column: 1;
    grid-row: 1;
  }
  /* On mobile, the title + edition selector + meta line stack tight in a
     flex column beside the poster (#169). Wrapping them in a single grid
     child stops the poster's row span from inflating the column-2 rows
     and pushing meta data 100+px below the editions chip strip. */
  .modal-info-header {
    grid-column: 2;
    align-self: start;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
  }
  .modal-info h2 {
    font-size: 18px;
    line-height: 1.25;
    margin: 0;
    text-align: left;
  }
  .modal-meta {
    font-size: 12px;
    margin: 0;
    gap: 4px 6px;
    line-height: 1.4;
  }
  .modal-overview {
    font-size: 13px;
    line-height: 1.55;
    margin-top: 8px;
    margin-bottom: 8px;
    grid-column: 1 / -1;
  }
  .modal-overview.clamp {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .read-more-toggle {
    background: none;
    border: none;
    color: var(--accent);
    font-size: 13px;
    font-weight: 600;
    padding: 2px 0 8px;
    cursor: pointer;
    grid-column: 1 / -1;
  }
  .modal-actions {
    grid-column: 1 / -1;
    flex-wrap: nowrap;
    gap: 6px;
    margin-top: 12px;
  }
  /* Touch-friendly mobile action row: buttons share equal width and labels
     wrap rather than clip — fixes overflow for long localized strings like
     "Als gesehen markieren" / "Segna come visto" / "Aggiungi a una lista"
     across every locale (#89). */
  .modal-actions button {
    flex: 1 1 0;
    min-width: 0;
    padding: 11px 6px;
    font-size: 12px;
    font-weight: 600;
    justify-content: center;
    text-align: center;
    white-space: normal;
    line-height: 1.25;
  }
  /* The info column needs to allow .modal-overview/.read-more-toggle/.modal-actions
     to span the full row — flatten the info container into the body grid */
  .modal-info {
    display: contents;
  }
  /* Edition chip strip sits inside .modal-info-header on mobile — flex gap
     handles vertical rhythm, no margins needed. */
  .edition-strip {
    margin: 0;
    gap: 4px;
  }
  .edition-chip {
    padding: 4px 10px;
    font-size: 11px;
  }
  .modal-backdrop { height: 200px; }
  .movie-grid {
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 14px;
  }
  .logo-text { display: none; }
  /* Tighten the inner scroll padding on phones; modal itself stays padding:0
     so the scroll wrapper can fill the modal and scrolling works properly */
  .settings-modal-scroll { padding: 22px 18px 18px; }

  /* Drilled-in state on mobile (#400, #446): once the user is inside a
     subgroup, the per-group `.settings-group-header` (Back chevron +
     group title) is the orienting affordance. The modal-level "Settings"
     h2 + "Changes are saved automatically" subtitle pill are both
     redundant on top of that — together they were eating ~100px of
     vertical space before the first piece of actual content, which on
     iPhone-class viewports pushed the meaningful controls below the
     fold (#442 visual report, then #446 follow-up after #443 was still
     too loose).
     Selector uses the JS-mirrored `data-drilled-in` attribute on
     `.settings-modal` rather than `:has(.settings-groups:not([...]))`
     because the `:has()` form silently no-ops on some iOS PWA standalone
     contexts — see settings-nav.js for the mirror, #446 for the trace.
     Top-level mobile list (no drilled state, attribute absent) still
     shows h2 + subtitle pill where they're most useful. */
  .settings-modal[data-drilled-in] .subtitle {
    display: none;
  }
  /* Hide the modal-level h2 visually but keep it in the accessibility
     tree so `aria-labelledby="settingsTitle"` still resolves to a name. */
  .settings-modal[data-drilled-in] .settings-title {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  /* Tighten the per-group header now that it's the first visible chrome. */
  .settings-modal[data-drilled-in] .settings-group-header {
    margin-bottom: 10px;
    padding-bottom: 8px;
  }
  /* iOS PWA standalone showed ~22–30px of empty space above the group
     header (the scroll wrapper's top padding compounding with the
     hidden h2 + the modal's own chrome). Drilled-in state hides h2
     and subtitle already, so we can pull the whole scroll wrapper up
     and remove the wasted air. The X close button keeps its own
     position absolute so it's untouched. */
  .settings-modal[data-drilled-in] .settings-modal-scroll {
    padding-top: 8px;
  }

  /* Filter bar layout on mobile: 2-col grid where segmented toggles and
     the sort select are first-class peers. The "Sort" label is hidden
     since the dropdown values are self-explanatory.
     - 1 toggle + sort  → toggle | sort           (one row)
     - 2 toggles + sort → toggle | toggle / sort  (two rows; sort spans)
     - 2 toggles only   → toggle | toggle         (one row)
     All controls share the same 36px height for a unified look. The
     search-bar and tab-bar are also slimmed so the filter strip eats
     less of the viewport. */
  .filter-bar {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    margin-bottom: 14px;
    align-items: stretch;
  }
  .filter-bar > label { display: none; }
  .filter-bar:has(.segmented + .segmented) > select { grid-column: 1 / -1; }
  /* In Read and Play modes the segmented toggles are display:none, so the
     sort select is the only visible control — span it full-width so widths
     match across the watchlist and watched tabs. */
  body.cat-books .filter-bar > select,
  body.cat-games .filter-bar > select { grid-column: 1 / -1; }
  /* Browse in games mode: only #discoverPlatformFilter is visible in the
     filter-bar (siblings are display:none). Span full width so it doesn't
     sit alone in the left column with empty space to the right. */
  body.cat-games #discoverPlatformFilter { grid-column: 1 / -1; }
  .filter-bar > select {
    width: 100%;
    height: 36px;
    padding: 0 32px 0 12px;
  }
  .segmented { width: 100%; padding: 2px; }
  /* For You toolbar packs two segmenteds — [Swipe|Grid] + [All|Movies|TV] —
     and the default `.segmented { width: 100% }` above stacks them onto two
     rows, pushing the swipe action buttons below the viewport on iPhone-class
     widths (#322 follow-up). Override so they share one row.
     #451: at iPhone-class widths the 2/5 slot couldn't fit longer locale
     labels (es "Cuadrícula" → "Cuadrí…"). The view-mode toggle is an
     icon-led control with intuitive glyphs (cards-stack vs grid) and full
     `aria-label`s, so collapse it to icon-only here and let the type
     toggle take whatever room is left. */
  #foryouPanel .tab-toolbar .segmented { width: auto; min-width: 0; }
  #foryouPanel .tab-toolbar #foryouMode { flex: 0 0 auto; }
  #foryouPanel .tab-toolbar #foryouType { flex: 0 1 auto; }
  #foryouPanel .tab-toolbar #foryouType button { padding: 0 4px; font-size: 11px; }
  #foryouPanel .tab-toolbar #foryouFilterPill { flex: 0 0 auto; padding: 7px 10px; }
  /* Watchlist mode-toggle: 3 buttons (All items / My lists / Upcoming). The
     default mobile `.segmented { width: 100% }` would stretch it across the
     whole row and push the +Filter pill to a second line. Force auto-width
     so the toggle sizes to its content and the pill can sit immediately to
     its right on the same row, per the design ask. */
  #watchlistPanel .tab-toolbar .watchlist-mode-toggle { width: auto; flex: 0 0 auto; min-width: 0; }
  #watchlistPanel .tab-toolbar .watchlist-mode-toggle button { flex: 0 1 auto; }
  /* Filter pill on mobile carries icon-only chrome to save room next to the
     3-segment toggle. The accompanying `data-i18n` text remains for a11y. */
  #watchlistPanel .tab-toolbar .filter-pill { padding: 7px 10px; }
  /* Watched view-toggle: same treatment as watchlist mode-toggle above —
     force auto-width so the [Items | Stats] segmented sizes to its content
     and the +Filter pill sits immediately to its right on the same row
     (#new). Without this, the default `.segmented { width: 100% }` mobile
     rule above stretches the toggle full-width and pushes the pill onto
     a second line — inconsistent with every other tab's mobile layout. */
  #watchedPanel .tab-toolbar .watched-view-toggle { width: auto; flex: 0 0 auto; min-width: 0; }
  #watchedPanel .tab-toolbar .watched-view-toggle button { flex: 0 1 auto; }
  #watchedPanel .tab-toolbar .filter-pill { padding: 7px 10px; }
  /* min-height (not height) so the 36px touch-target override below can take
     effect without buttons overflowing — keeps active and unselected pills
     the same height inside the wrapper. */
  .segmented button {
    flex: 1;
    font-size: 12px;
    padding: 0 6px;
    min-height: 32px;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
  }
  /* Trim the search bar's margins so the filter strip below it sits
     closer to the content. The right-padding here gives room for the
     AI button (1.7.37 — collapses to a 36px icon-only square on
     mobile via the @media (max-width: 480px) override above) plus
     the 32px clear-X when both are visible. */
  .search-bar { margin-bottom: 12px; }
  .search-bar input { padding: 11px 88px 11px 40px; }
  .search-bar:not(:has(.search-bar-clear:not([hidden]))) input {
    padding-right: 50px;
  }
  .search-icon { left: 12px; }

  /* Movie modal padding tighter */
  .providers-section { padding: 0 16px 20px; }
  .similar-section { padding: 16px 16px 20px; }
  .similar-grid {
    grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
    gap: 10px;
  }

  /* Bigger, more prominent close button on phones — easier to tap */
  .modal-close-floating {
    top: 12px;
    margin: 12px 12px -56px 0;
    width: 44px; height: 44px;
    font-size: 22px;
  }

  /* Genre chips and liked-movie chips: horizontal scroll instead of wrapping
     so they take one row instead of five on small screens */
  .genre-chips,
  .liked-chips {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 6px;
    margin-left: -14px;
    margin-right: -14px;
    padding-left: 14px;
    padding-right: 14px;
  }
  .genre-chips::-webkit-scrollbar,
  .liked-chips::-webkit-scrollbar { display: none; }
  .genre-chip {
    flex-shrink: 0;
    padding: 6px 12px;
    font-size: 12px;
  }
  .liked-chip {
    flex-shrink: 0;
    font-size: 11px;
    padding: 4px 9px;
  }

  /* Light-theme tab text override (#587 follow-up). The translucent bottom
     bar (50% bg + blur) washes out --text-dim on light themes — the dimmed
     warm grey reads too light against the frosted backdrop. Promote inactive
     tabs to full --text on the two light themes so the labels stay legible.
     Active tabs already use --accent which is darkened-for-AA on these themes
     so no change needed there. */
  body.cat-movies.theme-light .tab,
  body.cat-movies.theme-auto.system-light .tab,
  body.cat-games.theme-light .tab,
  body.cat-games.theme-auto.system-light .tab,
  /* Read is a light parchment theme by default (no .theme-light suffix) —
     its --text-dim is a medium warm brown that washes out against the
     frosted backdrop. Bump inactive tab labels to the full --text. */
  body.cat-books .tab {
    color: var(--text);
  }
}


/* ===== Accessibility (WCAG 2.1 AA) =====
   Centralised so a11y wins are easy to find and review. Source order is
   late-on-purpose: focus-visible / reduced-motion overrides need to win
   against the per-component transitions defined above. */

/* Visually-hidden text for screen readers — used by aria-labels we don't
   want sighted users to see (e.g. landmark headings, nav alt-text). */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Skip-to-content link — first tab stop for keyboard users. Hidden until
   focused. WCAG 2.4.1 Bypass Blocks. */
.skip-link {
  position: fixed;
  top: 0; left: 12px;
  z-index: 1000;
  padding: 10px 16px;
  background: var(--accent);
  color: white;
  border-radius: 0 0 8px 8px;
  font-size: 14px;
  font-weight: 600;
  transform: translateY(-110%);
  transition: transform 0.15s;
}
.skip-link:focus {
  transform: translateY(0);
  outline: 3px solid var(--text);
  outline-offset: 2px;
}

/* Visible keyboard focus on every interactive element. We use
   :focus-visible so mouse-clicks don't get a ring, but keyboard
   navigation always does. WCAG 2.4.7. */
button:focus-visible,
a:focus-visible,
[role="button"]:focus-visible,
.tab:focus-visible,
.segmented button:focus-visible,
.swipe-btn:focus-visible,
.modal-close:focus-visible,
.modal-close-floating:focus-visible,
.settings-modal-close:focus-visible,
.movie-card:focus-visible,
.alerts-banner-row:focus-visible,
.rate-star:focus-visible,
.update-banner-btn:focus-visible,
.update-banner-close:focus-visible,
.alerts-banner-dismiss:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 6px;
}
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Touch-target sizing: enforce 44×44 minimum on devices without a fine
   pointer (phones / tablets). Desktop hover layouts stay unchanged so
   dense filter rows don't reflow. WCAG 2.5.5 (AAA) but recommended for
   AA compliance on mobile. */
@media (hover: none) and (pointer: coarse) {
  .modal-close,
  .modal-close-floating,
  .settings-modal-close,
  .alerts-banner-dismiss,
  .update-banner-close {
    min-width: 44px;
    min-height: 44px;
  }
  .segmented button,
  .tab,
  .rate-star {
    min-height: 36px;
  }
}

/* ===== Personal stats dashboard (#65) — sub-view of the Watched tab.
   The Watched panel carries data-view="items"|"stats"; the rules below
   hide whichever sub-view is inactive. Stats charts use plain divs +
   SVG with theme colors via var(--accent), so the per-category palette
   flows through automatically. The .watched-view-toggle used to be
   width: 100% with flex: 1 buttons, which made it visibly oversized vs.
   every other segmented in the app — #271 dropped that override so
   it inherits the standard inline-flex segmented sizing. */
#watchedPanel[data-view="stats"] .watched-items-view { display: none; }
#watchedPanel[data-view="items"] .watched-stats-view { display: none; }

.watched-stats-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 18px;
}
.watched-stats-toolbar .segmented { flex: 1 1 auto; }
@media (max-width: 520px) {
  .watched-stats-toolbar { flex-direction: column; gap: 8px; }
  .watched-stats-toolbar .segmented { width: 100%; }
}

#watchedStats { display: flex; flex-direction: column; gap: 18px; }

.stat-headline {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 22px 20px;
  text-align: center;
  /* Enables cqw units on .display-num children (#970). */
  container-type: inline-size;
}
.stat-headline-big {
  /* Sentence label beneath the .display-num hero count (#970).
     Dropped from hero 22px to a supporting 15px; the large number
     already carries the visual weight. */
  font-size: 15px;
  font-weight: 500;
  color: var(--text-dim);
  margin-top: 6px;
  line-height: 1.4;
}
.stat-headline-sub {
  margin-top: 8px;
  font-size: 14px;
  color: var(--text-dim);
}

.stat-section {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 18px 18px 16px;
}
.stat-section-title {
  /* Converges onto the .eyebrow primitive (#970): 12px/600/uppercase/0.1em.
     The margin stays as the section layout is not being restructured. */
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text-dim);
  margin: 0 0 14px;
}
.stat-section-meta {
  margin: -6px 0 12px;
  font-size: 13px;
  color: var(--text-dim);
}
.stat-section-note {
  margin: 12px 0 0;
  font-size: 11px;
  color: var(--text-muted);
  font-style: italic;
}
.stat-empty-line {
  margin: 0;
  font-size: 13px;
  color: var(--text-muted);
}

/* Stacked horizontal hours bar with a per-type swatch legend underneath.
   Segments are typed (.seg-movie / -tv / -book / -game) so each gets its
   own tonal shade off the active accent — keeping the chart readable
   without diverging from the per-category palette. */
.stat-hours-bar {
  display: flex;
  height: 14px;
  width: 100%;
  border-radius: 7px;
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--border);
}
.stat-hours-seg {
  height: 100%;
  transition: width 700ms cubic-bezier(0.2, 0, 0.1, 1);
}
.stat-hours-seg.seg-movie { background: var(--chart-1); }
.stat-hours-seg.seg-tv    { background: var(--chart-2); }
.stat-hours-seg.seg-book  { background: var(--chart-3); }
.stat-hours-seg.seg-game  { background: var(--chart-4); }
.stat-hours-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 18px;
  margin-top: 12px;
  font-size: 12px;
  color: var(--text-dim);
}
.stat-hours-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.stat-hours-swatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  display: inline-block;
}
.stat-hours-swatch.seg-movie { background: var(--chart-1); }
.stat-hours-swatch.seg-tv    { background: var(--chart-2); }
.stat-hours-swatch.seg-book  { background: var(--chart-3); }
.stat-hours-swatch.seg-game  { background: var(--chart-4); }
.stat-hours-legend-val { color: var(--text); font-weight: 600; margin-left: 2px; }

/* Horizontal bars (genres + ratings). Track + fill, with a label cell on
   the left and a count cell on the right. Fill animates from 0 width on
   first paint via a CSS keyframe — global @prefers-reduced-motion zeroes
   the transition-duration. */
.stat-bars {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.stat-bar-row {
  display: grid;
  grid-template-columns: minmax(70px, 100px) 1fr 32px;
  align-items: center;
  gap: 10px;
  font-size: 13px;
}
.stat-bar-label {
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.stat-bar-track {
  height: 8px;
  background: var(--bg);
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid var(--border);
}
.stat-bar-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 4px;
  /* Inline width sets the *final* size; we animate via scaleX so the bar
     appears to grow from the left. transform-origin keeps the growth
     anchored to the start of the track. */
  transform-origin: left center;
  animation: statBarGrow 700ms cubic-bezier(0.2, 0, 0.1, 1) backwards;
}
.stat-bar-value {
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--text-dim);
  text-align: right;
}
.stat-rating-stars {
  letter-spacing: 1px;
  color: var(--accent);
  font-size: 14px;
}
.stat-rating-empty { color: var(--border); }

@keyframes statBarGrow { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* Vertical bars (decades + monthly cadence). The SVG holds only the bars
   and uses preserveAspectRatio="none" so they stretch full-width on any
   viewport. Labels live in a sibling HTML row because that same non-uniform
   stretch was blowing up SVG <text> glyphs by the X-scale ratio (#272). */
.stat-vbar-chart {
  display: block;
}
.stat-vbars {
  width: 100%;
  height: 120px;
  display: block;
  overflow: visible;
}
.stat-vbar rect {
  transform-origin: bottom;
  animation: statVbarGrow 700ms cubic-bezier(0.2, 0, 0.1, 1) backwards;
}
.stat-vbar-labels {
  display: flex;
  margin-top: 6px;
}
.stat-vbar-labels > .stat-vbar-label {
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  font-size: 11px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@keyframes statVbarGrow { from { transform: scaleY(0); } to { transform: scaleY(1); } }

/* Empty state — fewer than ~5 watched items in the current scope. */
.stats-empty {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 32px 24px;
  text-align: center;
}
.stats-empty-headline {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 10px;
}
.stats-empty-body {
  margin: 0;
  font-size: 14px;
  color: var(--text-dim);
  line-height: 1.5;
}

/* ===== Year-in-review recap (#724) ===== */

/* Entry-point banner at the top of the Stats view. Accent left border marks
   it as a call-out distinct from the stat cards below it. */
#recapBanner { margin-bottom: 16px; }
.recap-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 56px;
  padding: 14px 16px;
  border: 1px solid color-mix(in srgb, var(--accent) 32%, var(--border));
  border-inline-start: 4px solid var(--accent);
  border-radius: 14px;
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
  color: var(--text);
  text-align: start;
  cursor: pointer;
  transition: background 0.15s, transform 0.1s;
}
.recap-banner:hover { background: color-mix(in srgb, var(--accent) 14%, var(--surface)); }
.recap-banner:active { transform: scale(0.99); }
.recap-banner-text { display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; }
.recap-banner-title { font-size: 15px; font-weight: 700; }
.recap-banner-sub { font-size: 13px; color: var(--text-dim); }
.recap-banner-chevron { font-size: 22px; color: var(--accent); line-height: 1; }

/* Modal — the card stack floats on the deeper --bg base so it reads as a
   distinct moment, not the always-on Stats panel. */
.recap-modal {
  background: var(--bg);
  max-width: 560px;
  gap: 14px;
  padding: 0 18px 22px;
}
.recap-modal #recapBody {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* #1011 — sticky share bar pinned to the bottom of the scrolling recap modal.
   Sibling of #recapBody (not a child) so the 5-star detail-view innerHTML
   swap never destroys it. The gradient lets content fade out as it scrolls
   beneath the button. */
.recap-share-bar {
  position: sticky;
  bottom: 0;
  z-index: 2; /* above positioned cover imgs inside #recapBody */
  margin-bottom: -22px; /* cancel .recap-modal's bottom padding so the bar hugs the scrollport edge at full scroll */
  padding: 12px 0 calc(12px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(transparent, var(--bg) 40%);
}
.recap-share-btn {
  width: 100%;
  gap: 8px;
}
.recap-share-icon {
  display: inline-flex;
  flex: 0 0 auto;
}

/* Hero — full-bleed year with a subtle accent glow. */
.recap-hero {
  position: relative;
  text-align: center;
  padding: 32px 16px 28px;
  border-radius: 16px;
  background:
    radial-gradient(circle at 50% 30%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%),
    var(--surface-2);
  overflow: hidden;
}
.recap-hero-eyebrow {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text-dim);
}
.recap-hero-year {
  font-size: 56px;
  font-weight: 900;
  line-height: 1.05;
  color: var(--accent);
  margin: 6px 0 8px;
}
.recap-hero-sub { font-size: 14px; color: var(--text); }

.recap-year-nav {
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.recap-year-btn {
  min-width: 44px;
  min-height: 44px;
  font-size: 18px;
  color: var(--text);
  cursor: pointer;
}
.recap-year-btn:disabled { opacity: 0.35; cursor: default; }
.recap-year-current { min-width: 56px; text-align: center; font-weight: 700; }

.recap-rule {
  height: 1px;
  width: 100%;
  background: color-mix(in srgb, var(--accent) 20%, transparent);
}

/* Count tiles in the breakdown section. */
.recap-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 10px;
}
.recap-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 14px 10px;
  border-radius: 12px;
  background: var(--bg);
  border: 1px solid var(--border);
}
.recap-tile-num { font-size: 26px; font-weight: 800; color: var(--accent); }
.recap-tile-label { font-size: 12px; color: var(--text-dim); text-align: center; }

/* Standout callouts get the accent treatment to sit above the count cards. */
.recap-section-standout {
  border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
  background:
    radial-gradient(circle at 0% 0%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 60%),
    var(--surface);
}
.recap-standouts { display: flex; flex-direction: column; gap: 12px; }
.recap-standout { display: flex; flex-direction: column; gap: 2px; }
.recap-standout-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
}
.recap-standout-value { font-size: 15px; font-weight: 700; color: var(--text); }
.recap-standout-sub { font-size: 13px; color: var(--text-dim); }

/* Five-star preview card row inside .recap-standouts */
.recap-five-star-row {
  display: flex;
  flex-direction: row;
  gap: 10px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 4px;
  justify-content: flex-start;
}
.recap-five-star-row::-webkit-scrollbar { display: none; }

.recap-five-star-card {
  display: flex;
  flex-direction: column;
  width: 96px;
  flex-shrink: 0;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-align: center;
  transition: transform 0.1s;
}
.recap-five-star-card:active { transform: scale(0.97); }
.recap-five-star-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 12px;
}

.recap-five-star-card-poster {
  aspect-ratio: 2 / 3;
  background: var(--surface-2);
  position: relative;
  overflow: hidden;
  border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--accent) 36%, var(--border));
  display: flex;
  align-items: center;
  justify-content: center;
}
.recap-five-star-card-poster .poster-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.recap-five-star-card-poster .book-placeholder {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.recap-five-star-card-poster .recap-type-emoji {
  display: flex; align-items: center; justify-content: center;
}

.recap-five-star-badge {
  position: absolute;
  inset-block-start: 5px;
  inset-inline-end: 5px;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  padding: 2px 6px;
  line-height: 1.4;
  min-width: 18px;
  text-align: center;
}

.recap-five-star-type-pill {
  margin-top: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  font-size: 11px;
  font-weight: 600;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* Detail view (inline body swap) */
.recap-detail-header {
  position: sticky;
  top: 0;
  background: var(--bg);
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 0 16px;
}
.recap-detail-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
}
/* Back button reuses .recap-year-btn, which already guarantees a 44pt hit area
   (min-width/min-height: 44px); this rule documents that and keeps it explicit. */
.recap-detail-back { min-width: 44px; min-height: 44px; }
.recap-detail-list { display: flex; flex-direction: column; }
.recap-detail-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.recap-detail-item:last-child { border-bottom: none; }

.recap-detail-thumb {
  width: 48px;
  aspect-ratio: 2 / 3;
  border-radius: 6px;
  overflow: hidden;
  background: var(--surface-2);
  flex-shrink: 0;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}
.recap-detail-thumb .poster-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.recap-detail-thumb .book-placeholder {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.recap-detail-thumb .recap-type-emoji { display: flex; align-items: center; justify-content: center; }

.recap-detail-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1 1 auto;
}
.recap-detail-item-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.recap-detail-item-month {
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 2px;
}
.recap-detail-stars {
  color: var(--accent);
  font-size: 12px;
  flex-shrink: 0;
}

@media (max-width: 520px) {
  .recap-hero-year { font-size: 44px; }
}

/* Honour prefers-reduced-motion across the whole app. Strips out any
   transitions / animations / smooth-scroll for users who've asked the OS
   to minimise motion. Skeleton shimmer was already covered above; this
   block catches everything else (modal fades, swipe-deck transforms,
   hover lifts, segmented sliders). WCAG 2.3.3. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .movie-card:hover { transform: none; }
}

/* Bumped --text-muted on each theme so secondary copy (movie-year, byline,
   meta) clears 4.5:1 against the surface. Values were too dim before and
   failed Lighthouse contrast in three places (.movie-year on .movie-card,
   .tab-count, .poster-badge.unavailable). */
:root { --text-muted: #8C8473; }
body.cat-books { --text-muted: #6F5E48; }
body.cat-games { --text-muted: #8B7BBE; }

/* Utility classes for repeated inline-style patterns (#340). The CSP no longer
   allows `style="..."` attributes (no `'unsafe-inline'` in style-src), so these
   replace the previously-inline styles for static cases; truly dynamic values
   (cover URLs, percentages) are applied programmatically — see applyDataStyles
   in src/util.js. Kept narrow on purpose; resist growing this into a general
   utility framework. */
.text-warning { color: var(--warning); }
.text-dim-sm { color: var(--text-dim); font-size: 13px; }
.text-dim-md { color: var(--text-dim); font-size: 14px; }
.text-dim-sm-pad-y { color: var(--text-dim); font-size: 13px; padding: 8px 0; }
.error-inline { color: #f87171; font-size: 13px; padding: 12px; }
.error-inline-y { color: #f87171; font-size: 13px; padding: 8px 0; }
.empty-state.empty-state-compact { padding: 32px; }
.spinner-block { margin: 0 auto 12px; }
.provider-chip-static { cursor: default; }
.provider-chip-static-muted { cursor: default; opacity: 0.85; }
/* Provider chip rendered as a real `<a>` (Where to find it / Where to play /
   JustWatch link). Resets the global `a { color: var(--accent); }` and the
   default underline so it sits visually with the static `<span>` chips. */
a.provider-chip { text-decoration: none; color: var(--text); }
.h3-tag-cinema { background: rgba(255, 77, 109, 0.15); color: var(--accent); }
.modal-backdrop.modal-backdrop-placeholder {
  height: 160px;
  background: linear-gradient(135deg, var(--surface-2), var(--surface-3));
}
.modal-backdrop.modal-backdrop-fallback {
  background: linear-gradient(135deg, var(--surface-2), var(--surface-3));
}
.modal-foryou-empty { color: var(--text-dim); font-size: 14px; }
.justwatch-link-row { margin-top: 12px; font-size: 12px; color: var(--text-muted); }
.justwatch-link-row a { color: var(--text-dim); }
.similar-badge.similar-badge-tv {
  background: var(--vpn);
  left: 4px; right: auto; top: 4px;
  color: var(--bg);
}
/* Swipe-card backdrops (#340). The dynamic url() comes from data-bg-image
   (applied by applyDataStyles); positioning + sizing live here as static CSS. */
.swipe-card-bg {
  position: absolute; inset: 0;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.swipe-card-bg.swipe-card-bg-contain { background-size: contain; background-color: var(--surface-2); }
/* #903 - movie/tv/game swipe cover as a real <img> (so the reveal fires on the
   image's own load event, like books). Fills the card like the old bg-div did. */
.swipe-cover-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  background-color: var(--surface-2);
  display: block;
}
.swipe-card-fallback-bg {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2);
  color: var(--text-dim);
  opacity: 0.3;
}
.swipe-card-overview-faded { opacity: 0.6; }
.loading-row.loading-row-fill {
  position: absolute; inset: 0;
  align-items: center; justify-content: center;
}
/* Settings full-width form controls (#340 — replaces inline width/padding). */
.full-width { width: 100%; }
.btn-magic-link {
  width: 100%;
  padding: 12px 18px;
  background: var(--accent);
  color: #fff;
  border: 1px solid var(--accent);
  border-radius: 10px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s, box-shadow 0.15s;
  box-shadow: 0 4px 14px -4px rgba(212, 175, 55, 0.45);
  margin-bottom: 4px;
}
.btn-magic-link:hover { background: var(--accent-hover); box-shadow: 0 6px 18px -4px rgba(212, 175, 55, 0.55); }
.btn-magic-link:disabled { opacity: 0.55; cursor: not-allowed; box-shadow: none; }
.text-dim-sm-pad { color: var(--text-dim); font-size: 13px; padding: 12px 0; }

/* OAuth sign-in (#132.1.1, redesigned #538). Compact 3-column tile grid
 * shared by Settings ▸ Account and the onboarding wizard step 5. Each tile
 * is uniform and theme-aware so the cluster of 6 providers reads as one
 * cohesive group instead of a clashing rainbow of brand-color rectangles;
 * the brand color survives only inside the icon. Apple HIG compliance:
 * since every provider gets identical shape + emphasis, "Sign in with Apple"
 * is treated equivalently to the others (HIG allows equivalent or greater). */
.oauth-providers { display: flex; flex-direction: column; gap: 12px; margin-top: 14px; }
.oauth-divider {
  display: flex; align-items: center; gap: 10px;
  color: var(--text-dim); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em;
}
.oauth-divider::before,
.oauth-divider::after {
  content: ''; flex: 1; height: 1px; background: var(--border, rgba(255, 255, 255, 0.12));
}
.oauth-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}
@media (max-width: 360px) {
  .oauth-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.oauth-btn {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px;
  padding: 12px 6px 10px;
  min-height: 76px;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: 12px; font-weight: 600; text-decoration: none;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, transform 0.08s;
}
.oauth-btn:hover {
  background: var(--surface-3);
  border-color: var(--text-dim);
}
.oauth-btn:active { transform: scale(0.97); }
.oauth-btn:focus-visible {
  outline: 2px solid var(--accent, #f6c026);
  outline-offset: 2px;
}
.oauth-icon { width: 24px; height: 24px; flex-shrink: 0; }

/* #790 / #808: Terms-consent gate. Apple Guideline 1.2 requires explicit
 * agreement before sign-in; this card locks the OAuth tiles + magic-link
 * button until the checkbox is checked. The whole card is the tap target. */
.auth-consent {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-top: 0;
  margin-bottom: 12px;
  padding: 14px 16px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-size: 14px;
  line-height: 1.5;
  color: var(--text);
  cursor: pointer;
}
@media (max-width: 360px) {
  .auth-consent { padding: 12px 14px; }
}
/* Native input visually hidden but still keyboard-accessible (nested in the label). */
.auth-consent-check {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
/* Custom visual indicator replaces the native checkbox. */
.auth-consent-indicator {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  margin-top: 1px;
  border-radius: 7px;
  border: 1.5px solid var(--border);
  background: var(--surface-3);
  transition: background 0.15s, border-color 0.15s;
  display: flex;
  align-items: center;
  justify-content: center;
}
.auth-consent-indicator svg {
  visibility: hidden;
  color: var(--on-accent);
}
.auth-consent-check:checked + .auth-consent-indicator {
  background: var(--accent);
  border-color: var(--accent);
}
.auth-consent-check:checked + .auth-consent-indicator svg {
  visibility: visible;
}
.auth-consent-check:focus-visible + .auth-consent-indicator {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
@media (hover: hover) and (pointer: fine) {
  .auth-consent:hover .auth-consent-indicator {
    border-color: var(--text-muted);
  }
  .auth-consent:hover {
    background: var(--surface-3);
  }
}
.auth-consent-text label { cursor: pointer; }
.auth-consent-text a {
  color: var(--accent);
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: color-mix(in srgb, var(--accent) 60%, transparent);
  padding: 2px 0;
}
.auth-consent-text a:hover {
  color: var(--accent-hover);
  text-decoration-color: var(--accent-hover);
}
.oauth-locked .oauth-btn {
  opacity: 0.45;
  pointer-events: none;
  filter: grayscale(0.4);
}
@keyframes authConsentFlash {
  0%, 100% { border-color: var(--border); }
  40% { border-color: var(--accent); }
}
.auth-consent--flash { animation: authConsentFlash 0.6s ease; }
.auth-consent--error { border-color: var(--cinema); }
.auth-consent--error .auth-consent-indicator { border-color: var(--cinema); }
.auth-consent--agreed { border-color: var(--border); }
.auth-consent:focus-within { border-color: var(--accent); }

/* Brand-color overrides for SVGs that use currentColor — keeps each logo
 * visually identifiable inside the otherwise-neutral tile. Apple and Steam
 * stay in --text (monochrome brands that render in either polarity). Google
 * and MAL have hardcoded fills in the SVG so no override needed. */
.oauth-btn-trakt .oauth-icon { color: #ed1c24; }
.oauth-btn-discord .oauth-icon { color: #5865F2; }

/* #776 OAuth-primary hierarchy. OAuth is the only zero-email signup path
 * (magic-link is metered by Resend at 95/day), so the provider tiles lead and
 * the email form is demoted below an "or continue with email" divider. An
 * accent-tinted resting border lifts the tile cluster as the preferred zone
 * without filling the tiles, so every provider keeps equal emphasis (Apple
 * HIG). When no provider is configured the whole .oauth-providers block is
 * [hidden], so the sibling demotion rules below never fire and the email
 * button keeps its solid primary style — email-primary fallback. */
.oauth-btn { border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); }
.oauth-providers:not([hidden]) ~ .btn-magic-link,
.oauth-providers:not([hidden]) ~ .api-row button {
  background: transparent;
  color: var(--text-dim);
  border: 1px solid var(--border);
  box-shadow: none;
}
.oauth-providers:not([hidden]) ~ .btn-magic-link:hover,
.oauth-providers:not([hidden]) ~ .api-row button:hover {
  background: var(--surface-2);
  color: var(--text);
  box-shadow: none;
}
.oauth-providers:not([hidden]) ~ .wizard-field,
.oauth-providers:not([hidden]) ~ .api-row { margin-top: 14px; }

/* ===== Party tab (#188, sub-issue c #457) =================================
 * Lobby-first surface: minimal chrome, large room code, copy buttons, the
 * participant chips, and a primary Start button for the host. Lives in the
 * #partyPanel section. The whole content is rendered by src/ui/party.js
 * into #partyContent — the rules below are mode-agnostic (no [data-mode]
 * gating yet; the JS re-renders the inner DOM per mode).
 */
#partyPanel { padding: 16px; max-width: 720px; margin: 0 auto; }
.party-home, .party-wizard, .party-lobby, .party-empty {
  display: flex; flex-direction: column; gap: 16px;
}
.party-home h2, .party-wizard h2, .party-lobby h2, .party-empty h2 {
  font-size: 24px; font-weight: 700; margin: 0;
}
.party-home .subtitle, .party-empty p, .party-help {
  color: var(--text-dim); font-size: 14px; margin: 0;
}
.party-empty-copy { color: var(--text-dim); font-size: 15px; line-height: 1.5; margin: 0; }
/* Party home hero (#new). Decorative people icon inside a soft accent ring
   plus a confident headline + supporting subtitle. Sets the tone for the
   page: "we're inviting you to do this together" rather than the previous
   bare "Party / Decide together" pair. */
.party-hero {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 10px;
  padding: 28px 16px 8px;
  margin-bottom: 4px;
}
.party-hero-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  border-radius: 22px;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent) inset,
    0 14px 32px -10px color-mix(in srgb, var(--accent) 38%, transparent);
  margin-bottom: 4px;
}
.party-hero-icon svg { display: block; }
.party-home .party-hero-title,
.party-home h2.party-hero-title {
  font-size: 32px;
  font-weight: 800;
  letter-spacing: -0.02em;
  margin: 0;
  background: linear-gradient(
    135deg,
    var(--text) 0%,
    color-mix(in srgb, var(--accent) 70%, var(--text)) 60%,
    var(--accent) 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}
.party-hero-subtitle {
  font-size: 15.5px;
  line-height: 1.45;
  color: var(--text-dim);
  margin: 0;
  max-width: 32ch;
}

.party-home-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
.party-home-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px 18px;
  border-radius: 14px;
  font-size: 15.5px;
  font-weight: 600;
  border: 1px solid var(--border);
  background: var(--bg-card);
  color: var(--text);
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.18s ease, border-color 0.18s ease,
    background 0.18s ease;
}
.party-home-cta:hover:not(:disabled),
.party-home-cta:focus-visible:not(:disabled) {
  transform: translateY(-1px);
  border-color: var(--accent);
  box-shadow: 0 8px 20px -8px color-mix(in srgb, var(--accent) 35%, transparent);
}
.party-home-cta:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: none;
}
.party-home-cta-icon {
  flex-shrink: 0;
  opacity: 0.95;
}
.party-home-actions .party-home-cta.primary {
  background: linear-gradient(
    135deg,
    var(--accent) 0%,
    color-mix(in srgb, var(--accent) 80%, #fff) 100%
  );
  color: white;
  border-color: var(--accent);
  box-shadow: 0 10px 24px -10px color-mix(in srgb, var(--accent) 55%, transparent);
}
.party-home-actions .party-home-cta.primary:hover:not(:disabled),
.party-home-actions .party-home-cta.primary:focus-visible:not(:disabled) {
  box-shadow: 0 14px 28px -10px color-mix(in srgb, var(--accent) 70%, transparent);
}
/* Pre-#new fallback rule for any caller that still emits plain buttons
   in .party-home-actions (e.g. a future renderer or A/B). Keeps them
   visually in step until they migrate to .party-home-cta. */
.party-home-actions button:not(.party-home-cta) {
  padding: 14px 16px;
  border-radius: 12px;
  font-size: 16px;
  font-weight: 600;
  border: 1px solid var(--border);
  background: var(--bg-card);
  color: var(--text);
  cursor: pointer;
}

/* Screen-header row that pairs the back button with the screen title so the
   two sit on the same baseline. Previously the .party-back stacked above the
   <h2> because their parent was a column flex; users read the floating arrow
   as obstructing the heading. */
.party-screen-header {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 36px;
}
.party-screen-header h2 {
  margin: 0;
  flex: 1;
  min-width: 0;
}

.party-back {
  background: transparent; border: 0; color: var(--text);
  font-size: 22px; padding: 4px 10px; cursor: pointer; line-height: 1;
  flex-shrink: 0;
}
/* Legacy standalone .party-back (no .party-screen-header parent) — keeps the
   self-aligned top-start behaviour for any future caller that doesn't wrap. */
.party-wizard > .party-back,
.party-lobby > .party-back,
.party-join > .party-back { align-self: flex-start; }
.party-back:hover, .party-back:focus-visible { color: var(--accent); }

.party-category-grid { display: grid; grid-template-columns: 1fr; gap: 10px; }
@media (min-width: 480px) { .party-category-grid { grid-template-columns: repeat(3, 1fr); } }
.party-category {
  display: flex; flex-direction: column; gap: 10px; padding: 18px;
  border-radius: 14px; border: 2px solid var(--border); background: var(--bg-card);
  color: var(--text); font-size: 15px; cursor: pointer; align-items: center;
}
.party-category .category-icon { width: 32px; height: 32px; display: block; }
.party-category.selected { border-color: var(--accent); background: var(--accent-soft, var(--bg-card)); }

.party-filter-group { border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; display: flex; flex-direction: column; gap: 6px; }
.party-filter-group legend { padding: 0 6px; font-weight: 600; }
.party-filter-group label { display: flex; align-items: center; gap: 8px; font-size: 14px; padding: 6px 0; cursor: pointer; }
.party-filter-note { color: var(--text-dim); font-size: 13px; margin: 0; }

.party-wizard-footer { display: flex; gap: 10px; margin-top: 8px; }
.party-wizard-footer button { flex: 1; padding: 12px 14px; border-radius: 10px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); font-weight: 600; cursor: pointer; }
.party-wizard-footer button[disabled] { opacity: 0.55; cursor: not-allowed; }

.party-join input {
  text-align: center; font-size: 28px; letter-spacing: 0.4em; font-weight: 700;
  text-transform: uppercase; padding: 14px; border: 2px solid var(--border);
  border-radius: 12px; background: var(--bg-card); color: var(--text);
}
.party-join input:focus { outline: none; border-color: var(--accent); }

.party-error { color: var(--danger); font-size: 14px; margin: 0; }
.party-toast { color: var(--success, #27ae60); font-size: 13px; margin: 0; }

.party-code-block {
  border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border));
  border-radius: 20px;
  padding: 22px 20px 18px;
  text-align: center;
  background:
    radial-gradient(
      circle at 50% 0%,
      color-mix(in srgb, var(--accent) 12%, transparent) 0%,
      transparent 60%
    ),
    var(--bg-card);
  box-shadow: 0 12px 32px -16px color-mix(in srgb, var(--accent) 40%, transparent);
}
.party-code-label { color: var(--text-dim); font-size: 13px; text-transform: uppercase; letter-spacing: 0.1em; margin: 0; }
.party-code {
  font-size: 40px; font-weight: 800; letter-spacing: 0.3em;
  margin: 10px 0 14px;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  background: linear-gradient(135deg, var(--text), var(--accent));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}
/* QR thumbnail in the lobby (#760). Client-rendered SVG from the
   vendored qrcode bundle (see src/ui/party.js renderPartyQr); we wrap
   it in a white tile so the dark-theme card doesn't bleed contrast
   through transparent SVG areas, and cap the rendered size at 160px
   square so it stays scannable but doesn't dominate the code-block.
   The placeholder is a faint shimmer block shown for the few hundred
   ms it takes the qrcode bundle to load on a cold cache. */
.party-code-qr {
  margin: 12px auto;
  width: 160px;
  height: 160px;
  background: #fff;
  border-radius: 10px;
  padding: 8px;
  box-sizing: content-box;
  display: flex;
  align-items: center;
  justify-content: center;
}
.party-code-qr svg,
.party-code-qr img {
  display: block;
  width: 100%;
  height: 100%;
}
.party-code-qr-placeholder {
  width: 100%;
  height: 100%;
  border-radius: 6px;
  background: linear-gradient(90deg, #eee 0%, #f6f6f6 50%, #eee 100%);
  background-size: 200% 100%;
  animation: party-qr-shimmer 1.1s linear infinite;
}
@keyframes party-qr-shimmer {
  0% { background-position: 100% 0; }
  100% { background-position: -100% 0; }
}
.party-code-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.party-code-actions button { padding: 8px 14px; border-radius: 8px; border: 1px solid var(--border); background: transparent; color: var(--text); font-size: 13px; cursor: pointer; }
.party-code-actions button:hover, .party-code-actions button:focus-visible { background: var(--bg-soft, var(--bg-card)); }

.party-participants h3 { font-size: 15px; margin: 0 0 8px; }
.party-participants ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.party-chip {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 12px 6px 6px; border-radius: 999px; background: var(--bg-card); border: 1px solid var(--border);
  font-size: 14px;
}
.party-chip-host { border-color: var(--accent); }
.party-chip-avatar {
  width: 28px; height: 28px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent); color: white; font-weight: 700; font-size: 13px;
}

.party-lobby-footer { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
.party-lobby-footer button { padding: 12px 14px; border-radius: 10px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); font-weight: 600; cursor: pointer; }
.party-lobby-footer button.danger { color: var(--danger); border-color: var(--border); background: transparent; }
.party-lobby-footer button[disabled] { opacity: 0.55; cursor: not-allowed; }

@media (prefers-reduced-motion: reduce) {
  .party-code, .party-chip, .party-category, .party-home-actions button {
    transition: none !important;
  }
}

/* --- Party swipe surface (sub-issue d, #458) ----------------------------- */
.party-swipe {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 70vh;
}
.party-swipe-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.party-swipe-counter {
  margin: 0;
  color: var(--text-dim);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.party-swipe-threshold {
  margin: 0 0 4px;
  text-align: center;
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
}
.party-match-counter {
  margin: 0;
  text-align: center;
  color: var(--text-dim);
  font-size: 12px;
}
.party-swipe-stack {
  position: relative;
  flex: 1;
  min-height: 420px;
  display: flex;
  /* Was `align-items: stretch` — that let the card stretch vertically to
     fill the stack, and once the stretched height pushed past max-width's
     aspect-ratio width, the card grew well past 380px on wide viewports
     (the user-reported huge-loading-card screenshot). `center` keeps the
     card on its own aspect-ratio sizing so max-width: 380px wins. */
  align-items: center;
  justify-content: center;
}
/* #974 — Party deck stack peek. The .party-behind card is absolute-positioned
   inside the stack so it sits behind the flex-positioned active card.
   --deck-progress interpolation mirrors the For You deck model. */
.party-swipe-card.party-behind {
  position: absolute;
  /* Center within the stack to match where the flex-positioned active card lands. */
  top: 50%;
  left: 50%;
  transform: scale(calc(0.95 + 0.05 * var(--deck-progress, 0)))
             translateX(-50%) translateY(calc(-50% + 8px * (1 - var(--deck-progress, 0))));
  transform-origin: center center;
  filter: brightness(calc(0.85 + 0.15 * var(--deck-progress, 0)));
  pointer-events: none;
  z-index: 0;
  transition: transform 0.2s ease-out, filter 0.2s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .party-swipe-card.party-behind {
    transform: translateX(-50%) translateY(calc(-50% + 8px)) scale(0.95);
    filter: brightness(0.85);
    transition: none;
  }
}
.party-swipe-card {
  position: relative;
  width: 100%;
  /* Match the For-You .swipe-stack width (380px on wide viewports) so guest
     and host cards feel the same as the rest of the app. The narrow-mobile
     override below tracks the For-You mobile sizing rules. */
  max-width: 380px;
  aspect-ratio: 2 / 3;
  border-radius: 18px;
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--border);
  box-shadow: 0 16px 36px rgba(0, 0, 0, 0.5);
  user-select: none;
  touch-action: pan-y;
  cursor: grab;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}
.party-swipe-card:active { cursor: grabbing; }
.party-swipe-card-loading {
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  gap: 16px;
  padding: 24px;
  text-align: center;
}
/* Up-sized spinner for the swipe-card loading state — the default 16px
   .spinner reads as a footnote inside a 380×570 card. */
.party-swipe-spinner {
  width: 40px;
  height: 40px;
  border-width: 3px;
}
.party-swipe-card-waiting {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 32px 24px;
  text-align: center;
}
.party-waiting-icon {
  color: var(--accent);
  animation: party-waiting-pulse 1.8s ease-in-out infinite;
  display: inline-flex;
}
.party-waiting-text { color: var(--text-dim); font-size: 14px; margin: 0; }
@keyframes party-waiting-pulse {
  0%, 100% { transform: rotate(0); opacity: 0.7; }
  50% { transform: rotate(180deg); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .party-waiting-icon { animation: none; }
}
.party-match-declined {
  margin: 8px 0 0;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--bg-soft, var(--bg-card));
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 13px;
  text-align: center;
}
.party-anon-name-label {
  display: block;
  margin: 4px 0 6px;
  font-size: 13px;
  color: var(--text-dim);
}
.party-anon-name-input {
  width: 100%;
  padding: 12px 14px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--bg-card);
  color: var(--text);
  font-size: 15px;
  margin-bottom: 4px;
  box-sizing: border-box;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.party-anon-name-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.party-mine-section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}
.party-mine-section-header h3 { margin: 0; }
.party-mine-clear {
  background: none;
  border: none;
  padding: 4px 6px;
  color: var(--text-dim);
  font-size: 12px;
  cursor: pointer;
  text-decoration: underline;
}
.party-mine-clear:hover, .party-mine-clear:focus-visible { color: var(--text); }
.party-swipe-card-poster {
  position: absolute;
  inset: 0;
  background-position: center;
  background-size: cover;
  background-color: var(--bg-soft, var(--bg-card));
  overflow: hidden;
}
.party-swipe-card-fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 64px;
  font-weight: 800;
  color: var(--text-dim);
  background: var(--bg-soft, var(--bg-card));
}
.party-swipe-card-info {
  position: relative;
  /* Match For-You .swipe-card-info: a fuller bottom gradient + matching
     padding so the title sits the same distance off the card edge. */
  padding: 28px 22px 22px;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.96) 30%,
    rgba(0, 0, 0, 0.6) 70%,
    transparent
  );
  color: #fff;
}
.party-swipe-card-title {
  margin: 0;
  /* Match .swipe-card-title sizing/weight. */
  font-size: 22px;
  font-weight: 700;
  line-height: 1.2;
  margin-bottom: 4px;
}
.party-swipe-card-meta {
  margin: 0;
  font-size: 12px;
  opacity: 0.85;
}

.party-swipe-actions {
  display: flex;
  justify-content: center;
  gap: 24px;
}
.party-swipe-btn {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  border: 2px solid var(--border);
  background: var(--bg-card);
  color: var(--text);
  font-size: 28px;
  cursor: pointer;
  line-height: 1;
}
.party-swipe-btn.party-swipe-skip:hover, .party-swipe-btn.party-swipe-skip:focus-visible { border-color: var(--danger); color: var(--danger); }
.party-swipe-btn.party-swipe-like:hover, .party-swipe-btn.party-swipe-like:focus-visible { border-color: var(--accent); color: var(--accent); }
.party-swipe-hint { text-align: center; color: var(--text-dim); font-size: 12px; margin: 0; }

/* --- Party match modal --------------------------------------------------- */
.party-match {
  display: flex;
  flex-direction: column;
  gap: 16px;
  align-items: center;
  text-align: center;
  padding: 24px 0;
}
.party-match-heading { margin: 0; font-size: 28px; font-weight: 800; }
.party-match-sub { margin: 0; color: var(--text-dim); font-size: 14px; }
.party-match-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  max-width: 280px;
  width: 100%;
}
.party-match-poster {
  width: 200px;
  aspect-ratio: 2 / 3;
  border-radius: 14px;
  background-size: cover;
  background-position: center;
  background-color: var(--bg-soft, var(--bg-card));
  border: 1px solid var(--border);
  position: relative;
  overflow: hidden;
}
.party-match-poster-fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 64px;
  font-weight: 800;
  color: var(--text-dim);
}
.party-match-title { margin: 0; font-size: 22px; font-weight: 700; line-height: 1.25; }
.party-match-providers {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
}
.party-match-providers-label { color: var(--text-dim); font-size: 13px; margin: 0; }
.party-match-providers ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: center;
}
.party-match-providers li a {
  display: inline-block;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text);
  text-decoration: none;
  font-size: 13px;
}
.party-match-providers li a:hover, .party-match-providers li a:focus-visible {
  border-color: var(--accent);
}
.party-match-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
  max-width: 280px;
}
.party-match-actions button {
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-card);
  color: var(--text);
  font-weight: 600;
  cursor: pointer;
}

/* --- Party match celebration (#960) --------------------------------------- */
/* Backdrop flash: quick dim-in to accent tint, then fade out. */
@keyframes party-match-backdrop-flash {
  0%   { background: transparent; }
  18%  { background: color-mix(in srgb, var(--accent) 22%, transparent); }
  100% { background: transparent; }
}
.party-match.celebrating {
  animation: party-match-backdrop-flash 0.55s ease-out both;
}
/* Poster: land from scale 0.6 with pickLand overshoot curve. */
@keyframes party-match-poster-enter {
  0%   { transform: scale(0.6); opacity: 0; }
  70%  { transform: scale(1.05); opacity: 1; }
  100% { transform: scale(1);    opacity: 1; }
}
.party-match.celebrating .party-match-poster {
  animation: party-match-poster-enter 0.6s cubic-bezier(0.22, 1.2, 0.36, 1) both;
}
/* Heading slam: scale 1.4 -> 1 with overshoot. */
@keyframes party-match-heading-slam {
  0%   { transform: scale(1.4); opacity: 0; }
  60%  { transform: scale(0.95); opacity: 1; }
  100% { transform: scale(1);    opacity: 1; }
}
.party-match.celebrating .party-match-heading {
  animation: party-match-heading-slam 0.3s cubic-bezier(0.22, 1.2, 0.36, 1) 0.15s both;
}
/* Gold shine sweep: reuses party-qr-shimmer technique as a ::after gradient.
   The base .party-match-poster already has position:relative + overflow:hidden
   so we just need the ::after pseudo-element. */
@keyframes party-match-shine {
  0%   { transform: translateX(-120%); }
  100% { transform: translateX(120%); }
}
.party-match.celebrating .party-match-poster::after {
  content: '';
  position: absolute;
  inset: 0;
  width: 60%;
  height: 100%;
  background: linear-gradient(
    100deg,
    transparent 0%,
    rgba(255, 255, 255, 0.35) 50%,
    transparent 100%
  );
  animation: party-match-shine 0.7s cubic-bezier(0.4, 0, 0.2, 1) 0.35s both;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .party-match.celebrating,
  .party-match.celebrating .party-match-poster,
  .party-match.celebrating .party-match-poster::after,
  .party-match.celebrating .party-match-heading { animation: none; }
}

/* --- Goal bar completion pulse (#960) ------------------------------------- */
@keyframes goal-bar-completion-pulse {
  0%   { transform: scaleX(1);    }
  40%  { transform: scaleX(1.04); }
  70%  { transform: scaleX(0.98); }
  100% { transform: scaleX(1);    }
}
.goal-bar-fill.goal-bar-pulse {
  transform-origin: left center;
  animation: goal-bar-completion-pulse 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@media (prefers-reduced-motion: reduce) {
  .goal-bar-fill.goal-bar-pulse { animation: none; }
}

.party-contenders {
  list-style: none;
  padding: 12px 16px;
  margin: 0;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg-card);
  text-align: left;
}
.party-contenders li {
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.party-contenders li:last-child { border-bottom: 0; }

@media (prefers-reduced-motion: reduce) {
  .party-swipe-card, .party-match-poster, .party-match-card { transition: none !important; }
}

/* --- My-parties sections (sub-issue e, #459) ----------------------------- */
.party-mine-section { margin-top: 24px; }
.party-mine-section h3 {
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  margin: 0 0 8px;
}
.party-mine-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.party-mine-row {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 4px;
  text-align: left;
  padding: 12px 14px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--bg-card);
  color: var(--text);
  cursor: pointer;
  font: inherit;
}
.party-mine-row:hover, .party-mine-row:focus-visible {
  border-color: var(--accent);
  outline: none;
}
.party-mine-row-title { font-size: 15px; font-weight: 600; }
.party-mine-row-meta {
  display: flex;
  gap: 6px;
  font-size: 13px;
  color: var(--text-dim);
}

/* Recent-match row variant (#new). The base .party-mine-row stacks
   title/meta vertically; the recent variant adds a poster thumbnail on
   the left so the user can see what they matched on at a glance. The
   active-party rows keep the existing column layout — no poster, no
   change. */
.party-mine-row-recent {
  flex-direction: row;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
}
.party-mine-row-poster {
  flex-shrink: 0;
  width: 44px;
  height: 66px;
  border-radius: 6px;
  background-color: var(--surface-2, var(--bg-card));
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  position: relative;
  /* The skeleton shimmer that runs while details are loading is
     conditional on the parent's .party-mine-row-recent-loading marker
     so a row with a real poster never shimmers. */
}
.party-mine-row-poster-fallback {
  font-size: 20px;
  font-weight: 700;
  color: var(--text-dim);
}
.party-mine-row-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1;
  min-width: 0;
}
/* Truncate long titles instead of wrapping under the poster — better
   than two lines of text floating to the right of a 66px box. */
.party-mine-row-recent .party-mine-row-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Loading skeleton — gentle shimmer on the poster + title slot while
   the per-item details fetch is in flight. Picked up by the loading
   class on the parent button. Reduced motion: respects the user's
   preference and falls back to a static dim fill. */
.party-mine-row-recent-loading .party-mine-row-poster {
  background-image: linear-gradient(
    90deg,
    var(--surface-2, var(--bg-card)) 0%,
    var(--bg-soft, var(--surface-3, var(--border))) 50%,
    var(--surface-2, var(--bg-card)) 100%
  );
  background-size: 200% 100%;
  animation: party-mine-row-shimmer 1.4s linear infinite;
}
.party-mine-row-recent-loading .party-mine-row-title::before {
  content: '';
  display: inline-block;
  width: 120px;
  height: 14px;
  border-radius: 4px;
  background-image: linear-gradient(
    90deg,
    var(--surface-2, var(--bg-card)) 0%,
    var(--bg-soft, var(--surface-3, var(--border))) 50%,
    var(--surface-2, var(--bg-card)) 100%
  );
  background-size: 200% 100%;
  animation: party-mine-row-shimmer 1.4s linear infinite;
}
@keyframes party-mine-row-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .party-mine-row-recent-loading .party-mine-row-poster,
  .party-mine-row-recent-loading .party-mine-row-title::before {
    animation: none;
  }
}
.party-mine-row-status {
  font-size: 12px;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* --- Party filters step (sub-issue #480) --------------------------------- */
.party-filter-type { display: flex; flex-direction: column; gap: 8px; }
.party-filter-segmented { display: flex; gap: 0; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.party-filter-segmented button {
  flex: 1;
  padding: 10px 12px;
  border: 0;
  background: var(--bg-card);
  color: var(--text);
  cursor: pointer;
  font: inherit;
}
.party-filter-segmented button:not(:last-child) { border-right: 1px solid var(--border); }
.party-filter-segmented button.active { background: var(--accent); color: white; }
.party-genre-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.party-genre-chip {
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 13px;
  cursor: pointer;
  font: inherit;
}
.party-genre-chip.selected {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}
.party-filter-hint { color: var(--text-dim); font-size: 12px; margin: 4px 0 0; }

/* --- Mobile vertical-fit for the party swipe flow ------------------------ */
/* Anchor the entire .party-swipe surface to the viewport so its children
   (header → stack → actions → hint) can never push past the bottom edge
   and bury the action buttons behind the swipe card. The previous version
   only capped .party-swipe-stack with a max-height calc, which fought
   .party-swipe-card's aspect-ratio: 2/3 + width: 100% chain — on tall
   viewports the card grew to its width-derived 1.5×width height and
   overflowed the stack, parking the Skip/Like buttons under it. */
@media (max-width: 600px) {
  .party-swipe {
    /* App-chrome budget: topbar+header (~60) + main padding-bottom
       (80 reserved for bottom tabs) + main padding-top (14) +
       #partyPanel padding (32) + safe-area insets. Anything left is
       split between header / stack / actions / hint by the flex
       layout below. */
    min-height: 0;
    gap: 8px;
    height: calc(
      100dvh - 60px - 80px - 14px - 32px - env(safe-area-inset-bottom) - env(safe-area-inset-top)
    );
  }
  .party-swipe-stack {
    /* Drop the max-height calc — the parent .party-swipe is now bounded,
       so flex: 1 fills whatever the header/actions/hint leave behind.
       min-height: 0 lets the stack shrink below content size on short
       viewports instead of pushing the actions off-screen. */
    flex: 1 1 0;
    min-height: 0;
    max-height: none;
    overflow: hidden;
  }
  .party-swipe-card {
    /* Card sizes from the bounded stack: height fills the stack, then
       aspect-ratio: 2/3 derives a narrower width. max-width caps it so
       the card doesn't blow up on landscape where the stack would
       otherwise be very tall. */
    width: auto;
    height: 100%;
    max-width: min(100%, 380px);
    max-height: 100%;
  }
}

/* --- Mobile narrow-screen tightening (sub-issue f, #460) ----------------- */
/* 320pt is the smallest viewport we support (older iPhone SE). The big
   room code and the swipe card both bumped against the edge on the
   narrow side; these rules shrink them gracefully without changing the
   default ≥361px layout. */
@media (max-width: 360px) {
  #partyPanel { padding: 12px; }
  .party-code { font-size: 30px; letter-spacing: 0.22em; }
  /* Trim the QR a hair on iPhone SE-class so it doesn't squeeze the
     code-block out of the available width. Still well above the 21mm
     minimum scannable size at typical phone DPRs. */
  .party-code-qr { width: 130px; height: 130px; padding: 6px; }
  .party-code-actions { gap: 6px; }
  .party-code-actions button { padding: 7px 10px; font-size: 12px; }
  .party-home-actions button { padding: 12px 14px; font-size: 15px; }
  /* Narrow-screen card cap — the 600px block above bounds the whole
     .party-swipe to viewport height and lets flex divide the remainder,
     so we only need to override the card's max-width here so it doesn't
     dominate on iPhone SE-class widths. min-height is intentionally
     left at 0 from the 600px block; the parent .party-swipe height
     calc already keeps the stack on-screen. */
  .party-swipe-card { max-width: 280px; }
  .party-swipe-btn { width: 56px; height: 56px; font-size: 24px; }
  .party-match-poster { width: 180px; }
  .party-match-heading { font-size: 24px; }
  .party-genre-chips { gap: 4px; }
  .party-genre-chip { padding: 5px 10px; font-size: 12px; }
}

/* --- External-service imports (Settings ▸ Data ▸ Import) ----------------- */
/* Card grid: 2-up on desktop, 1-up on mobile. Each card is button-shaped
   with an iconographic chip on the left. "Coming soon" cards reuse the
   layout but with reduced opacity and no chevron. */
.import-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin-top: 12px;
}
@media (max-width: 560px) {
  .import-grid { grid-template-columns: 1fr; }
}
.import-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, transform 0.05s;
  font: inherit;
}
.import-card:hover {
  background: var(--surface-3);
  border-color: var(--accent);
}
.import-card:active {
  transform: scale(0.99);
}
.import-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.import-card-disabled {
  cursor: default;
  opacity: 0.5;
}
.import-card-disabled:hover {
  background: var(--surface-2);
  border-color: var(--border);
}
.import-card-icon {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.5px;
  color: #fff;
  background: linear-gradient(135deg, #3b82f6, #6366f1);
}
.import-card-icon-letterboxd {
  background: #14181c;
  gap: 4px;
}
.import-card-icon-letterboxd span {
  width: 8px; height: 8px; border-radius: 50%;
  display: inline-block;
}
.import-card-icon-letterboxd span:nth-child(1) { background: #00c030; }
.import-card-icon-letterboxd span:nth-child(2) { background: #40bcf4; }
.import-card-icon-letterboxd span:nth-child(3) { background: #ff8000; }
.import-card-icon-goodreads {
  background: #6b4f33;
}
.import-card-icon-goodreads svg { color: #f5e8d3; }
.import-card-icon-imdb {
  background: #f5c518;
  color: #000;
  font-size: 12px;
  letter-spacing: 0;
}
.import-card-icon-steam {
  background: linear-gradient(135deg, #1b2838, #2a475e);
}
.import-card-icon-steam svg { color: #66c0f4; }
.import-card-icon-trakt {
  background: #ed1c24;
}
.import-card-icon-storygraph {
  background: linear-gradient(135deg, #4b3870, #7458a6);
}
.import-card-icon-tvtime {
  background: #1a3b8c;
  color: #fff;
  font-size: 12px;
  letter-spacing: 0;
}
.import-card-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.import-card-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.import-card-desc {
  font-size: 12px;
  color: var(--text-dim);
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.import-card-chevron {
  flex: 0 0 auto;
  color: var(--text-dim);
  transition: transform 0.15s, color 0.15s;
}
.import-card:hover .import-card-chevron {
  color: var(--accent);
  transform: translateX(2px);
}

/* Imports modal: fixed-width, scrolls within itself on small viewports.
   Source-specific panes (CSV vs Steam) toggle via the [hidden] attribute
   from src/ui/imports.js. */
/* Sits above the onboarding wizard when triggered from inside the
   wizard's Import step — both modals default to z-index 200 and the
   wizard appears first in the DOM. */
#importsModal { z-index: 220; }
.imports-modal {
  max-width: 560px;
  width: min(560px, 100%);
  padding: 0;
  display: flex;
  flex-direction: column;
  max-height: 90vh;
}
.imports-modal-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 18px 22px 8px;
}
.imports-modal-header h2 {
  flex: 1;
  margin: 0;
  font-size: 18px;
  font-weight: 600;
}
.imports-modal-close {
  background: transparent;
  border: 0;
  color: var(--text-dim);
  padding: 4px;
  border-radius: 6px;
  cursor: pointer;
  display: inline-flex;
}
.imports-modal-close:hover {
  background: var(--surface-2);
  color: var(--text);
}
.imports-help {
  padding: 0 22px;
  margin: 0 0 12px;
  font-size: 13px;
  color: var(--text-dim);
  line-height: 1.5;
}
.imports-pane {
  padding: 0 22px 4px;
  overflow-y: auto;
  flex: 1 1 auto;
}
.imports-field-label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 14px 0 6px;
}
.imports-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 18px;
  border: 1.5px dashed var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-dim);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
  text-align: center;
}
.imports-dropzone:hover,
.imports-dropzone.drag {
  border-color: var(--accent);
  background: var(--surface-3);
  color: var(--text);
}
.imports-dropzone svg { color: currentColor; }
.imports-dropzone-hint { font-size: 13px; }
.imports-dropzone-file {
  font-size: 12px;
  font-weight: 500;
  color: var(--text);
  word-break: break-all;
}
.imports-textarea {
  width: 100%;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
  resize: vertical;
  min-height: 80px;
}
.imports-textarea:focus {
  outline: none;
  border-color: var(--accent);
}
.imports-input {
  width: 100%;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
}
.imports-input:focus { outline: none; border-color: var(--accent); }
.imports-steam-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}
.imports-steam-row .imports-input { flex: 1 1 auto; }
.imports-steam-row button {
  flex: 0 0 auto;
  padding: 10px 16px;
  border-radius: 8px;
  border: 1px solid var(--accent);
  background: var(--accent);
  color: #fff;
  font: inherit;
  font-weight: 500;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.15s;
}
.imports-steam-row button:hover:not(:disabled) { background: var(--accent-hover); }
.imports-steam-row button:disabled { opacity: 0.5; cursor: not-allowed; }
.imports-hint {
  font-size: 12px;
  color: var(--text-dim);
  margin: 8px 0 0;
}
.imports-preview {
  margin-top: 12px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 12px;
  color: var(--text-dim);
}
.imports-preview-head {
  font-weight: 600;
  color: var(--text);
  margin-bottom: 4px;
  font-size: 13px;
}
.imports-preview-list {
  line-height: 1.5;
}
.imports-preview-hint {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px dashed var(--border);
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.45;
}
.imports-error {
  padding: 0 22px;
  margin: 8px 0 0;
  font-size: 13px;
  color: #f87171;
}
.imports-progress {
  padding: 22px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.imports-progress-title {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
}
.imports-progress-bar {
  width: 100%;
  height: 8px;
  background: var(--surface-2);
  border-radius: 999px;
  overflow: hidden;
}
.imports-progress-bar span {
  display: block;
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--accent), #6366f1);
  transition: width 0.2s ease;
}
.imports-progress-label {
  margin: 0;
  font-size: 12px;
  color: var(--text-dim);
}
.imports-result {
  padding: 16px 22px 0;
}
.imports-result-title {
  margin: 0 0 10px;
  font-size: 16px;
  font-weight: 600;
}
.imports-result-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.imports-result-row {
  padding: 8px 12px;
  border-radius: 8px;
  font-size: 13px;
  background: var(--surface-2);
  border-left: 3px solid transparent;
}
.imports-result-row.imports-result-imported {
  border-left-color: #22c55e;
}
.imports-result-row.imports-result-already {
  border-left-color: #60a5fa;
}
.imports-result-row.imports-result-skipped {
  border-left-color: #fbbf24;
}
.imports-result-row.imports-result-failed {
  border-left-color: #f87171;
}
.imports-modal-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  padding: 14px 22px 18px;
  border-top: 1px solid var(--border);
  margin-top: 8px;
}
.imports-modal-actions button {
  padding: 10px 18px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  font-weight: 500;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
  font-family: inherit;
}
.imports-modal-actions button:hover:not(:disabled) {
  background: var(--surface-3);
  border-color: var(--accent);
}
.imports-modal-actions button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
@media (max-width: 560px) {
  .imports-modal {
    width: 100%;
    max-height: 100vh;
    border-radius: 14px 14px 0 0;
    align-self: flex-end;
  }
  .imports-modal-header,
  .imports-help,
  .imports-pane,
  .imports-progress,
  .imports-result,
  .imports-error,
  .imports-modal-actions {
    padding-left: 16px;
    padding-right: 16px;
  }
  .imports-steam-row { flex-direction: column; }
}


/* ============================================================
 * #561 — In-app person sheet
 * Stacks on top of #movieModal when a cast card is tapped. Shows the
 * actor's headshot + role in this title + biography + filmography grid.
 * z-index sits above .modal-overlay (200) so the sheet covers the
 * underlying movie modal.
 * ============================================================ */
#personSheet { z-index: 220; }
.person-sheet-modal {
  max-width: 720px;
}
.person-sheet-header {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 20px;
  padding: 28px 24px 8px;
}
.person-photo {
  width: 132px;
  aspect-ratio: 2 / 3;
  border-radius: 14px;
  background-size: cover;
  background-position: center top;
  background-color: var(--surface-2);
  border: 1px solid var(--border);
  position: relative;
  flex-shrink: 0;
}
.person-photo.no-image::after {
  content: '👤';
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 56px; opacity: 0.45;
}
.person-meta {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
}
.person-name {
  font-size: 24px;
  font-weight: 700;
  line-height: 1.2;
  margin: 0 0 4px;
  color: var(--text);
}
.person-character {
  font-size: 13px;
  color: var(--text-dim);
  font-style: italic;
  margin-bottom: 10px;
}
.person-meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}
.person-meta-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 10px;
  font-size: 11px;
  color: var(--text-dim);
  white-space: nowrap;
}
.person-meta-chip-muted { color: var(--text-muted); }
.person-bio {
  padding: 4px 24px 8px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-dim);
}
.person-bio p {
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 6;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.person-bio.expanded p {
  -webkit-line-clamp: unset;
  overflow: visible;
  display: block;
}
.person-bio-toggle {
  background: transparent;
  border: none;
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  padding: 6px 0 0;
}
.person-bio-toggle:hover { text-decoration: underline; }
.person-filmography {
  padding: 16px 24px 8px;
}
.person-filmography h3 {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  margin: 0 0 12px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.person-filmography h3 .h3-meta {
  color: var(--text-muted);
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
  font-size: 11px;
}
.person-filmography-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 14px 10px;
}
.person-filmography-empty {
  color: var(--text-dim);
  font-size: 13px;
  margin: 0;
}
.person-credit-card {
  display: flex;
  flex-direction: column;
  text-align: left;
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  color: inherit;
  min-height: 44px;
  transition: transform 0.15s;
}
.person-credit-card:hover { transform: translateY(-2px); }
.person-credit-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 8px;
}
.person-credit-poster {
  width: 100%;
  aspect-ratio: 2 / 3;
  border-radius: 8px;
  background-size: cover;
  background-position: center;
  background-color: var(--surface-2);
  border: 1px solid var(--border);
  margin-bottom: 6px;
  position: relative;
  overflow: hidden;
}
.person-credit-card:hover .person-credit-poster {
  border-color: var(--accent);
}
.person-credit-title {
  font-size: 12px;
  font-weight: 600;
  line-height: 1.25;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--text);
}
.person-credit-year {
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
.person-sheet-footer {
  display: flex;
  justify-content: center;
  padding: 12px 24px 28px;
}
.person-tmdb-link {
  color: var(--text-dim);
  font-size: 12px;
  text-decoration: none;
  padding: 8px 14px;
  border-radius: 8px;
  border: 1px solid var(--border);
  transition: border-color 0.15s, color 0.15s;
}
.person-tmdb-link:hover {
  color: var(--text);
  border-color: var(--accent);
}

@media (max-width: 600px) {
  .person-sheet-header {
    grid-template-columns: 96px 1fr;
    gap: 14px;
    padding: 20px 16px 8px;
  }
  .person-photo { width: 96px; }
  .person-name { font-size: 20px; }
  .person-bio { padding: 4px 16px 8px; }
  .person-filmography { padding: 14px 16px 8px; }
  .person-filmography-grid {
    grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
    gap: 12px 8px;
  }
  .person-sheet-footer { padding: 12px 16px 24px; }
}

/* #797 Tier A removed the Custom Lists client entry points (Add-to-list,
   the My Lists segment, the in-modal hint chip). The watchlist filter
   sheet's Lists section still exists in the DOM but is part of the
   data/filter layer scheduled for removal in Tier B/C; keep it hidden so
   the deprecated filter UI stays off until then. */
#watchlistListFilterSection {
  display: none !important;
}

/* View Transitions (#975) -- assign stable names to chrome elements so they
   stay pinned (no crossfade) while panels swap during tab transitions.
   The root crossfade handles the panel content; header + tab bar hold in
   place because they carry their own view-transition-name tokens.

   NOTE: ::view-transition-* pseudo-elements are NOT reached by the global
   prefers-reduced-motion block above (which targets * / *::before / *::after
   only). Motion is gated in JS by withViewTransition() which checks
   matchMedia('(prefers-reduced-motion)') before calling
   document.startViewTransition at all. No CSS ::view-transition overrides
   are needed for reduced-motion here. */
/* #978: scope to the single top-level app header; a bare `header` selector
   matched the settings-group/imports headers too, and duplicate
   view-transition-name values make every transition throw. */
body > header {
  view-transition-name: app-header;
}
/* #992: the tab bar has NO view-transition-name for general transitions.
   Pinning it (app-tabs, #975) made the fixed bar participate in VT snapshots,
   racing the keyboard hide/restore transform (tab-bar-keyboard.js restores via
   setTimeout(0), which lands AFTER the new-state capture) and leaving the bar
   invisible on real iOS after a tab switch. Unpinned, the live bar renders
   normally throughout the transition.
   #996: the one exception is a tab-switch crossfade. During activateTab() the
   html.vt-tab-switch class is present (added synchronously before the
   transition starts, removed on vt.finished). In that window the keyboard is
   definitionally closed (a tab switch is user navigation, so no editable can
   legitimately hold focus) and the bar's transform has already been cleared by
   activateTab() itself, so the #992 race cannot occur. Pinning the bar as
   app-tabs here lifts it out of the root snapshot crossfade: its near-identical
   old/new snapshots hold it visually steady instead of double-exposing it
   through the page fade, and its group paints above the root snapshot (which
   also reinforces #995's cards-under-bar guarantee). The name is present only
   while the class is on <html> and disappears again once the transition
   settles. */
html.vt-tab-switch .tabs {
  view-transition-name: app-tabs;
}
/* #995: suppress per-card VT names during tab-switch crossfades.
   cards.js (#975) stamps view-transition-name: card-<id> via element.style.setProperty
   (normal priority) on the first 20 .movie-card elements. Those lifted
   pseudo-elements always paint ABOVE the root snapshot group, which means they
   paint over the un-named fixed tab bar for the ~250ms crossfade duration and
   cause the bar to flicker. The fix: while activateTab() is running a tab-switch
   transition it adds .vt-tab-switch to <html>; this !important declaration wins
   over the inline style (spec: !important stylesheet > normal inline) and forces
   all card names to none so they stay in the root layer. The class is removed
   when vt.finished resolves, so per-card names continue to work for the
   filter-apply FLIP (#975) and poster morph (#987). Also prevents the unintended
   cross-panel card morph when the same item id appears in both the outgoing and
   incoming grids. */
html.vt-tab-switch .movie-card {
  view-transition-name: none !important;
}

/* #987: Shared-element poster morph (grid card to detail modal).
   The tapped card's .poster-img and the modal hero img both carry
   view-transition-name: detail-poster during the transition, set by JS.
   These rules shape the morph animation:
   - image-pair: isolation:auto lets the browser scale+position both images
     to interpolate between the card thumbnail and the modal hero size.
   - old/new: object-fit:cover keeps the poster art correctly cropped at
     every intermediate size (avoids letterboxing during the morph).
   - 350ms ease-out matches the modal slide-in feel and reads as instant on
     most devices without being rushed on older hardware.
   Motion is always gated in JS (withViewTransition checks reduced-motion +
   VT feature-detect) so no CSS reduced-motion override is needed here. */
::view-transition-image-pair(detail-poster) {
  isolation: auto;
}
::view-transition-old(detail-poster),
::view-transition-new(detail-poster) {
  animation-duration: 350ms;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  object-fit: cover;
  overflow: clip;
  border-radius: 8px;
}
/* #1026: card->modal poster morph (#987) layering fix. While the morph runs,
   <html> carries .vt-poster-morph (added synchronously before
   startViewTransition in openMovie, removed on vt.finished). Mirror of #995's
   tab-switch suppression: force every grid card's view-transition-name (the
   card-<id> tokens from #975) AND app-header to none so the ONLY named element
   captured during the morph is the poster img (detail-poster). Without this the
   lifted card/header groups paint above the root snapshot in capture order, so
   the tapped card holds in the grid as a stray leftover poster and cards
   captured after it paint OVER the morphing poster (it travels behind the grid),
   the exact #1026 symptoms. The rule targets the .movie-card wrapper (whose name
   is card-<id>), NOT its .poster-img child, so the morph SOURCE's detail-poster
   name survives. !important wins over the normal-priority inline style cards.js
   sets (spec: !important stylesheet > normal inline), same mechanism #995 uses. */
html.vt-poster-morph .movie-card { view-transition-name: none !important; }
html.vt-poster-morph body > header { view-transition-name: none !important; }
