
* { 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);
  --font-body: -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);
  --font-body: Georgia, 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', 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);
  --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);
  --topbar-bg: rgba(255, 255, 255, 0.94);
  /* Badges/pills sit on the darkened accent colors above — white text reads
     reliably. (Default in :root is --bg, which would be light cream here.) */
  --on-accent: #FFFFFF;
  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);
  --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);
  --topbar-bg: rgba(255, 255, 255, 0.94);
  --on-accent: #FFFFFF;
  color-scheme: light;
}

/* 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 {
  overflow-x: hidden;
  max-width: 100vw;
}
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);
}
.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; }
.region-picker {
  display: flex; align-items: center; gap: 6px;
  height: 32px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0 10px 0 12px;
  box-sizing: border-box;
  position: relative;
}
.region-picker:hover { border-color: var(--accent); }
/* Mobile flag-only display (#162). Hidden on desktop — the native select
   shows "🇺🇸 United States" inline. On narrow viewports the country name
   is dropped in favour of just the flag and this overlay span renders it
   while the underlying select goes opacity:0 (still tappable, still opens
   the native dropdown which keeps full names per option). */
.region-flag-display { display: none; 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);
}
.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 {
  background: none; border: none;
  padding: 0;
  height: 100%;
  font-size: 13px;
  cursor: pointer;
}
/* 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. Replaces the prior avatar+chevron / sign-in-pill twin. */
.header-avatar {
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-dim);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  border: 2px solid var(--border);
  overflow: hidden;
  position: relative;
  box-sizing: border-box;
  transition: transform 0.12s, border-color 0.12s, color 0.12s;
}
.header-avatar:hover {
  transform: scale(1.05);
  border-color: var(--accent);
  color: var(--accent);
}
.header-avatar[aria-expanded="true"] {
  border-color: var(--accent);
  color: var(--accent);
}
/* Signed-in state (#187): fill with accent + white silhouette so auth state
   reads at a glance. Toggled in renderHeaderAvatar() via .signed-in. The
   hover/expanded overrides keep the icon white (the default :hover sets
   color to accent — on an accent background that disappears). */
.header-avatar.signed-in {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
.header-avatar.signed-in:hover,
.header-avatar.signed-in[aria-expanded="true"] {
  filter: brightness(1.08);
  color: #fff;
  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%;
  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: 0 12px 32px rgba(0, 0, 0, 0.18);
  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; }
.tab-count {
  background: var(--surface-3); padding: 2px 8px;
  font-size: 11px; border-radius: 999px;
  color: var(--text-dim);
}
.tab.active .tab-count { background: var(--accent); color: white; }

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;
}
/* 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-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: white;
}

.movie-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 20px;
}

/* 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: 0 6px 16px rgba(0, 0, 0, 0.28);
  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: 0 12px 28px rgba(0, 0, 0, 0.4);
}
/* 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, 0 6px 16px rgba(0, 0, 0, 0.28);
}
.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, 0 6px 16px rgba(0, 0, 0, 0.28);
}
.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, 0 6px 16px rgba(0, 0, 0, 0.28);
}
.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, 0 6px 16px rgba(0, 0, 0, 0.28);
}
.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;
}
/* 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; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 48px; 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.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;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: calc(50% - 4px);
  -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. */
.movie-card.has-quick-add .movie-card-info { padding-bottom: 44px; }
/* #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; }
  .poster-quick-add-btn { font-size: 10.5px; padding: 6px 7px; }
  .movie-card.has-quick-add .movie-card-info { padding-bottom: 38px; }
}
/* Pick-for-me modal "Already watched" inline action (#259). Sits between
   Roll-again and Watch-it; lower visual weight than Watch-it (the primary
   path on the pick stage) but available as a one-tap "actually I've seen
   this — pick something else" without hopping through the modal. */
.pick-already-watched {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
}
.pick-already-watched:hover { background: var(--surface-3); }

.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; }
/* 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: '🌐';
  font-size: 11px;
  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 { font-size: 9px; 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; }

/* Hover-preview trailer (#88). Iframe is inserted right after the .poster-img
   so the badges (corner status, tv progress, streaming) keep painting over
   it; .movie-card-info has z-index: 1 and stays on top regardless.
   pointer-events: none is load-bearing — without it the YouTube iframe
   captures the cursor and mouseleave never fires on the card, so teardown
   never runs. Black background fills the 2:3 frame while the 16:9 video
   letterboxes (covers the poster underneath during the brief load gap). */
.hover-trailer-iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0;
  background: #000;
  pointer-events: none;
  opacity: 0;
  animation: hover-trailer-fade-in 200ms ease forwards;
}
@keyframes hover-trailer-fade-in {
  to { 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: 18px; 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); }

.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;
  transition: opacity 0.2s;
}
.modal-overlay.visible { opacity: 1; pointer-events: auto; }
.modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 16px;
  max-width: 800px; width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  position: relative;
  display: flex;
  flex-direction: column;
}
.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: 0 4px 16px rgba(0,0,0,0.5);
  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); }

.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;
}

/* Tinder-style swipe deck (For You → Swipe mode) */
.swipe-stack {
  position: relative;
  width: 100%;
  max-width: 380px;
  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: 0 16px 36px rgba(0,0,0,0.5);
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s;
  will-change: transform, opacity;
  touch-action: none;
}
.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;
}
.swipe-card.behind {
  transform: scale(0.95) translateY(8px);
  filter: brightness(0.85);
  pointer-events: 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: #ef4444; border-color: #ef4444; }
.swipe-btn.watchlist { color: var(--accent); border-color: var(--accent); }
.swipe-btn.watched { color: var(--warning); border-color: var(--warning); }
.swipe-btn:hover { transform: scale(1.1); box-shadow: 0 4px 16px rgba(255,77,109,0.2); }
.swipe-btn:active { transform: scale(0.92); }
.swipe-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.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: white;
  border-radius: 8px;
  font-weight: 500;
  border: none;
  cursor: pointer;
}

@media (max-width: 600px) {
  /* Lock the swipe surface to fit comfortably in the viewport so the user
     never has to scroll while interacting with cards. The math: subtract the
     sticky topbar (~50), tabs (~50), For You toggle row (~52), filters (~58),
     actions (~80), hint (~30), breathing room (~30), the position:fixed bottom
     tabs bar (~50) and its safe-area padding (#333). The earlier ~350px figure
     forgot the bottom-tabs bar, so on iPhones with a home-indicator the swipe
     stack pushed actions+hint into the tabs bar's z-index and clipped them. */
  .swipe-stack {
    aspect-ratio: 2/3;
    height: min(540px, calc(100dvh - 400px - env(safe-area-inset-bottom)));
    width: auto;
    max-width: min(380px, calc((100dvh - 400px - env(safe-area-inset-bottom)) * 2 / 3));
    min-height: 320px;
  }
  .swipe-card-title { font-size: 19px; }
  .swipe-card-info { padding: 22px 18px 18px; }
  .swipe-actions { gap: 16px; margin-top: 14px; }
  .swipe-btn { width: 54px; height: 54px; font-size: 22px; }
  .swipe-hint { margin-top: 10px; font-size: 11px; }
}

/* 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;
  padding-bottom: 6px;
}
.cast-strip::-webkit-scrollbar { display: none; }
.cast-card {
  flex: 0 0 90px;
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: transform 0.15s;
}
.cast-card:hover { transform: translateY(-2px); }
.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;
}
.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); }
.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;
}
.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); }
.modal-body {
  padding: 24px;
  margin-top: -120px;
  position: relative; z-index: 2;
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: 24px;
}
.modal-poster {
  aspect-ratio: 2/3;
  border-radius: 12px;
  background-size: cover;
  background-color: var(--surface-2);
  box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.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); }
.modal-overview {
  font-size: 14px; line-height: 1.6;
  color: var(--text-dim);
  margin-bottom: 20px;
}

.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; }
.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); }
.modal-actions button.primary { background: var(--accent); color: white; border-color: var(--accent); }
.modal-actions button.primary:hover { background: var(--accent-hover); }

/* 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-footer {
  flex-shrink: 0;
  padding: 14px 24px;
  border-top: 1px solid var(--border);
  background: var(--surface);
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 10px;
}
.settings-modal-footer button.secondary {
  padding: 10px 18px;
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text);
  font-weight: 500;
  font-size: 14px;
  border: 1px solid var(--border);
  transition: background 0.15s;
}
.settings-modal-footer button.secondary:hover { background: var(--surface-3); }
.settings-modal-footer button.primary {
  padding: 10px 22px;
  border-radius: 10px;
  background: var(--accent);
  color: white;
  font-weight: 600;
  font-size: 14px;
  border: none;
  transition: background 0.15s;
}
.settings-modal-footer button.primary:hover { background: var(--accent-hover); }
.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-section {
  border-top: 1px solid var(--border);
  padding-top: 20px;
  margin-top: 20px;
}
.settings-section:first-of-type {
  border-top: none;
  padding-top: 0;
  margin-top: 0;
}
.settings-section h3 {
  font-size: 14px;
  font-weight: 700;
  margin-bottom: 4px;
}
.settings-section p.section-desc {
  color: var(--text-dim);
  font-size: 13px;
  margin-bottom: 14px;
}
.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: white;
  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;
}
@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;
}
/* The HTML `hidden` attribute should always win over class display rules. */
[hidden] { display: none !important; }

.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: 0 10px 30px rgba(0,0,0,0.5);
  opacity: 0;
  transition: transform 0.3s, opacity 0.3s;
}
.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;
}
.episodes-section h3 {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 0 0 8px;
  font-size: 16px;
}
.episodes-section .episodes-progress {
  font-size: 13px;
  color: var(--text-dim);
  font-weight: 500;
}
.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;
}
.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); }
}
.rate-star.active { color: var(--warning); }
.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 { background: var(--accent); color: white; border-color: var(--accent); }
.rate-actions button.primary:hover { background: var(--accent-hover); }
.rate-actions button.primary:disabled { opacity: 0.5; cursor: not-allowed; background: var(--surface-3); border-color: var(--border); }
#rateModal { z-index: 250; }

/* 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, #d33);
}
.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); }
.party-name-prompt-actions button.primary {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
}
.party-name-prompt-actions button.primary:hover { background: var(--accent-hover); }

/* 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: white;
  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: white;
}
.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; }
}

/* 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: white;
  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: white;
  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;
  }
  .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. */
  .modal-overlay.is-bottom-sheet > .modal.settings-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;
    }
  }
}

/* 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 .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: white;
  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: white;
  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: white;
}

/* 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. */
.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;
}
.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);
}
.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: 0 12px 32px rgba(0, 0, 0, 0.22);
}
.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 menu shrinks but still shows the metric + direction. The popup
     anchors to the right edge so it doesn't clip past the viewport when
     the toggle sits at the far right of a wrapping row. */
  .tab-toolbar .sort-menu-toggle {
    height: 32px;
    padding: 0 6px 0 10px;
    font-size: 12px;
    max-width: 150px;
  }
  /* 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; }
}
/* Watchlist on mobile keeps a 3-segment toggle when Upcoming is available
   (#260) — segmented can climb to ~165px. Shrink the sort menu's toggle
   on this surface so [segmented] [+Filter] [sort] fits one row at 360–390
   px (the dir-toggle is gone now — see 1.7.31 — so the row only has
   three pieces to pack). */
@media (max-width: 720px) {
  #watchlistPanel .tab-toolbar .sort-menu-toggle { max-width: 130px; }
}
/* Narrow-screen tightening: ≤430px (iPhone 14/15 standard + below).
   With the asc/desc toggle gone (1.7.31) the filter-pill keeps its
   "Filter" word at every viewport. The remaining squeeze is on the
   Watchlist 3-segment toggle: trim its button padding + font so the
   row [segmented] [+Filter] [sort] still fits one line on iPhone-class
   widths even with Upcoming visible. The sort dropdown shows a compact
   "Date added (↓)" via the dir glyph — no localized word in the trigger
   means it stays narrow in every locale. */
@media (max-width: 430px) {
  #watchlistPanel .tab-toolbar .sort-menu-toggle {
    max-width: 96px;
    padding: 0 4px 0 8px;
    font-size: 11px;
    gap: 4px;
  }
  #watchedPanel .tab-toolbar .sort-menu-toggle {
    max-width: 140px;
    padding: 0 6px 0 10px;
    font-size: 11.5px;
  }
  #watchlistPanel .tab-toolbar .watchlist-mode-toggle button {
    padding: 0 4px;
    font-size: 10.5px;
    min-height: 28px;
  }
  #watchlistPanel .tab-toolbar .filter-pill {
    padding: 5px 8px;
    font-size: 11.5px;
  }
  #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 Lists) 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; }
#watchlistPanel:not([data-mode="lists"]) .lists-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: white;
  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: white;
  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-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);
}
.filter-sheet-footer button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}
.filter-sheet-footer button.primary:hover {
  background: var(--accent-hover);
}
.discover-filter-sheet { display: flex; flex-direction: column; padding: 0; overflow: hidden; }

/* Mobile-only sheet sections (e.g. sort moves into sheet on mobile only). */
.filter-sheet-section.mobile-only-section { display: none; }
@media (max-width: 720px) {
  .filter-sheet-section.mobile-only-section { display: block; }
}

/* 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);
}

/* 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;
}
.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);
  font-size: 18px;
}
.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: white;
}
.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 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 {
  background: var(--accent);
  color: #fff;
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 10px 18px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s;
}
.wizard-actions .primary:hover { background: var(--accent-hover); }
.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);
}

/* Welcome hero — replaces the older flat .wizard-brand text. A soft
   radial accent glow sits behind the wordmark; together they make the
   first frame feel less like a form and more like a real app intro. */
.wizard-hero {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 18px auto 14px;
  height: 90px;
}
.wizard-hero-glow {
  position: absolute;
  inset: 0;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 38%, transparent), transparent 70%);
  filter: blur(6px);
  pointer-events: none;
}
.wizard-hero-logo {
  position: relative;
  font-size: 40px;
  font-weight: 700;
  letter-spacing: -0.02em;
  background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 60%, #fff));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.wizard-welcome-bullets {
  list-style: none;
  margin: 6px 0 26px;
  padding: 0;
  display: flex;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
}
.wizard-welcome-bullets li {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-size: 12.5px;
  color: var(--text);
}
.wizard-bullet-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}
.wizard-bullet-watch { background: #f59e0b; }
.wizard-bullet-read  { background: #34d399; }
.wizard-bullet-play  { background: #60a5fa; }

/* 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;
    max-height: 100vh;
    border-radius: 0;
    padding: 24px 20px 20px;
  }
  /* Cancel the centering so the modal really fills the viewport on mobile. */
  .modal-overlay#onboardingWizard { padding: 0; }
}

/* ===== 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;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 14px 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  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);
}
.settings-group-nav-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.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; }
.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: block;
}
/* 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. */
details.settings-section-advanced {
  border-top: 1px solid var(--border);
  padding-top: 14px;
  margin-top: 24px;
}
details.settings-section-advanced > summary {
  font-size: 13px;
  font-weight: 700;
  color: var(--text-dim);
  cursor: pointer;
  padding: 6px 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: 6px; }
details.settings-section-advanced .settings-section.api-section {
  border-top: none;
  padding-top: 8px;
  margin-top: 0;
}
/* #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. */
@media (min-width: 721px) {
  .settings-groups {
    display: grid;
    grid-template-columns: 200px 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; }
  /* 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 {
  content: '📖';
}
.movie-card[data-type="game"] .movie-poster.no-image::before {
  content: '🎮';
}

/* 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;
}

.modal-poster.book-no-cover {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
}
.modal-poster.book-no-cover::after {
  content: '📖';
  font-size: 72px;
  opacity: 0.28;
}
body.cat-games .modal-poster.book-no-cover::after { content: '🎮'; }

/* 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, the Availability section is moot — hide the section header +
   segmented. The + Filter pill stays visible (#240) because the sheet now
   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; }

/* 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 */
.danger-section h3 { color: #f87171; }

.app-version {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  font-weight: 500;
  color: var(--text-dim);
  margin-left: 4px;
}

.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: 0 10px 30px rgba(0,0,0,0.5);
  max-width: calc(100vw - 32px);
}
.update-banner > span { flex: 1 1 auto; min-width: 0; }
.update-banner-btn {
  padding: 6px 12px;
  background: var(--accent);
  color: #fff;
  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: rgba(239, 68, 68, 0.12);
  border: 1px solid rgba(239, 68, 68, 0.4);
  color: #ef4444;
  border-radius: 8px;
  font-weight: 500;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.danger-btn:hover {
  background: rgba(239, 68, 68, 0.22);
  border-color: #ef4444;
}

.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;
}
.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);
}

/* Account: delete-button row sits below the signed-in info block */
.account-delete-row {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px dashed var(--border);
}

/* 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;
}
#profileSaveBtn:disabled {
  opacity: 0.55; cursor: not-allowed;
}
.profile-save-status {
  font-size: 12px;
}
.profile-save-status.ok { color: #22c55e; }
.profile-save-status.err { color: #f87171; }

.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: #f87171; 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: #f87171;
}
.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; }

.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 + dedicated Friends modal. Lives next to
   the avatar slot in the topbar (signed-in only) so the social graph has a
   visible, one-tap entry point instead of being buried in Settings. */
.header-friends-btn {
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-dim);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  border: 2px solid var(--border);
  box-sizing: border-box;
  margin-right: 6px;
  padding: 0;
  transition: transform 0.12s, border-color 0.12s, color 0.12s, background 0.12s;
}
.header-friends-btn:hover {
  transform: scale(1.05);
  border-color: var(--accent);
  color: var(--accent);
}
.header-friends-btn[hidden] { display: none; }
@media (max-width: 600px) {
  .header-friends-btn { width: 28px; height: 28px; margin-right: 4px; }
  .header-friends-btn svg { width: 18px; height: 18px; }
}
@media (max-width: 380px) {
  .header-friends-btn { width: 26px; height: 26px; border-width: 1.5px; }
  .header-friends-btn 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: #f87171; }
.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); }

/* 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; }
.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);
}

.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;
}
.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;
}

/* Mobile: expand the modal to fill the viewport so the content gets
   real estate (was 24–32px side padding on a narrow modal — felt
   cramped). The close button is a flex sibling of .profile-modal
   inside #profileModal; on row-flex layouts it eats ~56px of width,
   so we lift it out of the flex flow with position:absolute. */
@media (max-width: 560px) {
  #profileModal { padding: 0; align-items: stretch; }
  .profile-modal {
    width: 100%;
    max-width: 100%;
    max-height: 100vh;
    height: 100vh;
    border-radius: 0;
    border: 0;
    /* iOS standalone PWA: the viewport extends under the status bar
       (notch / time / carrier / battery), so the modal's top edge sits
       behind it. Inset the top padding by safe-area-inset-top — 0 in
       non-PWA browsers, ~47–59px on notched iPhones in standalone. */
    padding: calc(18px + env(safe-area-inset-top)) 16px 16px;
    display: flex;
    flex-direction: column;
  }
  #profileModal #profileClose {
    position: absolute;
    top: calc(12px + env(safe-area-inset-top));
    right: 12px;
    margin: 0;
    align-self: auto;
  }
  .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 .report-bug-link { margin-left: 14px; }

/* 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: rgba(239, 68, 68, 0.12);
  border-color: rgba(239, 68, 68, 0.4);
  color: #ef4444;
}
.delete-account-modal .modal-actions button.danger-btn:hover {
  background: rgba(239, 68, 68, 0.22);
  border-color: #ef4444;
}
.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);
}
/* Override the destructive-red title coloring for non-destructive modals
   (Create list reuses the layout but isn't a danger surface). */
#listCreateModal .delete-account-modal h2 { color: var(--text); }

/* Per-item "Add to list" picker (#175). Compact modal with a scrollable
   stack of checkbox rows, an inline "+ New list" expander at the bottom,
   and a single Done action. Reuses tokens from delete-account-modal so the
   picker matches the rest of the lists surface (#174). */
.list-picker-modal {
  max-width: 460px;
  padding: 22px 26px 18px;
}
.list-picker-modal h2 {
  margin: 0 0 4px;
  font-size: 18px;
  font-weight: 600;
  color: var(--text);
}
.list-picker-subtitle {
  margin: 0 0 14px;
  font-size: 12.5px;
  color: var(--text-dim);
  /* Single-line item title — long titles ellipsize rather than reflowing the
     header into 3 lines. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.list-picker-subtitle:empty { display: none; margin: 0; }
.list-picker-empty {
  margin: 0 0 12px;
  padding: 14px 16px;
  background: var(--surface-2);
  border: 1px dashed var(--border);
  border-radius: 10px;
  font-size: 13px;
  color: var(--text-dim);
  text-align: center;
}
.list-picker-rows {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 50vh;
  overflow-y: auto;
  margin: 0 -6px;
  padding: 0 6px;
}
.list-picker-rows:empty { display: none; }
.list-picker-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.list-picker-row:hover { background: var(--surface-3); border-color: var(--accent); }
.list-picker-row.checked { border-color: var(--accent); }
.list-picker-row input[type="checkbox"] {
  width: 18px; height: 18px;
  accent-color: var(--accent);
  cursor: pointer;
  flex-shrink: 0;
}
.list-picker-row-name {
  flex: 1; min-width: 0;
  font-size: 14px;
  color: var(--text);
  word-break: break-word;
}
.list-picker-row-count {
  font-size: 12px;
  color: var(--text-dim);
  flex-shrink: 0;
}
.list-picker-create {
  margin-top: 10px;
}
.list-picker-create-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 12px;
  background: transparent;
  border: 1px dashed var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.list-picker-create-toggle:hover {
  background: var(--surface-2);
  border-color: var(--accent);
  color: var(--accent);
}
.list-picker-create-toggle svg { stroke: currentColor; }
.list-picker-create-form {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 10px;
}
.list-picker-create-form input[type="text"] {
  width: 100%;
  padding: 9px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
  box-sizing: border-box;
}
.list-picker-create-form input[type="text"]:focus {
  outline: none; border-color: var(--accent);
}
.list-picker-create-form-actions {
  display: flex; gap: 8px; justify-content: flex-end;
}
.list-picker-create-form-actions button {
  padding: 8px 14px;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
}
.list-picker-create-form-actions button:hover { background: var(--surface-3); }
.list-picker-create-form-actions button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}
.list-picker-create-form-actions button.primary:hover { background: var(--accent-hover); }
.list-picker-create-form-actions button:disabled {
  opacity: 0.5; cursor: not-allowed;
}
.list-picker-modal .modal-actions {
  display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;
}
.list-picker-modal .modal-actions button {
  padding: 9px 18px;
  border-radius: 8px;
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--accent);
  background: var(--accent);
  color: white;
}
.list-picker-modal .modal-actions button:hover { background: var(--accent-hover); }

/* Per-item "Added to N lists" hint (#175). Small chip rendered in the modal
   meta line so users see at a glance that the item is on one of their lists.
   Uses the bookmark accent so it visually links to the picker affordance. */
.modal-lists-hint {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 12px;
  color: var(--accent);
  /* Click-through to the picker — handled by data-act="add-to-list" wiring. */
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
}
.modal-lists-hint:hover { text-decoration: underline; }

/* #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. Custom-list picker is still reachable via the
   detail modal `i` button. */

@media (max-width: 720px) {
  .list-picker-modal { padding: 20px 18px 16px; }
  .list-picker-row { padding: 11px 12px; }
  .list-picker-row input[type="checkbox"] { width: 20px; height: 20px; }
  .list-picker-rows { max-height: 55vh; }
}

/* Watchlist mode sub-segment (#260, hoisted into .tab-toolbar in #271).
   The toggle still uses .watchlist-mode-toggle so JS can target it; the
   wrapping .watchlist-mode-row was retired with the unified toolbar
   refactor. Three buttons (All items / My lists / Upcoming) — Upcoming
   was folded in from the retired inner Grid|Upcoming row. */
.watchlist-mode-toggle { display: inline-flex; }

/* Mobile (≤720px): tighten button padding on the 3-segment mode toggle
   so the Filter pill stays on the same row to its right at iPhone-class
   widths. Without this, "Minhas listas" / "Le mie liste" / "Meine Listen"
   push the toggle past the 320pt available row and the pill wraps below.
   The 8px horizontal padding still leaves the 44pt touch target intact
   via the inherited :is(button) min-height. */
@media (max-width: 720px) {
  .watchlist-mode-toggle button {
    padding: 6px 9px;
    font-size: 12.5px;
  }
}

/* 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);
}
@media (max-width: 720px) {
  .list-detail-title { font-size: 19px; }
  .list-detail-title-input { font-size: 19px; }
  .list-detail-header { margin: 0 0 10px; }
}

/* 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: 0 18px 48px rgba(0, 0, 0, 0.55);
  transition: transform 0.18s ease, filter 0.18s ease;
}
.pick-poster img { width: 100%; height: 100%; object-fit: cover; display: block; }
.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; gap: 10px;
  width: 100%;
  max-width: 360px;
}
.pick-actions button {
  flex: 1 1 0;
  padding: 12px 14px;
  border-radius: 10px;
  font-size: 14.5px; 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: 6px;
}
.pick-actions button:hover { background: var(--surface-3); }
.pick-actions .pick-watch-it {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}
.pick-actions .pick-watch-it:hover { filter: brightness(1.08); background: var(--accent); }
.pick-actions button svg { width: 16px; height: 16px; }

.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; }
}

/* 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.on::after { transform: translateX(18px); }

@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;
  }
  main { padding: 14px; }

  /* Mobile region picker: collapse to a flag-only chip (#162). The native
     <select> sits opacity:0 over the flag span — taps still open the
     OS-rendered dropdown which keeps full "🇺🇸 United States" labels. */
  .region-picker {
    height: 30px;
    padding: 0;
    width: 38px;
    min-width: 38px;
    justify-content: center;
  }
  .region-picker .region-globe { display: none; }
  .region-flag-display {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    font-size: 16px;
  }
  .region-picker select {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    padding: 0;
    cursor: pointer;
  }
  /* 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;
  }
  .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; }
  .header-avatar-icon { width: 16px; height: 16px; }

  /* 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.
     Background is fully opaque + theme-aware (#223): iOS Safari can drop
     backdrop-filter mid-scroll, and the previous hard-coded rgba(20,20,31,0.96)
     was only a partial backdrop AND only matched the Watch-dark theme — so on
     Read/Play/Light Watch the nav also rendered the wrong tint. */
  .tabs {
    position: fixed;
    bottom: 0; left: 0; right: 0;
    background: var(--bg);
    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 */
  .tab.active svg.tab-icon { stroke-width: 2.2; }
  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%;
  }
  /* On mobile, the count is a small floating badge anchored to the tab icon
     instead of taking horizontal space alongside the label. This frees up
     room for longer labels like "Reading list" without truncation. */
  .tab { position: relative; }
  .tab-count {
    position: absolute;
    top: 4px;
    left: calc(50% + 8px);
    background: var(--accent);
    color: white;
    font-size: 9px;
    font-weight: 700;
    padding: 0 4px;
    border-radius: 999px;
    line-height: 14px;
    height: 14px;
    min-width: 14px;
    text-align: center;
    box-sizing: border-box;
  }
  .tab.active .tab-count { background: white; color: var(--accent); }
  /* 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: 14px;
    padding: 16px;
    margin-top: -70px;
    align-items: start;
  }
  .modal-poster {
    width: 110px;
    margin: 0;
    grid-column: 1;
    grid-row: 1 / span 2;
  }
  .modal-info h2 {
    font-size: 18px;
    line-height: 1.25;
    margin-bottom: 6px;
    text-align: left;
  }
  .modal-meta {
    font-size: 12px;
    margin-bottom: 0;
    gap: 4px;
    line-height: 1.4;
    align-self: start;
  }
  .modal-overview {
    font-size: 13px;
    line-height: 1.55;
    margin-top: 14px;
    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;
  }
  .modal-info h2,
  .modal-meta {
    grid-column: 2;
  }
  .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;
  }
}

/* ===== 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: absolute;
  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;
}
.stat-headline-big {
  font-size: 22px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.35;
}
.stat-headline-sub {
  margin-top: 8px;
  font-size: 14px;
  color: var(--text-dim);
}
@media (max-width: 520px) {
  .stat-headline-big { font-size: 18px; }
}

.stat-section {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 18px 18px 16px;
}
.stat-section-title {
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  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(--accent); }
.stat-hours-seg.seg-tv    { background: var(--accent); filter: brightness(0.78); }
.stat-hours-seg.seg-book  { background: var(--accent); filter: brightness(1.18) saturate(0.85); }
.stat-hours-seg.seg-game  { background: var(--accent); filter: brightness(0.6) saturate(1.2); }
.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(--accent); }
.stat-hours-swatch.seg-tv    { background: var(--accent); filter: brightness(0.78); }
.stat-hours-swatch.seg-book  { background: var(--accent); filter: brightness(1.18) saturate(0.85); }
.stat-hours-swatch.seg-game  { background: var(--accent); filter: brightness(0.6) saturate(1.2); }
.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;
}

/* 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); }
.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; }

/* ===== 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: var(--accent-text, #fff);
  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;
}
.party-home-actions button.primary:not(.party-home-cta) {
  background: var(--accent);
  color: var(--accent-text, #fff);
  border-color: var(--accent);
}

/* 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-emoji { font-size: 32px; line-height: 1; }
.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.primary { background: var(--accent); color: var(--accent-text, #fff); border-color: var(--accent); }
.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, #c0392b); 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 (#new). Server-rendered SVG from /api/qr;
   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. */
.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 img {
  display: block;
  width: 100%;
  height: 100%;
}
.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: var(--accent-text, #fff); 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.primary { background: var(--accent); color: var(--accent-text, #fff); border-color: var(--accent); }
.party-lobby-footer button.danger { color: var(--danger, #c0392b); 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;
}
.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));
}
.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, #c0392b); color: var(--danger, #c0392b); }
.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-actions button.primary {
  background: var(--accent);
  color: var(--accent-text, #fff);
  border-color: var(--accent);
}

.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;
  /* 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: var(--accent-text, #fff); }
.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: var(--accent-text, #fff);
}
.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-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-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.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.imports-modal-actions button.primary:hover:not(:disabled) {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}
.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; }
}
