/* ============================================================================
   CineClub — three complete design systems (A24 / Festival Print / The Strokes)
   The base below is theme-agnostic structure; each [data-theme] block is a full
   redesign: layout, shapes, type, the wheel framing — not just colours.
   Default values in :root mirror the A24 theme.
   ========================================================================== */
:root {
  --bg: #ffffff; --bg-soft: #f4f4f4; --panel: #ffffff; --panel-2: #f4f4f4;
  --line: #111111; --line-strong: #000000; --text: #0a0a0a; --muted: #6b6b6b;
  --faint: #9a9a9a; --accent: #0a0a0a; --accent-hi: #000000; --accent-ink: #ffffff;
  --danger: #c0241c; --star-empty: #d6d6d6; --topbar-bg: rgba(255, 255, 255, 0.95);
  --radius: 0px; --maxw: 880px;
  --serif: "Archivo", system-ui, sans-serif;
  --sans: "Archivo", system-ui, sans-serif;
}

/* Tells the browser which palette to paint its OWN chrome in — select-box
   arrows, date-input calendar icons, checkbox/radio fallback rendering,
   scrollbars — none of which styles.css otherwise touches. Without this the
   app's own [data-mode] dark toggle was invisible to the browser, so that
   chrome stayed light-themed (a black arrow on a dark dropdown) even with
   dark mode on, since dark mode here is an app choice, not necessarily the
   OS's own colour-scheme preference. */
:root { color-scheme: light; }
[data-mode="dark"] { color-scheme: dark; }

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg); color: var(--text); font-family: var(--sans);
  font-size: 16px; line-height: 1.6; min-height: 100vh; overflow-x: clip; -webkit-font-smoothing: antialiased;
}
h1, h2, h3 { font-family: var(--serif); font-weight: 700; line-height: 1.18; letter-spacing: 0.2px; }
a { color: var(--accent); text-underline-offset: 2px; }
.hidden { display: none !important; }
.muted { color: var(--muted); }
.small { font-size: 0.85rem; }
.center { text-align: center; }
.error { color: var(--danger); min-height: 1.2em; }
/* Backend-trouble banner (over free quota / unreachable / App Check block). */
.conn-banner { margin-bottom: 18px; padding: 14px 18px; border: 1px solid var(--danger); border-radius: var(--radius); background: var(--panel); color: var(--text); text-align: center; font-size: 0.95rem; line-height: 1.5; }
/* Landing footer (support link + legal links) */
.site-footer { margin-top: 64px; padding-bottom: 24px; }
.site-footer p { margin: 6px 0; }
/* Install-to-home-screen hint (mobile only, dismissable) */
.install-hint { position: relative; margin: 20px 0 0; padding: 11px 40px 11px 14px; background: var(--bg-soft); border: 1px solid var(--line); border-radius: var(--radius); font-size: 0.85rem; line-height: 1.5; text-align: left; }
.install-dismiss { position: absolute; top: 50%; right: 10px; transform: translateY(-50%); background: none; border: none; cursor: pointer; font-size: 1.15rem; color: var(--muted); padding: 4px 6px; line-height: 1; }
.install-dismiss:hover { color: var(--text); }
/* Accent border (not .install-hint's neutral one) — this one's actionable,
   not just informational, so it should read as a step worth taking. */
.update-banner { position: relative; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 18px; padding: 11px 40px 11px 14px; background: var(--bg-soft); border: 1px solid var(--accent); border-radius: var(--radius); font-size: 0.9rem; }
.update-banner .btn { width: auto; }
/* Privacy / Terms pages */
.legal { max-width: 760px; margin: 0 auto 20px; }
.legal h1 { margin-top: 0; }
.legal h2 { margin-top: 24px; font-size: 1.15rem; }
.legal p, .legal li { line-height: 1.6; }
.legal ul { padding-left: 20px; }
.legal li { margin: 5px 0; }
.action-form input { margin: 0 0 14px; }
.action-form .btn { margin-bottom: 10px; }
.legal-nav { margin-top: 18px; }
/* FAQ page — each question is a closed-by-default <details>, same underlying
   pattern as .lb-help/.acct-paste elsewhere in the app, just boxed and sized
   for standalone reading rather than a folded-away inline aside. */
.faq-group { margin-top: 28px; }
.faq-group:first-of-type { margin-top: 8px; }
.faq-group h2 { margin-bottom: 10px; }
.faq-item { border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; margin: 0 0 10px; }
.faq-item > summary { cursor: pointer; font-weight: 600; list-style: none; }
.faq-item > summary::-webkit-details-marker { display: none; }
.faq-item > summary::before { content: "+"; display: inline-block; width: 1em; color: var(--accent); }
.faq-item[open] > summary::before { content: "\2013"; }
.faq-item[open] > summary { margin-bottom: 8px; }
.faq-item p, .faq-item ul, .faq-item ol { margin: 6px 0 0; line-height: 1.6; }
.faq-item p:first-child { margin-top: 0; }
.faq-item li { margin: 4px 0; }
.faq-item code { border: 1px solid var(--line); background: var(--panel-2); padding: 0 4px; border-radius: var(--radius); }

/* ---- topbar ---- */
.topbar {
  display: flex; align-items: center; gap: 14px;
  padding: 16px 22px; border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 50; background: var(--topbar-bg); backdrop-filter: blur(8px);
}
.brand { font-family: var(--serif); font-weight: 800; font-size: 1.35rem; letter-spacing: 0.3px; }
.topbar-group { display: flex; align-items: center; gap: 10px; }
.group-name { font-weight: 600; }
/* Long personal names shouldn't be able to push the always-visible row (brand,
   club identity, this, Menu) into overflowing — same idea as .group-name's own
   cap below, just fixed rather than viewport-relative since there's much less
   slack left in this row now that everything else lives behind Menu. */
#who-am-i { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The utility chips (Account, Notifications, Busy, Theme, Dark, Sound, Update,
   Feedback, Clubs) live behind Menu at every width, not just on mobile — with
   this many of them, "all inline, always" simply doesn't fit a real desktop
   window, let alone a resized or half-screen one. Menu itself sits inline
   (pushed to the far right); this panel is what it opens. */
.menu-btn { display: inline-flex; margin-left: auto; }
.topbar-right {
  position: absolute; top: 100%; right: 14px; margin: 0; gap: 6px;
  display: none; flex-direction: column; align-items: stretch;
  min-width: 172px; max-height: 72vh; overflow-y: auto; padding: 8px; z-index: 60;
  background: var(--panel); border: 1px solid var(--line-strong);
  box-shadow: 0 16px 44px rgba(0, 0, 0, 0.3);
}
.topbar-right.open { display: flex; }
.topbar-right .chip { width: 100%; text-align: left; }
.chip {
  background: transparent; color: var(--text); border: 1px solid var(--line-strong);
  border-radius: var(--radius); padding: 7px 13px; font-size: 0.82rem; font-family: var(--sans);
  line-height: 1.2; cursor: pointer; transition: border-color 0.15s, color 0.15s;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.code-chip { display: inline-flex; gap: 8px; align-items: center; }
.mute-chip { display: inline-flex; align-items: center; gap: 8px; }
.mute-row { display: flex; align-items: center; gap: 6px; }
/* Overrides .topbar-right .chip's width:100% (same specificity, but this loses
   on source order alone) — inside .mute-row the chip shares its line with the
   info bubble rather than claiming the whole row for itself.
   Vertical padding reduced from the base chip's 7px: this chip's content is
   whichever is taller, its own text or the 22px toggle switch beside it — the
   switch wins, making it a few pixels taller inside than every other chip's
   plain text. Only really visible on desktop, where chips sit inline next to
   each other in a row (stacked full-width on mobile, so a slightly taller one
   isn't sitting right beside a shorter one to compare against). Trimming the
   padding here brings the whole button back to the same outer height as its
   neighbours instead of standing taller. */
#mute-btn.mute-chip { width: auto; flex: 1 1 auto; min-width: 0; padding-top: 4px; padding-bottom: 4px; }
/* --star-empty (not --line-strong) for the off state: in the default theme
   --accent and --line-strong are both near-black, so the two switch states
   were indistinguishable — --star-empty is already a per-theme/per-mode
   "unfilled" grey that's guaranteed distinct from --accent (same idea as an
   empty star rating). */
.sound-switch { position: relative; width: 38px; height: 22px; border-radius: 999px; background: var(--star-empty); flex-shrink: 0; transition: background 0.15s; }
.sound-switch-knob { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.15s; }
.mute-chip[aria-checked="true"] .sound-switch { background: var(--accent); }
.mute-chip[aria-checked="true"] .sound-switch-knob { transform: translateX(16px); }
#group-code { font-weight: 700; letter-spacing: 0.22em; }
.copy-label { color: var(--muted); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; }

/* ---- layout ---- */
/* --main-pad is #main's own side padding, published as a variable so the tab
   slider can cancel it and re-apply it per pane — see #tab-slider. */
:root { --main-pad: 20px; }
#main { max-width: var(--maxw); margin: 0 auto; padding: 40px var(--main-pad) 80px; }
.screen { animation: fade 0.3s ease; }
@keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 24px; margin-bottom: 20px; }
/* Collapsible page sections — a <details> wearing .card, so it keeps every
   theme's card look; the padding moves from the card itself onto the summary
   (header row) and body separately, so a collapsed section is just the header.
   scroll-margin-top: any of these can be a scrollIntoView target (e.g. tapping
   the streak chip jumps to the Streaks card on the Stats tab) — without it,
   its top edge lands at the very top of the viewport, hidden behind the
   sticky topbar+tabs stacked above it. --topbar-h/--tabs-h are measured in
   app.js (syncTopbarHeight). */
.card-collapse { padding: 0; scroll-margin-top: calc(var(--topbar-h, 62px) + var(--tabs-h, 46px)); }
.card-collapse > summary { list-style: none; cursor: pointer; padding: 24px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.card-collapse > summary::-webkit-details-marker { display: none; }
.card-collapse > summary h3 { margin: 0; min-width: 0; }
.card-collapse-chevron { color: var(--text); font-size: 1.7rem; flex-shrink: 0; transition: transform 0.15s; }
.card-collapse[open] > summary .card-collapse-chevron { transform: rotate(90deg); }
.card-collapse-body { padding: 0 24px 24px; }

/* ---- inputs & buttons ---- */
input, textarea {
  width: 100%; background: var(--bg-soft); color: var(--text); border: 1px solid var(--line-strong);
  border-radius: var(--radius); padding: 12px 14px; font-size: 1rem; font-family: var(--sans);
}
input::placeholder, textarea::placeholder { color: var(--faint); }
input:focus, textarea:focus { outline: none; border-color: var(--accent); }
textarea { resize: vertical; min-height: 70px; }
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  background: transparent; color: var(--text); border: 1px solid var(--line-strong);
  border-radius: var(--radius); padding: 11px 20px; font-size: 0.92rem; font-weight: 600;
  font-family: var(--sans); cursor: pointer; letter-spacing: 0.02em;
  transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.06s;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.btn.primary:hover { background: var(--accent-hi); border-color: var(--accent-hi); color: var(--accent-ink); }
.btn.big { font-size: 1.05rem; padding: 16px 44px; text-transform: uppercase; letter-spacing: 0.16em; }
.btn.small { padding: 8px 14px; font-size: 0.82rem; }
.link-btn { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 0.95rem; padding: 10px 8px; min-height: 44px; display: inline-flex; align-items: center; }
.link-btn:hover { color: var(--danger); }
.text-link { background: none; border: none; color: var(--muted); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; font-size: 0.82rem; font-family: var(--sans); }
.text-link:hover { color: var(--accent); }

/* ---- accessibility ---- */
.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; }
/* a clear, consistent keyboard focus ring everywhere */
a:focus-visible, input:focus-visible, textarea:focus-visible,
.btn:focus-visible, .chip:focus-visible, .tab:focus-visible,
.link-btn:focus-visible, .text-link:focus-visible, .theme-opt:focus-visible,
.start-btn:focus-visible, .half:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* Honour users who ask for less motion. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}

/* ---- landing ---- */
.hero { text-align: center; margin: 24px 0 40px; }
.hero h1 { font-size: 2.6rem; margin: 0 0 14px; }
.lead { color: var(--muted); max-width: 540px; margin: 0 auto; font-size: 1.05rem; }
.lead + .lead { margin-top: 10px; }
.actions-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.panel h2 { margin-top: 0; font-size: 1.3rem; }
.panel input { margin-bottom: 14px; }
.code-input { text-transform: uppercase; letter-spacing: 0.3em; font-weight: 700; text-align: center; font-size: 1.2rem; }
/* "Already someone" line under Create / Join — quieter than the two panels,
   but present, so a second browser isn't funnelled into becoming a stranger. */
.landing-signin { margin: 16px auto 0; max-width: 460px; line-height: 1.6; }
.setup-list { line-height: 2; color: var(--muted); padding-left: 20px; }
.setup-list code, p code { background: var(--bg-soft); padding: 2px 6px; border-radius: 3px; font-size: 0.88em; color: var(--text); }
@media (max-width: 560px) { .actions-grid { grid-template-columns: 1fr; } .hero h1 { font-size: 2rem; } }

/* ---- film-strip picker (alternative to the wheel) ----
   IMPORTANT: .strip-frame's width (104px) and the 8px gap are mirrored by
   FRAME_W / FRAME_GAP in js/strip.js, which computes the travel distance from
   them. Change one and you must change the other, or the reel settles
   off-centre. They deliberately do NOT shrink on small screens for that reason
   — the viewport narrows instead. */
.film-strip {
  background: #14141a; border-radius: 4px; padding: 0; overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.14);
}
/* Sprocket holes, drawn with a repeating gradient rather than elements. */
.strip-perfs {
  height: 13px;
  background-image: repeating-linear-gradient(to right,
    transparent 0 6px, rgba(255, 255, 255, 0.82) 6px 16px, transparent 16px 28px);
  background-size: 28px 7px;
  background-position: center;
  background-repeat: repeat-x;
}
.strip-track { display: flex; gap: 8px; padding: 6px; align-items: center; will-change: transform; }
.strip-track.idle { overflow-x: auto; scrollbar-width: thin; }
.strip-frame {
  flex: 0 0 104px; width: 104px; height: 156px; position: relative;
  background: #23232c; border-radius: 2px; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.strip-poster { width: 100%; height: 100%; object-fit: cover; display: block; }
/* No artwork (TMDB off, or an obscure film) — a readable title card instead. */
.strip-titlecard {
  font-family: var(--sans); font-size: 0.7rem; font-weight: 700; line-height: 1.25;
  color: #f2f2f5; text-align: center; padding: 8px; overflow-wrap: anywhere;
  text-transform: uppercase; letter-spacing: 0.04em;
}
.strip-empty { padding: 26px 14px; text-align: center; }
.idle-strip { width: 100%; max-width: 100%; }

/* The running reel + its gate. */
.strip-viewport { position: relative; overflow: hidden; width: min(86vw, 620px); }
.strip-viewport .strip-track { flex-wrap: nowrap; }
.strip-gate {
  position: absolute; top: 0; bottom: 0; left: 50%; transform: translateX(-50%);
  width: 110px; border: 2px solid var(--accent); border-radius: 3px;
  box-shadow: 0 0 0 2000px rgba(10, 10, 13, 0.45); pointer-events: none;
}
.strip-frame.winner.landed { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Idle frames open the film's details. */
.strip-frame.tappable { cursor: pointer; }
.strip-frame.tappable:hover { filter: brightness(1.12); }
.strip-frame.tappable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.strip-overlay .spin-caption { margin-top: 26px; }

@media (max-width: 560px) {
  /* Narrow the window, never the frames — see the note above. */
  .strip-viewport { width: 92vw; }
}

/* ---- landing walkthrough ("see how it works") ----
   The demo fragments deliberately reuse the app's own classes (.deadline-pill,
   .round-progress, .sealed-banner, .rating-line, .stars-display…), so they
   inherit all three themes and dark mode for free. Only the scaffolding below
   is new. */
.howto-toggle-row { margin: 22px 0 0; }
#howto-toggle { font-size: 0.9rem; }
.howto { margin: 22px 0 4px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); padding: 26px 24px; }
.howto-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 26px; }
.howto-step { display: flex; gap: 16px; align-items: flex-start; }
.howto-n {
  flex: none; width: 30px; height: 30px; border-radius: 999px; display: flex;
  align-items: center; justify-content: center; font-weight: 700; font-size: 0.85rem;
  background: var(--accent); color: var(--accent-ink);
}
.howto-body { flex: 1; min-width: 0; }
.howto-body h3 { margin: 2px 0 6px; font-size: 1.05rem; }
.howto-body p { margin: 0; }
.howto-demo { margin-top: 12px; padding: 14px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel-2); }
.howto-centre { text-align: center; }
.howto-centre .round-progress { margin-bottom: 0; }
.howto-films { display: flex; flex-direction: column; gap: 8px; }
.howto-film { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.howto-wheel-wrap { display: flex; flex-direction: column; align-items: center; }
.howto-picker-note { margin: 8px 0 0; }
/* renderIdleWheel() sets an inline width (460px) via setupHiDPI, so the cap has
   to be max-width — a stylesheet `width` would just lose to the inline one. */
.howto-wheel { max-width: min(260px, 68vw); height: auto; }
.howto-sealed-note { margin: 10px 0 0; }
.howto-reviews { display: flex; flex-direction: column; gap: 14px; }
.howto-foot { margin: 24px 0 0; padding-top: 18px; border-top: 1px solid var(--line); }
@media (max-width: 560px) {
  .howto { padding: 20px 16px; }
  .howto-step { gap: 12px; }
}

/* ---- tabs ---- */
/* Tabs are the app's main navigation, so they stay put as you scroll and read
   as a proper bar rather than quiet text. --topbar-h is measured in app.js:
   the topbar is sticky and its height changes as it wraps, so a fixed offset
   would either overlap it or float below it. z-index sits under the topbar's 50
   so the two layer correctly. */
.tabs {
  display: flex; gap: 4px;
  width: 100vw; margin-left: calc(50% - 50vw); margin-bottom: 28px;
  /* Cancels #main's own top padding (40px here, 24px in the mobile media
     query below — the two have to move together). Without this, the tabs
     bar sits that far below the topbar until scrolling far enough to trigger
     its sticky position, at which point it snaps flush — a blank strip that
     visibly closes as you scroll, rather than the tabs just living right
     under the topbar from the start like they're meant to. */
  margin-top: -40px;
  padding-inline: max(20px, calc((100vw - var(--maxw)) / 2 + 20px));
  border-bottom: 2px solid var(--line);
  position: sticky; top: var(--topbar-h, 62px); z-index: 40;
  background: var(--bg);
  /* scrollIntoView targets this row; without a margin it'd be scrolled to the
     very top of the viewport, i.e. underneath the sticky header. */
  scroll-margin-top: var(--topbar-h, 62px);
}
/* The negative margin above assumes .tabs is the very first thing under the
   topbar — true almost always, but not when any banner ahead of it is showing:
   #conn-banner/#update-banner sit before #screen-app itself, and
   #join-requests-banner/#reset-banner/#admin-claim-banner/#busy-banner/
   #account-nudge sit inside it as its own earlier siblings. Either way, pulling .tabs up by that
   same amount would yank it into whichever banner is visible instead of just
   closing the gap under the topbar. Cancel it back to 0 in that case — the
   small gap under a banner is normal and doesn't need closing the way the gap
   under the bare topbar does. Higher specificity than both the base and
   mobile-media-query rules above (an extra ID selector each), so this wins
   regardless of viewport width without needing its own copy inside that
   media query. */
#conn-banner:not(.hidden) ~ #screen-app .tabs,
#update-banner:not(.hidden) ~ #screen-app .tabs,
#join-requests-banner:not(.hidden) ~ .tabs,
#reset-banner:not(.hidden) ~ .tabs,
#account-nudge:not(.hidden) ~ .tabs,
#admin-claim-banner:not(.hidden) ~ .tabs,
#busy-banner:not(.hidden) ~ .tabs {
  margin-top: 0;
}
.tab {
  flex: 1 1 0; min-width: 0;
  background: none; border: none; color: var(--muted); padding: 14px 18px; cursor: pointer;
  font-size: 1rem; font-weight: 700; font-family: var(--sans); letter-spacing: 0.04em;
  border-bottom: 3px solid transparent; margin-bottom: -2px;
  transition: color 0.15s, border-color 0.15s;
}
.tab:hover { color: var(--text); }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }

/* #tab-slider is the fixed clipping window (never itself transformed);
   #tab-track is the row that actually slides inside it (never itself
   clipped). Normally both inert block wrappers — a swipe (wireTabSwipe in
   app.js) adds .dragging for the duration of the gesture, which lays
   whichever of the panes aren't .hidden (the current tab, plus whichever
   neighbour it's being dragged toward) out side by side so the live drag
   can slide between them; align-items: stretch keeps the row a single
   consistent height (the taller of the two) rather than jumping as one
   pane's natural height is swapped for another's mid-drag. Removed again
   the moment the gesture ends, whichever way it resolved. */
/* wireTabSwipe also pins an inline height on #tab-slider for the duration, so
   turning on the side-by-side layout below (which makes the row as tall as the
   TALLER of the two panes on show) can't resize the page mid-gesture. */
/* The slider runs edge to edge, cancelling #main's side padding, and each pane
   carries that padding itself instead. At rest this looks identical (content
   still sits --main-pad from the screen edge), but during a swipe it's the
   difference between a page and a cut-out strip: content now slides all the
   way off the screen edge, instead of stopping --main-pad short of it and
   leaving a fixed band of background sitting there down each side while
   everything moved past it. That band was the "white bar at the side". It also
   means the space between two panes mid-swipe is just each page's own margin —
   the outgoing one's and the incoming one's — so their cards never touch,
   without an artificial gap being invented to hold them apart. */
#tab-slider { margin-inline: calc(-1 * var(--main-pad)); }
.tabpane { padding-inline: var(--main-pad); }
#tab-slider.dragging { overflow: hidden; }
/* will-change gets the row its own compositor layer for the drag, so following
   the finger is a cheap re-composite instead of repainting a whole tab's worth
   of content on every touchmove. Only while .dragging — it costs memory, so
   it's not worth holding permanently for a gesture that's mostly not happening. */
#tab-slider.dragging > #tab-track { display: flex; align-items: stretch; will-change: transform; }
/* :not(.hidden) matters: without it this would out-specificity .hidden's own
   display:none (id + 2 classes beats 1 class) and force ALL FOUR panes
   visible, not just whichever one(s) the gesture actually revealed. */
#tab-slider.dragging > #tab-track > .tabpane:not(.hidden) { flex: 0 0 100%; min-width: 0; display: block; }
/* The settle animation after you let go of a swipe. wireTabSwipe() overrides
   the duration inline per gesture (distance left to travel over the speed you
   released at, the way a native pager does it), so the 260ms here is only the
   fallback. The curve is the iOS sheet/pager one; its opening slope
   (0.72/0.32 = 2.25) is the SETTLE_SLOPE the duration maths is scaled by, so
   the animation starts off at about the speed your finger was already going
   instead of visibly restarting. Change one, change the other. */
#tab-track.settling { transition: transform 260ms cubic-bezier(0.32, 0.72, 0, 1); }

/* Club switcher on the landing screen. */
.saved-clubs-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.saved-club-row { display: flex; align-items: stretch; min-width: 0; border: 1px solid var(--line); border-radius: var(--radius); }
.saved-club-row:hover { border-color: var(--accent); }
.saved-club { flex: 1; min-width: 0; padding: 12px 14px; text-align: left; color: var(--text); background: transparent; border: none; cursor: pointer; font: inherit; }
.saved-club-row:hover .saved-club { color: var(--accent); }
.saved-club span, .saved-club small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.saved-club span { font-weight: 700; }
.saved-club small { color: var(--muted); font-size: 0.75rem; letter-spacing: 0.12em; }
.saved-club-forget { flex-shrink: 0; background: none; border: none; border-left: 1px solid var(--line); color: var(--muted); cursor: pointer; padding: 0 12px; font-size: 0.8rem; }
.saved-club-forget:hover { color: var(--text); background: var(--panel-2); }

/* ---- film card ---- */
.film-card { text-align: center; padding: 32px 24px; }
.film-banner { font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.18em; color: var(--accent); }
.film-title { font-size: 2.1rem; margin: 14px 0 10px; }
/* Only when it's the card's last element (the "no film picked, nothing to
   click" case, no button underneath) — matches the space above it to
   .film-card's own bottom padding, so the gap above and below the text reads
   the same. Left alone (:last-child doesn't match) whenever a button follows
   (Add films / Nudge), where the browser-default paragraph margin already
   gives the button its usual spacing. */
.no-film-subtext:last-child { margin: 32px 0 0; }
[data-theme="a24"] .no-film-subtext:last-child { margin-top: 36px; }
/* "picked by / added by" now sits with the film's own identity (title, year,
   genre) rather than leading the deadline/scheduling group below — it reads
   as film trivia, not as the start of "when to watch it". */
.film-meta { display: flex; gap: 20px; justify-content: center; flex-wrap: wrap; color: var(--muted); font-size: 0.9rem; margin-top: 10px; }
.film-meta b { color: var(--text); font-weight: 600; }
/* First element of #film-round — the divider marking "streaming info above,
   deadline/scheduling below" moved here from .film-meta once picked-by/
   added-by relocated to sit with the film's own identity instead. */
.deadline-row { display: flex; gap: 12px; align-items: center; justify-content: center; margin: 20px 0; flex-wrap: wrap; padding-top: 20px; border-top: 1px solid var(--line); }
.film-card .filter-help-text { text-align: center; margin-bottom: 20px; }
.deadline-pill { border: 1px solid var(--accent); color: var(--accent); font-weight: 600; padding: 5px 14px; border-radius: 999px; font-size: 0.85rem; letter-spacing: 0.04em; }
.deadline-edit { margin: 12px auto; max-width: 320px; display: flex; gap: 10px; align-items: center; justify-content: center; }
.deadline-edit input { width: auto; }
/* Label above the input rather than beside it — same fix as the movie-night
   date/time field: an empty date input has no placeholder text of its own on
   some browsers, and squeezed next to a long label it read as an unlabelled
   blank box. flex-wrap is needed on the row for flex-basis: 100% to actually
   push the label onto its own line — .deadline-edit itself doesn't wrap
   (shared with other, single-line uses), so it's added here, scoped. */
.busy-modal-date-pick { flex-wrap: wrap; }
.busy-modal-date-label { flex-basis: 100%; }
/* iOS Safari renders an unset <input type="date"> with no visible text at
   all (no "dd/mm/yyyy", nothing) — unlike desktop browsers, which show one —
   so the box above read as genuinely blank there, not just squeezed. This
   input has no default value by design (no date is pre-selected), so the
   label-position fix above can't help; the field needs its own always-visible
   hint text. `required` makes an empty date input match `:invalid` in every
   browser that matters here, so the hint can hide itself the moment a real
   date's chosen without any JS — it's cosmetic only, nothing ever submits
   this as a form. The hint fully covers the input's own content box (same
   background, so it also papers over whatever browsers DO render for the
   empty state) and is `pointer-events: none`, so every tap still lands on the
   real input underneath and opens the native picker. */
.date-input-wrap { position: relative; display: inline-block; }
.date-input-hint {
  position: absolute; inset: 0; display: flex; align-items: center;
  padding: 0 14px; border-radius: var(--radius); background: var(--bg-soft);
  color: var(--faint); font-size: 1rem; font-family: var(--sans); pointer-events: none;
}
.date-input-wrap input:valid + .date-input-hint { display: none; }

/* The propose-it prompt (nothing set yet) is a plain line, no box — see the
   comment in movieNightHtml() for why. Its form (revealed on tap) DOES get
   the same bordered-box treatment as .movie-night below, once there's an
   actual date/time field and button in it to frame — a bare, unframed
   datetime-local input just read as a stray grey box. */
/* display:flex (rather than the plain block + text-align it looks like it
   only needs): a block container inherits the page's own line-height for its
   inline content's "strut", and since that's taller than the button's own
   text, the leftover space used to collect above the button rather than
   splitting evenly around it (buttons align to baseline, not middle). Flex
   lays the button out directly, with no line box and no strut, removing that.
   Margins are NOT equal on purpose, and paired with .wheel-status's own
   line-height below: buttons get a tight "normal" line-height from the
   browser by default, but a plain <p> like the auto-spin countdown doesn't,
   so it was carrying noticeably more built-in padding above the line than
   the button carries below it even once the strut above was fixed. Matched
   .wheel-status to the same "normal" line-height so both sides are built the
   same way, then closed the last couple of pixels with a slightly bigger
   bottom margin — checked pixel-for-pixel against a real render, not just
   eyeballed, since this exact spot has been wrong before. */
.mn-prompt { margin: 16px 0 21px; display: flex; flex-direction: column; align-items: center; text-align: center; }
[data-theme="a24"] .mn-prompt { align-items: flex-start; text-align: left; }
.mn-prompt .mn-form, .movie-night .mn-form { margin-top: 14px; padding: 10px 14px; border: 1px solid var(--line); border-radius: var(--radius); display: flex; gap: 10px; align-items: center; justify-content: center; flex-wrap: wrap; }
[data-theme="a24"] .mn-prompt .mn-form, [data-theme="a24"] .movie-night .mn-form { justify-content: flex-start; }
/* Label gets the whole row to itself (flex-basis: 100%), so the input and
   button that follow it wrap onto their own line underneath — an empty
   datetime-local input has no placeholder text of its own, and stretched to
   the field's default full width with nothing labelling it, it just read as
   a blank box with no clue it was tappable. */
.mn-form-label { flex-basis: 100%; }
.mn-form input { width: auto; }

.movie-night { margin: 10px 0; padding: 10px 14px; border: 1px solid var(--line); border-radius: var(--radius); text-align: center; }
[data-theme="a24"] .movie-night { text-align: left; }
.movie-night .mn-when { margin-bottom: 4px; }
.movie-night .mn-actions { margin: 8px 0; display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
[data-theme="a24"] .movie-night .mn-actions { justify-content: flex-start; }
.movie-night .cal-row { margin-top: 4px; }
.movie-night .mn-change-row { margin-top: 4px; display: flex; gap: 14px; justify-content: center; }
[data-theme="a24"] .movie-night .mn-change-row { justify-content: flex-start; }

/* round progress (watched + rated gating) — the pills are <button>s (tap one
   for the popup naming who's done that step and who's outstanding), so reset
   the usual button chrome back to how the plain text they replaced looked. */
.round-progress { display: flex; gap: 36px; justify-content: center; margin: 24px 0 18px; flex-wrap: wrap; }
/* Divider before the progress numbers on the real film card only — the
   landing page's howto demo reuses .round-progress for a small mockup that
   doesn't want one. */
.film-card .round-progress { padding-top: 20px; border-top: 1px solid var(--line); }
.rp-item { text-align: center; background: none; border: none; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; }
.rp-item:hover .rp-label, .rp-item:focus-visible .rp-label { color: var(--accent); }
.rp-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.rp-count { font-family: var(--serif); font-size: 1.6rem; color: var(--text); }
.rp-count .of { color: var(--faint); }
.rp-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted); margin-top: 2px; }

/* …and the popup behind them */
.round-group + .round-group { margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--line); }
.round-group-title { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted); margin: 0 0 10px; font-weight: 600; }
.round-names { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.round-names li { color: var(--text); }
.watch-actions { display: flex; gap: 12px; justify-content: center; align-items: center; flex-wrap: wrap; margin-top: 6px; }
/* "Can't watch this week?" always sits on its own line under "I've watched
   it", centered independently of the row above — flex-basis: 100% forces the
   wrap regardless of screen width, rather than leaving it to whether the two
   happen to fit side by side at any given size. */
.watch-actions .pass-link { flex-basis: 100%; text-align: center; }
.ack-pill { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line-strong); color: var(--muted); padding: 9px 16px; border-radius: var(--radius); font-size: 0.85rem; }
.ack-pill.done { border-color: var(--accent); color: var(--accent); }
.ack-pill.passed { border-style: dashed; color: var(--muted); }
.reveal-note { color: var(--faint); font-size: 0.82rem; margin-top: 16px; font-style: italic; }
/* Extra bottom margin for the one .reveal-note usage that isn't the last
   thing inside a padded card (the "This week's film is still in play" line
   above the Spin Settings box) — everywhere else, the space below a
   reveal-note comes for free from its card's own padding. Kept as an add-on
   rather than changing .reveal-note itself, so every other reveal-note's
   spacing is untouched. */
.wheel-status-note { margin-bottom: 24px; }
.force-line { margin-top: 14px; }
/* Personal streak line under the watch/rate actions — consecutive finished
   rounds kept up with. Text-only (no emoji, per house style); the chip gets
   its "hot streak" read from colour/weight instead. Matches the reveal-note's
   own top margin below (16px) so the gap above and below reads the same. */
.streak-line { margin: 16px 0; text-align: center; }
/* The wrap is exactly the chip's own width (the info bubble is taken out of
   flow, absolutely positioned off its right edge) — text-align: center on
   .streak-line above then centers the CHIP itself, not the chip-plus-bubble
   pair, so the bubble sitting beside it doesn't pull the chip off-centre. */
.streak-chip-wrap { position: relative; display: inline-block; }
/* Sized to match .deadline-pill directly above it in the same card (0.85rem,
   600 weight, 5px/14px padding) — it used to inherit the surrounding
   paragraph's own font instead of the app's chip sizing, which on a plain
   <p> meant full body size, noticeably bigger and heavier than every other
   pill/chip in the UI. */
.streak-chip {
  display: inline-block; border: 1px solid var(--accent); color: var(--accent); font-weight: 600;
  padding: 5px 14px; border-radius: var(--radius); background: none; cursor: pointer;
  font-family: var(--sans); font-size: 0.85rem; letter-spacing: 0.04em;
}
.streak-chip-wrap .info-bubble { position: absolute; top: 50%; left: 100%; transform: translateY(-50%); margin-left: 6px; }

/* ---- wheel ---- */
.wheel-wrap { display: flex; justify-content: center; margin: 4px 0 22px; }
#wheel-canvas { max-width: 92vw; height: auto; }
.wheel-controls { text-align: center; }
/* Only the auto-spin countdown line uses this now — the main "This week's
   film is still in play" status line above the Spin button used to share it
   too, which meant a spacing fix aimed at the countdown line (pairing it with
   "Organise a time to spin together?" below it) also retuned that unrelated
   line's spacing as a side effect. It now uses .reveal-note instead, matching
   the look of every other "what happens next" caption in the app. */
.wheel-status { margin-top: 12px; margin-bottom: 0; line-height: normal; color: var(--muted); }
/* A themed pill (tokens only, so it fits every theme + dark mode) instead of a
   bare browser checkbox floating under the Spin button. */
.stream-filter { display: flex; width: fit-content; align-items: center; gap: 9px; margin: 14px auto 0; padding: 8px 14px; border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--panel-2); color: var(--text); font-family: var(--sans); font-size: 0.85rem; cursor: pointer; }
.stream-filter:hover { border-color: var(--accent); }
.stream-filter:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Square checkbox (native accent-color boxes have unremovable rounded corners),
   themed via tokens: fills with the accent + an ink checkmark when ticked. */
.stream-filter input { appearance: none; -webkit-appearance: none; position: relative; width: 15px; height: 15px; margin: 0; cursor: pointer; border: 1px solid var(--line-strong); border-radius: 0; background: var(--panel); }
.stream-filter input:checked { background: var(--accent); border-color: var(--accent); }
.stream-filter input:checked::after { content: ""; position: absolute; left: 50%; top: 50%; width: 4px; height: 8px; border: solid var(--accent-ink); border-width: 0 2px 2px 0; transform: translate(-50%, -60%) rotate(45deg); }
.filter-note { margin-top: 8px; }
/* Generic "checkbox pill + info bubble" box — originally built for the
   everyone-can-stream filter, now shared with the seen-it filter too (any
   future wheel toggle that needs an explainer can reuse it the same way):
   opening the explanation adds height below, never changes the box's width
   or moves the bubble off the checkbox's line. That ruled out
   <details>/<summary> (the usual pattern here) — a shrink-to-fit box resizes
   to fit whichever content is showing, and forcing the revealed text onto
   its own line pushes the trigger down with it. A plain toggle button +
   hidden paragraph avoids both: the button never leaves the row, and the
   box's width is fixed up front rather than derived from content. The
   bubble is still a flex sibling of the label (not nested inside it), so
   tapping it still can't toggle the checkbox. */
.filter-box { width: min(346px, 100%); margin: 14px auto 0; padding: 8px 10px; border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--panel-2); }
.filter-box:hover { border-color: var(--accent); }
.filter-box + .filter-box { margin-top: 14px; }
/* Vote instead sits at the TOP of Spin settings, right under the film count,
   not among the tuning knobs below it: it's a different kind of decision
   ("how do we pick this week?") than they are ("tune the automatic pick"),
   and it uses the exact same filtered film list those knobs produce, so
   reading it before them would wrongly suggest it's unaffected by them. Its
   own gap to what follows matches every other row's, though, same 14px
   rhythm throughout the panel. */
.filter-row { display: flex; align-items: center; gap: 7px; }
.filter-row:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
.filter-box .stream-filter { width: auto; flex: 1 1 auto; min-width: 0; margin: 0; padding: 0; border: none; background: none; }
.late-line { flex: 1 1 auto; min-width: 0; text-align: left; }
.last-film-late-box .text-link { display: block; margin-top: 8px; }
.info-bubble {
  flex: none; cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  width: 17px; height: 17px; padding: 0; border-radius: 50%; border: 1px solid var(--line-strong);
  color: var(--accent); font-size: 0.7rem; font-weight: 700; font-style: italic;
  font-family: Georgia, "Times New Roman", serif; background: var(--panel-2);
}
.info-bubble:hover { border-color: var(--accent); background: var(--panel); }
.info-bubble:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.filter-help-text { margin: 8px 0 0; text-align: left; }
.filter-help-text.hidden { display: none; }
/* "In the mood for…" genre/mood picker, and the other two dropdown rows
   (Spin with, Time to watch) — same box width/height/padding as the
   checkbox rows below them (.filter-box), rather than each shrink-wrapping
   to its own select's content width, so the whole Spin settings panel reads
   as one consistent column instead of a ragged mix of narrow pills and full
   rows. */
.wheel-filter-row { display: flex; align-items: center; justify-content: center; gap: 9px; width: min(346px, 100%); margin: 14px auto 0; padding: 8px 10px; border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--panel-2); color: var(--text); font-size: 0.85rem; }
.wheel-filter-row:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Wraps a .wheel-filter-row that also carries an info bubble, so its
   .filter-help-text has somewhere to sit below the pill rather than
   squeezed inside it. */
.wheel-filter-row-wrap { width: min(346px, 100%); margin: 14px auto 0; }
.wheel-filter-row-wrap .wheel-filter-row { margin-top: 0; }
.wheel-filter-label { white-space: nowrap; }
.wheel-filter-select { background: var(--panel); color: var(--text); border: 1px solid var(--line-strong); border-radius: var(--radius); padding: 5px 8px; font-family: var(--sans); font-size: 0.85rem; max-width: 48vw; cursor: pointer; }
.wheel-filter-select:disabled { opacity: 0.5; cursor: not-allowed; }
.wheel-filter-row.is-disabled { opacity: 0.7; }
.vote-start-link { flex: 1 1 auto; min-width: 0; width: auto; margin: 0; padding: 0; border: none; background: none; text-align: center; font-family: var(--sans); font-size: 0.85rem; color: var(--text); text-decoration: none; cursor: pointer; }
.vote-start-link:hover { color: var(--accent); text-decoration: underline; }
.vote-start-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.vote-start-link:disabled { color: var(--faint); cursor: not-allowed; }
.vote-start-link:disabled:hover { color: var(--faint); text-decoration: none; }
.vote-card { text-align: center; }
.vote-list { list-style: none; padding: 0; margin: 14px 0; text-align: left; }
.vote-row { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-top: 1px solid var(--line); cursor: pointer; }
.vote-list li:first-child .vote-row { border-top: none; }
.vote-row input { width: 18px; height: 18px; flex-shrink: 0; }
.vote-title { flex: 1; }
.vote-count { min-width: 22px; text-align: center; border: 1px solid var(--line); border-radius: 999px; padding: 1px 8px; font-size: 0.8rem; color: var(--muted); }
.vote-actions { display: flex; gap: 14px; align-items: center; justify-content: center; flex-wrap: wrap; margin-top: 6px; }
.vote-admin { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-top: 16px; }
.turn-order { margin-top: 26px; text-align: center; }
.turn-order .small { color: var(--muted); text-transform: uppercase; letter-spacing: 0.12em; font-size: 0.7rem; }
.turn-chips { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; align-items: center; margin-top: 10px; }
.turn-chip { display: inline-flex; align-items: center; border: 1px solid var(--line-strong); border-radius: 999px; padding: 5px 13px; font-size: 0.82rem; color: var(--muted); }
.turn-chip.current { border-color: var(--accent); color: var(--accent); font-weight: 600; }
.turn-arrow { color: var(--faint); }
.chip-admin { margin-left: 6px; font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--accent); border: 1px solid var(--accent); border-radius: 999px; padding: 1px 5px; }
/* Deliberately NOT accent-coloured like .chip-admin — that colour already
   means something else (admin / current spinner) here, and reusing it would
   make "is this chip admin, or is it me, or both" ambiguous at a glance. */
.chip-you { margin-left: 6px; font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); border: 1px solid var(--line-strong); border-radius: 999px; padding: 1px 5px; }
/* Dashed rather than solid — same "temporary, not a fixed identity" cue as
   .svc-chip-anything elsewhere, since being busy is meant to lapse. */
.chip-busy { margin-left: 6px; font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); border: 1px dashed var(--line-strong); border-radius: 999px; padding: 1px 5px; }
/* Visual size stays compact (it sits inline in a turn-order pill), but ::after
   pads the actual clickable/tappable area out to a comfortable size. */
.chip-kick { position: relative; margin-left: 6px; border: none; background: none; color: var(--muted); cursor: pointer; font-size: 1.05rem; line-height: 1; padding: 0 2px; }
.chip-kick::after { content: ""; position: absolute; inset: -11px; }
.chip-kick:hover { color: #c2482e; }
.chip-kick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.chip-makeadmin { margin-left: 6px; border: none; background: none; color: var(--muted); cursor: pointer; font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.06em; text-decoration: underline; text-underline-offset: 2px; padding: 0; }
.chip-makeadmin:hover { color: var(--accent); }
.chip-makeadmin:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.claim-admin-note { margin-top: 12px; }
.busy-filter-box { margin-top: 22px; }
.busy-durations { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 10px; }
/* Same tap-to-expand pattern as .lb-help/.svc-everyone elsewhere. */
.unban-list { margin: 12px auto 0; text-align: center; width: fit-content; }
.unban-list summary { cursor: pointer; color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.unban-list ul { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.unban-list li { display: flex; align-items: center; gap: 10px; justify-content: space-between; }

/* ---- films list ---- */
.add-row { display: flex; gap: 12px; }
.add-row input { flex: 1; }
.movie-search { margin-bottom: 10px; }
.movie-list { list-style: none; padding: 0; margin: 0; }
.movie-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 14px 4px; border-bottom: 1px solid var(--line); }
.movie-row:last-child { border-bottom: none; }
.movie-title { font-weight: 600; }
.movie-by { margin-left: auto; }
.movie-main { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.movie-main .movie-by { margin-left: 0; }
/* The row's action buttons, grouped so they can drop to their own line on narrow
   phones (see the mobile media query) instead of squeezing the title column. */
.movie-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin-left: auto; }
.movie-actions:empty { display: none; }

/* ---- TMDB metadata (posters + autocomplete) ---- */
.poster-thumb { width: 44px; height: 66px; object-fit: cover; border-radius: 4px; border: 1px solid var(--line); background: var(--line); flex-shrink: 0; }
.poster-thumb.tiny { width: 32px; height: 48px; }
.poster-thumb.empty { display: inline-block; }
.person-thumb { border-radius: 50%; }
.tmdb-results { margin-top: 8px; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--panel); }
.tmdb-item { display: flex; gap: 10px; align-items: center; width: 100%; text-align: left; padding: 8px 10px; background: none; border: none; border-bottom: 1px solid var(--line); cursor: pointer; color: var(--text); font: inherit; }
.tmdb-item:last-child { border-bottom: none; }
.tmdb-item:hover { background: var(--line); }
.tmdb-item:focus-visible { background: var(--line); outline: 2px solid var(--accent); outline-offset: -2px; }
.tmdb-none { padding: 10px; text-align: center; }
.tmdb-section-label { padding: 8px 10px 4px; border-top: 1px solid var(--line); }
.tmdb-item-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.tmdb-item-title { font-weight: 600; }
.tmdb-attribution { margin: 10px 0 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* TMDB brand guidelines forbid altering the logo's colour — keep it at full
   opacity and its native aspect ratio (height set, width auto). */
.tmdb-logo { height: 16px; width: auto; vertical-align: middle; }
/* A touch larger on the About/Credits page where it's the primary attribution. */
.about-tmdb-logo { height: 22px; }
.import-row { margin: 8px 0 0; }
.lb-help { margin: 6px 0 0; }
.lb-help summary { cursor: pointer; color: var(--accent); text-decoration: underline; text-underline-offset: 2px; width: fit-content; }
.lb-help ol { margin: 8px 0 0; padding-left: 18px; }
.lb-help li { margin: 4px 0; }
.import-help-h { margin: 12px 0 0; text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.8rem; }
.import-help-h:first-of-type { margin-top: 10px; }
.lb-tip { margin: 8px 0 0; opacity: 0.85; }
.lb-help code { border: 1px solid var(--line); background: var(--panel-2); padding: 0 4px; border-radius: var(--radius); }
.import-all { display: flex; gap: 10px; align-items: center; font-weight: 600; margin-bottom: 12px; }
.import-list { list-style: none; padding: 0; margin: 0; max-height: 48vh; overflow-y: auto; }
.import-list li { padding: 4px 0; }
.import-list label { display: flex; gap: 10px; align-items: center; cursor: pointer; }
.imp-ttl { flex: 1; min-width: 0; line-height: 1.35; }
/* Several Letterboxd lists imported at once — one block each, so a whole list
   can be taken or left in one click. The outer list scrolls, not each group. */
.import-skipped { margin: -6px 0 12px; }
.import-group + .import-group { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 12px; }
.import-group-all { margin-bottom: 6px; font-size: 0.92rem; }
.import-group .import-list { max-height: none; }
/* Override the global input{width:100%} so these checkboxes stay small boxes,
   not full-width fields (which squashed the titles into a per-word column). */
.modal-box .import-all input, .modal-box .import-list input { width: 18px; height: 18px; flex-shrink: 0; margin: 0; padding: 0; }
.rec-head-right { display: inline-flex; align-items: center; gap: 10px; min-width: 0; max-width: 100%; margin-top: 10px; }
.rec-seed-label { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; flex-shrink: 1; overflow: hidden; }
/* Icon-only, so it needs its own accessible-name label rather than visible
   text — the .link-btn base's roomy text padding would look oversized around
   a single glyph, so this trims it down. */
/* The ⟳ glyph's own ink sits low in its em-box (a font-metrics quirk of this
   character, not a flex/alignment issue) — nudged up to actually look centred
   against the dropdown beside it. var(--text), not literal black, so it stays
   readable in dark mode too. */
.rec-refresh { padding: 8px; font-size: 1.6rem; line-height: 1; flex-shrink: 0; color: var(--text); transform: translateY(-3px); }
/* min-width: 0 is load-bearing: a <select> is a flex item here (child of the
   inline-flex .rec-seed-label), and its default min-width:auto resolves to its
   min-content — which iOS Safari takes to be the width of the widest <option>
   (a long film title). Per spec min-width OVERRIDES max-width, so without this
   the select ignores the 52vw cap and pushes the page wider than the viewport
   (mobile horizontal scroll). Chromium clips selects so it never showed there. */
.rec-seed { min-width: 0; max-width: 52vw; font: inherit; font-size: 0.85rem; padding: 4px 6px; border: 1px solid var(--line-strong); border-radius: 6px; background: var(--panel); color: var(--text); }
.rec-seed-search { position: relative; margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
.rec-seed-search .rec-seed-query, .rec-seed-search input { max-width: 320px; }
.rec-seed-chip { display: inline-flex; align-items: center; gap: 6px; align-self: flex-start; background: var(--panel-2); border: 1px solid var(--line-strong); border-radius: 999px; padding: 3px 6px 3px 12px; font-size: 0.85rem; }
.rec-seed-clear { background: none; border: none; cursor: pointer; color: var(--muted); font-size: 1rem; line-height: 1; padding: 2px 4px; }
.rec-seed-clear:hover { color: var(--danger); }
.rec-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 10px; margin-top: 8px; }
.rec { display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; background: none; border: 1px solid var(--line); border-radius: var(--radius); padding: 10px; cursor: pointer; color: var(--text); font: inherit; }
.rec:hover { border-color: var(--accent); }
.rec-poster { width: 66px; height: 99px; object-fit: cover; border-radius: 4px; background: var(--line); }
.rec-poster.empty { display: block; }
.rec-title { font-size: 0.82rem; line-height: 1.2; }
.rec-add { font-size: 0.75rem; color: var(--accent); font-weight: 600; }
/* aspect-ratio reserves the poster's box (TMDB posters are always 2:3) BEFORE
   the image itself has downloaded — without it, height:auto collapses to 0
   until the real image arrives, then everything below snaps down all at
   once. That sudden reflow, on essentially every film card and detail popup,
   is most of what reads as the app being "jumpy". */
.film-poster { display: block; width: 120px; height: auto; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 6px; border: 1px solid var(--line); margin: 18px auto 14px; background: var(--line); }
.film-tmdb { margin: 4px 0 0; }
.watch-providers:empty { display: none; }
.watch-providers { margin: 14px 0 2px; }
.watch-label { text-transform: uppercase; letter-spacing: 0.1em; margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); }
.watch-logos { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin: 6px 0; }
.watch-logo { width: 38px; height: 38px; border-radius: 8px; border: 1px solid var(--line); object-fit: cover; background: var(--line); display: block; }
.watch-logo-link { display: inline-block; border-radius: 8px; transition: transform 0.15s; }
.watch-logo-link:hover, .watch-logo-link:focus-visible { transform: scale(1.06); }
.watch-name { padding: 4px 8px; border: 1px solid var(--line); border-radius: 6px; font-size: 0.8rem; }
.watch-attr { margin-top: 4px; }

/* ---- who can watch (streaming-service match) ---- */
.who-can-watch:empty { display: none; }
.who-can-watch { margin: 10px 0 2px; }
.who-label { text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 4px; margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); }
.who-row { font-size: 0.9rem; margin: 2px 0; }
.who-row.cant { color: var(--muted); }
.who-note { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); }
.svc-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.svc-chip { padding: 6px 12px; border: 1px solid var(--line-strong); border-radius: 999px; background: var(--panel); color: var(--text); font: inherit; font-size: 0.85rem; cursor: pointer; }
.svc-chip:hover { border-color: var(--accent); }
.svc-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.svc-chip.on { background: var(--accent); color: var(--accent-contrast, #fff); border-color: var(--accent); }
/* The catch-all option, not just another service — a dashed border sets it
   apart from the specific ones in the same row until it's actually selected,
   at which point it fills in identically to any other chosen chip. */
.svc-chip-anything { font-weight: 700; border-style: dashed; }
.svc-chip-anything.on { border-style: solid; }
/* Same tap-to-expand pattern as .lb-help/.stream-filter-help elsewhere. */
.svc-everyone { margin: 10px 0 0; }
.svc-everyone summary { cursor: pointer; color: var(--accent); text-decoration: underline; text-underline-offset: 2px; width: fit-content; }
.svc-everyone-list { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
/* "Arrange this tab" — a plain list of section names with up/down buttons
   (arrangeTabHtml in app.js), not a drag target, so a simple row list is all
   this needs. */
.reorder-list { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; }
.reorder-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--line); }
.reorder-row:last-child { border-bottom: none; }
.reorder-btns { display: flex; gap: 6px; flex-shrink: 0; }
.reorder-btn { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; background: var(--panel-2); color: var(--text); border: 1px solid var(--line-strong); border-radius: var(--radius); font-size: 0.95rem; cursor: pointer; }
.reorder-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.reorder-btn:disabled { opacity: 0.35; cursor: default; }
.modal-actions { display: flex; gap: 8px; margin-top: 18px; flex-wrap: wrap; }
/* Busy modal only: "I'm back" and "Close" sit side by side rather than
   stacked full-width like other modals' .modal-actions — .modal-box .btn's
   width: 100% would otherwise make each one claim the whole row and force
   the other onto its own line, so it's overridden here specifically rather
   than for every modal on the site. */
#busy-modal .modal-actions .btn { width: auto; flex: 1 1 0; min-width: 0; }
.modal-box.wide { max-width: 540px; max-height: 85vh; overflow-y: auto; }
.tappable { cursor: pointer; }
.film-title.tappable:hover { text-decoration: underline; }
.film-poster.tappable:hover { filter: brightness(1.06); }
.movie-detail-head { display: flex; gap: 16px; align-items: flex-start; text-align: left; }
.movie-detail-poster { width: 104px; height: auto; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 6px; border: 1px solid var(--line); flex-shrink: 0; background: var(--line); }
.movie-detail-poster.tappable:hover { filter: brightness(1.06); }
.movie-detail-poster.tappable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.movie-detail-meta { min-width: 0; }
.movie-detail-meta h2 { margin: 0 0 4px; }
.movie-tagline { margin: 8px 0 0; font-style: italic; }
.movie-overview { margin: 16px 0 0; line-height: 1.55; text-align: left; }
.movie-credits { margin: 10px 0 0; text-align: left; }
.movie-attr { margin-top: 14px; }
.movie-trailer { margin-top: 12px; }
#movie-modal-watch { margin-top: 14px; }
.voteoff.on { color: var(--accent); }
.edit-prov-link { display: inline-block; margin-top: 10px; }
.region-row { display: flex; align-items: center; gap: 8px; margin-top: 12px; }
.region-row select { padding: 6px 10px; border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--panel); color: var(--text); font: inherit; font-size: 0.85rem; }
.add-tip { margin: 10px 0 0; }
.movie-avail { color: var(--muted); margin-top: 2px; }
.movie-avail:empty { display: none; }
.movie-avail.ok { color: #1c7c43; }
.movie-avail.warn { color: #9a6a06; }
.seen-note { display: block; margin-top: 2px; }
.seen-toggle.on { color: var(--accent); }
.tmdb-item-avail { margin-top: 2px; }
.tmdb-item-avail:empty { display: none; }
.tmdb-item-avail.ok { color: #1c7c43; }
.tmdb-item-avail.warn { color: #9a6a06; }
/* The poster is taller than the title/avg text next to it, so top-align
   rather than the general .card-collapse summary's vertical centring. */
.watched-card > summary { align-items: flex-start; }
.watched-card .poster-thumb { width: 52px; height: 78px; }
[data-theme="a24"] .film-poster { margin: 18px auto 16px; }

/* ---- ratings / history ---- */
.pending-card { border-color: var(--line-strong); scroll-margin-top: 84px; }
.cal-row { margin-top: 6px; display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
[data-theme="a24"] .cal-row { justify-content: flex-start; }
.activity { list-style: none; padding: 0; margin: 0; }
.activity li { padding: 7px 0; border-top: 1px solid var(--line); font-size: 0.92rem; }
.activity li:first-child { border-top: none; }
/* Same tap-to-expand pattern as .svc-everyone elsewhere. */
.activity-more { margin-top: 4px; }
.activity-more summary { cursor: pointer; color: var(--accent); text-decoration: underline; text-underline-offset: 2px; width: fit-content; font-size: 0.85rem; }
.activity-more[open] summary { margin-bottom: 4px; }
/* No margin-top: it used to collapse with .ratings-list's own bottom margin
   (adjacent-sibling margins take whichever is bigger, not both added
   together), so the divider before Discussion sat a full 16px further from
   the last review than the same-looking divider between two reviews (28px
   vs 12px) — a real, visible inconsistency, not just a difference in how
   much content came before it. Border + padding-top alone now give it the
   same 12px every other review-to-review divider already has. */
.comments { border-top: 1px solid var(--line); padding-top: 12px; }
.cmt-h { margin: 0 0 8px; font-size: 0.95rem; }
.cmt-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
.cmt { font-size: 0.92rem; line-height: 1.45; }
.cmt-name { font-weight: 600; margin-right: 6px; }
/* Sits inline in running comment text, so it keeps its small visual size (unlike
   the general .link-btn bump above) but gets the same enlarged tap area via
   ::after, the same trick as .chip-kick. */
.cmt-del { position: relative; margin-left: 6px; font-size: 0.78rem; padding: 2px; min-height: 0; display: inline; }
.cmt-del::after { content: ""; position: absolute; inset: -12px; }
.cmt-form { display: flex; gap: 8px; }
.cmt-form .cmt-input { flex: 1; min-width: 0; padding: 8px 12px; font-size: 0.92rem; }
.rating-hist { display: flex; flex-direction: column; gap: 2px; margin: 10px 0 4px; max-width: 220px; }
.hist-bars { display: flex; align-items: flex-end; gap: 2px; height: 34px; }
.hist-bar { flex: 1; min-height: 1px; background: var(--accent); border-radius: 1px 1px 0 0; opacity: 0.85; }
.hist-axis { display: flex; justify-content: space-between; }
.sealed-banner { display: inline-block; border: 1px solid var(--line-strong); color: var(--muted); padding: 4px 12px; border-radius: 999px; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.12em; margin-bottom: 8px; }
.watched-head { flex: 1; min-width: 0; display: flex; justify-content: space-between; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.watched-head h3 { margin: 0; font-size: 1.4rem; }
.watched-avg { color: var(--muted); }
/* Top only — see the comment on .comments above for why a bottom margin here
   was the actual source of that inconsistency, not just something added on
   top of it. */
.ratings-list { margin: 16px 0 0; }
.rating-line { padding: 14px 0 12px; border-top: 1px solid var(--line); }
.rating-name { font-weight: 600; margin-right: 10px; }
/* The review is the card's main content — full text colour so it doesn't read
   as metadata next to the muted "added by" line (italic keeps the voice). */
/* pre-wrap: the box it's typed into is a real textarea, so line breaks
   someone deliberately typed were always there in the stored text — without
   this the DEFAULT "normal" white-space collapses every one of them back
   into a single space the moment it's displayed, so a review written as a
   few short paragraphs came out as one run-on block once posted. */
.review { color: var(--text); font-size: 0.95rem; line-height: 1.55; margin-top: 6px; font-style: italic; white-space: pre-wrap; overflow-wrap: break-word; }

/* Emoji reactions + threaded replies on a review. Everything in this row is a
   compact inline control: the 44px .link-btn treatment would double the row's
   height, so the small buttons keep their visual size and get their tap area
   from an ::after overlay instead (same trick as .cmt-del). */
/* user-select: none across the whole row, not just the chips: holding a chip
   down is a real gesture here (it shows who reacted — see wireReviewSocial),
   and without this iOS answers the same hold with its own text-selection
   handles and the Copy/Look Up/Translate bar on top of the popover.
   -webkit-touch-callout alone doesn't stop that; it only suppresses the
   link/image callout, never the selection itself. Nothing in this row is
   worth selecting anyway (an emoji, a count, "Reply"). */
.reactions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; -webkit-user-select: none; user-select: none; }
.react-chip { position: relative; background: none; border: 1px solid var(--line-strong); border-radius: 999px; padding: 2px 8px; font-size: 0.85rem; cursor: pointer; color: var(--text); display: inline-flex; align-items: center; gap: 4px; -webkit-touch-callout: none; }
.react-chip.mine { border-color: var(--accent); background: var(--panel-2); }
.react-chip .rc-n { color: var(--muted); font-size: 0.78rem; }
/* Holding a chip down shows who reacted (see wireReviewSocial) — the
   touch-only equivalent of the chip's own `title`, which a hover needs. */
.reactor-popover {
  position: absolute; bottom: 130%; left: 50%; transform: translateX(-50%);
  max-width: 200px; padding: 6px 10px; border-radius: var(--radius);
  background: var(--panel); border: 1px solid var(--line-strong);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
  font-size: 0.78rem; font-weight: 400; color: var(--text); text-align: center;
  white-space: normal; line-height: 1.3; z-index: 31; pointer-events: none;
}
.react-add { position: relative; display: inline-flex; }
.react-toggle { background: none; border: 1px dashed var(--line-strong); border-radius: 999px; min-width: 26px; height: 26px; line-height: 1; cursor: pointer; color: var(--muted); font-size: 1rem; }
/* The picker is now two stacked parts: a wrapping grid of one-tap shortcuts
   (the six defaults plus this browser's recent picks) and, under it, the
   any-emoji field. max-width keeps the shortcut grid to a sensible number per
   row once recents have filled up, instead of growing off the screen edge. */
.react-picker { position: absolute; z-index: 30; bottom: 32px; left: 0; display: flex; flex-direction: column; gap: 6px; padding: 6px; background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--radius); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25); }
.react-opts { display: flex; flex-wrap: wrap; gap: 2px; max-width: 238px; }
/* The "+" that ends the quick row and opens the full sheet — dashed, like the
   bar's own toggle, so it reads as "more" rather than as another emoji. */
.react-more { background: none; border: 1px dashed var(--line-strong); border-radius: 6px; min-width: 30px; height: 30px; cursor: pointer; color: var(--muted); font-size: 1rem; line-height: 1; }
.react-more:hover { color: var(--text); border-color: var(--accent); }

/* ---- the all-emoji sheet (js/emoji.js) ---------------------------------
   A bottom sheet on a phone and a centred panel on a wide screen, rather
   than a popup hung off the button: 1,870 emoji will not fit beside a chip.
   position: fixed with its own backdrop, so it can't be clipped by the
   collapsed film card or the tab slider it's opened from. */
.emoji-sheet { position: fixed; inset: 0; z-index: 120; display: flex; align-items: flex-end; justify-content: center; }
.emoji-sheet.hidden { display: none; }
.emoji-sheet-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.45); }
/* height, not max-height: the grid below is flex: 1 and is meant to fill
   whatever's left, but that only works if the panel itself has a fixed size
   to divide up. With max-height, a search with only a few hits let the whole
   panel shrink-wrap to fit them, so the search box and tabs above it visibly
   dropped down the screen as you typed. Fixed height means the grid always
   gets the same amount of room and just sits mostly blank when there isn't
   much to fill it, instead of the whole sheet moving. */
.emoji-sheet-panel { position: relative; display: flex; flex-direction: column; width: 100%; max-width: 460px; height: 70vh; height: min(70dvh, 100%); padding: 12px; gap: 10px; background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--radius) var(--radius) 0 0; box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.3); }
.emoji-sheet-head { display: flex; gap: 8px; align-items: center; }
.emoji-search { flex: 1; min-width: 0; padding: 8px 12px; font-size: 0.95rem; }
.emoji-sheet-close { background: none; border: none; color: var(--muted); font-size: 1.6rem; line-height: 1; cursor: pointer; padding: 0 6px; }
.emoji-sheet-close:hover { color: var(--text); }
/* Nine equal columns that always fit, rather than a scrolling strip: at
   flex: 0 0 auto the row ran off the edge on a phone, so the last category
   was half cut off and selecting it left its underline hidden too. */
.emoji-tabs { display: flex; border-bottom: 1px solid var(--line); }
/* Searching looks across every group, so the tab strip stops claiming one of
   them is the one you're looking at. */
.emoji-tabs.dimmed { opacity: 0.4; }
/* Same shape as the app's own .tab: equal width, and a negative bottom margin
   so the active underline sits ON the strip's border instead of below it. The
   fixed height with the glyph centred is what keeps that underline clear of
   the emoji — at line-height: 1 an emoji overflows its own line box, and the
   border ended up drawn through the middle of it. */
.emoji-tab { flex: 1 1 0; min-width: 0; display: flex; align-items: center; justify-content: center; height: 40px; background: none; border: none; border-bottom: 3px solid transparent; margin-bottom: -1px; font-size: 1.15rem; line-height: 1; padding: 0; cursor: pointer; }
.emoji-tab.active { border-bottom-color: var(--accent); }
/* align-content/align-items: start — without them, a grid container's default
   ("normal", which resolves to stretch here) stretches the one row a short
   search result list needs to fill this WHOLE fixed-height area, and stretches
   each button along with it. The emoji itself still centres inside its own
   (now much taller) button, so it visibly floats in the middle of the sheet
   instead of sitting at the top with the rest of the space plainly blank
   underneath — plus the stretched, near-empty scroll area is what iOS was
   showing that stray grey scrollbar thumb for. */
.emoji-grid { flex: 1; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); align-content: start; align-items: start; gap: 2px; -webkit-overflow-scrolling: touch; }
.emoji-grid button { background: none; border: none; font-size: 1.45rem; line-height: 1; padding: 5px 0; border-radius: 6px; cursor: pointer; }
.emoji-grid button:hover, .emoji-grid button:focus-visible { background: var(--panel-2); }
/* Emoji from different Unicode blocks otherwise sit at visibly different
   heights in a row. The culprits are the ones that were plain text symbols
   first and only became emoji later, so they carry a variation selector to
   ask for colour (the plane U+2708, the heart U+2764, the white flag): Safari
   lays those out on the TEXT font's metrics before the colour font takes
   over, and they ride higher than their neighbours. Naming the colour emoji
   font directly keeps every glyph on one set of metrics. Applied only to
   elements whose whole content is a single emoji — NOT to .react-chip, which
   also holds its count, and whose digits would then be looking for glyphs in
   a font that has none. The category icons are separately all chosen from the
   emoji-native range; see scripts/gen-emoji.mjs. */
.emoji-tab, .emoji-grid button, .react-opt {
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla", sans-serif;
}
.emoji-note { margin: 0; text-align: center; }
.emoji-note:empty, .emoji-note.hidden { display: none; }
/* Stops the page behind scrolling under the sheet on a phone. */
body.emoji-sheet-open { overflow: hidden; }

@media (min-width: 640px) {
  .emoji-sheet { align-items: center; }
  .emoji-sheet-panel { border-radius: var(--radius); height: 60vh; height: min(60dvh, 100%); }
}
.react-opt { background: none; border: none; font-size: 1.3rem; cursor: pointer; padding: 3px 5px; border-radius: 6px; }
.react-opt.mine, .react-opt:hover { background: var(--panel-2); }

/* Compact reaction row, used under discussion comments and replies. Same
   markup as a review's bar, just quieter: a full-size row under every line in
   a busy thread reads as a wall of buttons. Deliberately NOT hover-revealed —
   most of the use here is on a phone, where there is no hover to reveal it
   with, so it stays permanently present and tappable and is toned down by
   size instead. The picker sits closer in to match the smaller "+". */
.reactions-compact { gap: 4px; margin-top: 4px; }
.reactions-compact .react-chip { padding: 1px 6px; font-size: 0.78rem; }
.reactions-compact .react-chip .rc-n { font-size: 0.72rem; }
.reactions-compact .react-toggle { min-width: 20px; height: 20px; font-size: 0.8rem; }
.reactions-compact .react-picker { bottom: 26px; }
/* .reply-line is itself a wrapping flex row, so without this the bar would try
   to sit alongside the reply text rather than under it. */
.reply-line .reactions-compact { flex: 0 0 100%; }
.reply-toggle { position: relative; font-size: 0.8rem; padding: 2px 4px; min-height: 0; display: inline; }
.reply-toggle::after { content: ""; position: absolute; inset: -10px; }
.reply-toggle:hover { color: var(--text); }
.review-replies { margin: 8px 0 0; padding-left: 12px; border-left: 2px solid var(--line); }
/* .reply-line and .reply-form (below) both space themselves from whatever
   comes before with margin-top only, deliberately with no margin-bottom.
   The reply box is only ever hidden by a class, not removed from the DOM
   (see repliesHtml), so whichever of the two actually ends up last on
   screen — a reply, or the open reply box — used to leave its own trailing
   margin sitting on top of the fixed gap below every review, and the space
   under "Reply" before the divider line came out a different size on every
   review depending on how many replies it had and whether the box was open.
   With no bottom margin anywhere in here, that gap is always exactly
   .rating-line's own padding, regardless of what's showing. */
.reply-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; margin: 5px 0 0; font-size: 0.9rem; line-height: 1.45; }
.reply-name { font-weight: 600; }
.reply-text { color: var(--text); }
.reply-del { position: relative; font-size: 0.75rem; padding: 2px; min-height: 0; display: inline; }
.reply-del::after { content: ""; position: absolute; inset: -10px; }
.reply-form { display: flex; gap: 8px; margin: 8px 0 0; align-items: center; }
.reply-form .reply-input { flex: 1; min-width: 0; padding: 8px 12px; font-size: 0.92rem; }
.reply-post { position: relative; font-size: 0.85rem; padding: 4px 6px; min-height: 0; }
.reply-post::after { content: ""; position: absolute; inset: -8px; }
.reply-post:hover { color: var(--text); }
/* padding-bottom matches .rating-line's own trailing 12px (see the comment on
   .comments above) — when this box is what sits directly above Discussion
   (rating not yet given, so the editor is open) rather than a review, it
   needs its OWN matching trailing space, since .comments no longer supplies
   any itself. */
.my-rating { margin-top: 18px; padding-top: 18px; padding-bottom: 12px; border-top: 1px solid var(--line-strong); }
.my-rating-stars { margin: 8px 0 12px; }
.review-input { margin-bottom: 12px; }
.save-note { margin-left: 12px; color: var(--muted); }

/* ---- stars ---- */
.stars-input { font-size: 30px; display: inline-flex; line-height: 1; user-select: none; touch-action: none; }
.stars-input .star { position: relative; display: inline-block; height: 1em; }
.stars-input .bg { color: var(--star-empty); }
.stars-input .fg { position: absolute; left: 0; top: 0; color: var(--accent); overflow: hidden; width: 0; white-space: nowrap; }
.stars-input .half { position: absolute; top: 0; height: 100%; width: 50%; background: none; border: none; padding: 0; cursor: pointer; z-index: 2; }
.stars-input .half.left { left: 0; }
.stars-input .half.right { right: 0; }
.stars-display { font-size: 17px; display: inline-flex; line-height: 1; vertical-align: middle; }
.stars-display .star { position: relative; display: inline-block; height: 1em; }
.stars-display .bg { color: var(--star-empty); }
.stars-display .fg { position: absolute; left: 0; top: 0; color: var(--accent); overflow: hidden; white-space: nowrap; }

/* ---- stats ---- */
.stats-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 24px; }
.tile { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px 14px; text-align: center; }
.tile-value { font-family: var(--serif); font-size: 2rem; font-weight: 700; color: var(--text); }
.tile-label { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 4px; }
.superlatives { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px; margin-bottom: 24px; }
.superlative { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; }
.sup-label { font-size: 0.7rem; color: var(--accent); text-transform: uppercase; letter-spacing: 0.12em; }
.sup-who { font-family: var(--serif); font-weight: 700; font-size: 1.05rem; margin-top: 4px; }
.sup-detail { font-size: 0.85rem; color: var(--muted); margin-top: 2px; }
.leaderboard { margin: 6px 0 0; padding-left: 24px; }
.leaderboard li { padding: 8px 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; border-bottom: 1px solid var(--line); }
.leaderboard li:last-child { border-bottom: none; }
.lb-title { font-weight: 600; }
.lb-score { color: var(--muted); margin-left: auto; }
/* Six columns of numbers is the only table in the app. It used to force a
   420px min-width and rely on .table-scroll's own side-scroll to cope on a
   narrow phone — table-layout: fixed plus a wider first (Name) column lets
   it actually shrink to fit instead: text wraps within a cell rather than
   the table pushing wider than its box. .table-scroll's scroll stays as a
   fallback only, for a name long enough that wrapping alone isn't enough. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.people-table { width: 100%; table-layout: fixed; border-collapse: collapse; margin-top: 10px; }
.people-table th, .people-table td { text-align: left; padding: 10px 6px; border-bottom: 1px solid var(--line); overflow-wrap: break-word; }
.people-table th:first-child, .people-table td:first-child { width: 22%; }
.people-table th:last-child, .people-table td:last-child { width: 20%; }
.meta-line { margin: 8px 0; line-height: 1.5; }
.people-table th { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
@media (max-width: 560px) { .stats-tiles { grid-template-columns: repeat(2, 1fr); } }

/* ---- achievements ---- */
.achievements .ach-section { margin-top: 14px; }
.ach-h { margin: 0 0 8px; font-size: 0.95rem; }
.ach-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 10px; }
.ach-badge { display: flex; align-items: flex-start; gap: 9px; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel-2); }
/* Earned: accent frame + a solid star (the same ★ glyph used by star ratings
   elsewhere — not an emoji). Locked: muted, dashed, dimmed — a goal. */
.ach-badge.earned { border-color: var(--accent); }
.ach-badge.locked { border-style: dashed; opacity: 0.55; }
.ach-mark { width: 1.1em; flex-shrink: 0; color: var(--accent); font-size: 1.05rem; line-height: 1.2; text-align: center; }
.ach-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.ach-name { font-weight: 600; line-height: 1.25; }
.ach-desc { line-height: 1.35; }
.ach-board { margin-top: 14px; display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; }
.ach-board-item { white-space: nowrap; }
.ach-board-item b { color: var(--accent); }

/* One-shot ring pulse on the exact badge that just unlocked, so the grid
   itself draws your eye to it even before/alongside the toast. Animates a
   solid-colour box-shadow from a visible ring down to zero spread (invisible)
   — no alpha blending needed, so it doesn't depend on color-mix() support. */
@keyframes ach-pulse {
  0%   { box-shadow: 0 0 0 3px var(--accent); }
  100% { box-shadow: 0 0 0 0 var(--accent); }
}
.ach-badge.just-unlocked { animation: ach-pulse 1.6s ease-out 1; }

/* Achievement-unlock toast: fixed top-center, stacks if several unlock at
   once, auto-dismisses. pointer-events: none on the root so it never blocks
   the page underneath — purely a celebratory notice, nothing to click.
   `top` here is only the default; stackToastBelow() in app.js overrides it
   inline when the push-toast root is showing at the same time, so the two
   don't land on top of each other. */
.ach-toast-root { position: fixed; top: 14px; left: 50%; transform: translateX(-50%); z-index: 200; display: flex; flex-direction: column; gap: 8px; align-items: center; width: min(92vw, 360px); pointer-events: none; }
.ach-toast {
  position: relative; overflow: visible; display: flex; flex-direction: column; gap: 2px; width: 100%; padding: 12px 16px;
  background: var(--panel); border: 1px solid var(--accent); border-radius: var(--radius);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
  opacity: 0; transform: translateY(-14px) scale(0.97);
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.ach-toast.show { opacity: 1; transform: translateY(0) scale(1); }
.ach-toast.hide { opacity: 0; transform: translateY(-8px) scale(0.98); }
.ach-toast-label { font-size: 0.7rem; color: var(--accent); text-transform: uppercase; letter-spacing: 0.14em; font-weight: 700; }
.ach-toast .ach-name { font-family: var(--serif); font-size: 1.1rem; }
/* Small coloured sparks radiating from the toast on unlock — the toast's own
   celebration effect, scoped to itself, not a page-wide confetti rain. */
@keyframes ach-spark-fly {
  0%   { transform: translate(-50%, -50%) rotate(45deg) scale(1); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--tx)), calc(-50% + var(--ty))) rotate(45deg) scale(0); opacity: 0; }
}
.ach-spark {
  position: absolute; top: 50%; left: 50%; width: 6px; height: 6px;
  background: var(--accent); border-radius: 1px;
  pointer-events: none; animation: ach-spark-fly 0.75s ease-out forwards;
}
@media (prefers-reduced-motion: reduce) {
  .ach-badge.just-unlocked { animation: none; }
  .ach-toast { transition: opacity 0.2s linear; transform: none !important; }
  .ach-spark { display: none; }
}

/* A push notification that arrived while the app was already open — same
   look/motion as .ach-toast, but pointer-events re-enabled since this one's
   tappable (dismiss, or jump to the club it's about) and that one isn't. */
.push-toast-root { position: fixed; top: 14px; left: 50%; transform: translateX(-50%); z-index: 200; display: flex; flex-direction: column; gap: 8px; align-items: center; width: min(92vw, 360px); pointer-events: none; }
.push-toast {
  display: flex; flex-direction: column; gap: 2px; width: 100%; padding: 12px 16px;
  background: var(--panel); border: 1px solid var(--accent); border-radius: var(--radius);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); cursor: pointer; pointer-events: auto;
  opacity: 0; transform: translateY(-14px) scale(0.97);
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.push-toast.show { opacity: 1; transform: translateY(0) scale(1); }
.push-toast.hide { opacity: 0; transform: translateY(-8px) scale(0.98); }
.push-toast-title { font-weight: 700; font-size: 0.95rem; }
.push-toast-body { line-height: 1.4; }
@media (prefers-reduced-motion: reduce) {
  .push-toast { transition: opacity 0.2s linear; transform: none !important; }
}

/* ---- generic "saved" toast (rating submit, etc.) — same look/motion as
   .push-toast, but bottom-anchored and never tappable: it's confirming
   something the user just did, not asking them to go anywhere. ---- */
.save-toast-root { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 200; display: flex; flex-direction: column; gap: 8px; align-items: center; width: min(92vw, 360px); pointer-events: none; }
.save-toast {
  padding: 12px 20px; background: var(--panel); border: 1px solid var(--accent); border-radius: var(--radius);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); font-weight: 600; font-size: 0.9rem; text-align: center;
  opacity: 0; transform: translateY(14px) scale(0.97);
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.save-toast.show { opacity: 1; transform: translateY(0) scale(1); }
.save-toast.hide { opacity: 0; transform: translateY(8px) scale(0.98); }
@media (prefers-reduced-motion: reduce) {
  .save-toast { transition: opacity 0.2s linear; transform: none !important; }
}

/* ---- rating distribution bar chart ---- */
.rating-dist { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; }
.dist-row { display: flex; align-items: center; gap: 8px; }
.dist-label { width: 30px; text-align: right; flex-shrink: 0; }
.dist-bar-wrap { flex: 1; background: var(--bg-soft); border: 1px solid var(--line); border-radius: 2px; height: 10px; overflow: hidden; min-width: 0; }
.dist-bar { height: 100%; background: var(--accent); border-radius: 2px; }
.dist-count { width: 24px; flex-shrink: 0; }

/* ---- modal ---- */
/* transform: forces its own compositing layer, which is what keeps a
   position:fixed;inset:0 overlay actually covering the full screen as iOS
   Safari's toolbar shows/hides — without it, the overlay can visibly stop
   short of the real bottom edge after the toolbar collapses, leaving a strip
   of the page underneath showing through un-dimmed. */
.modal { position: fixed; inset: 0; background: rgba(10, 10, 13, 0.8); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 20px; transform: translateZ(0); }
/* max-height + scroll so a long dialog can always reach its own Close button:
   .modal centres the box in a fixed overlay, so anything taller than the
   viewport used to hang off both ends with no way to scroll to the buttons.
   (.modal-box.wide below has carried this for a while; it belongs on all of
   them.) */
.modal-box { background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--radius); padding: 30px; max-width: 400px; width: 100%; max-height: 85vh; overflow-y: auto; }
/* Stacks on top of #movie-modal (both are position:fixed — later in the DOM
   isn't enough on its own to guarantee that once z-index is in play, so this
   is bumped explicitly above the shared .modal z-index of 100). Darker
   backdrop and no padding, so the poster reads as the whole screen rather
   than a boxed dialog. */
.poster-lightbox { z-index: 110; background: rgba(4, 4, 6, 0.92); padding: 0; cursor: pointer; }
.poster-lightbox-img { max-width: 92vw; max-height: 92vh; width: auto; height: auto; border-radius: 6px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); }
.modal-box h2 { margin-top: 0; }
.modal-box input { margin: 14px 0 18px; }
.modal-box .btn { width: 100%; }
/* The name prompt specifically: the gap above and below the input looked
   noticeably bigger than the gap between the title and the description right
   above it, because that title-to-description gap is just two browser
   default margins colliding, while the input's own 14px/18px above/below
   stacked on TOP of the paragraph's default margin rather than replacing it.
   Rebuilt every gap in this modal from one explicit value instead of leaving
   some of them to browser defaults: title and description each carry the gap
   as their own bottom margin; input and the (usually-empty, reserved-height)
   error line carry none, so #name-error's own ~16px min-height supplies the
   matching gap below the input without it being doubled up. */
#name-modal h2, #name-modal h2 + p { margin: 0 0 16px; }
#name-modal input, #name-error { margin: 0; }
#name-modal .modal-actions { margin-top: 0; }
/* These two are read-only popups (round detail, film details) whose Close
   button sits directly under a variable-height body with no field/label
   above it to create breathing room the way the other modals' inputs do —
   so it read as cramped, right on top of the last line of content. */
#round-modal-close, #movie-modal-close { margin-top: 20px; }

/* Feedback modal: category dropdown + textarea */
.feedback-select { width: 100%; background: var(--bg-soft); color: var(--text); border: 1px solid var(--line-strong); border-radius: var(--radius); padding: 10px 12px; font-size: 0.95rem; font-family: var(--sans); margin: 8px 0 14px; }
#feedback-text { min-height: 110px; margin-bottom: 6px; }
#feedback-error:empty { display: none; }
#feedback-thanks p { margin-top: 0; }

/* ---- spin overlay ---- */
.spin-overlay { position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center; background: rgba(10, 10, 13, 0.94); backdrop-filter: blur(6px); animation: fade 0.25s ease; }
.spin-overlay.closing { animation: fadeout 0.6s ease forwards; }
@keyframes fadeout { to { opacity: 0; } }
/* The spin overlay is ALWAYS a dark scrim, whatever the theme or mode — so the
   theme's --accent (tuned for a light page) can be unreadable on it. In the
   default theme's light mode it's #0a0a0a: the winning film's name was being
   drawn black-on-black at the one moment everyone is looking at it. Pin a
   readable, still-on-brand accent for everything inside the overlay. */
.spin-overlay { --accent: #f0f1f3; }
[data-theme="festival"] .spin-overlay { --accent: #e8794f; }
[data-theme="strokes"] .spin-overlay { --accent: #ff5a5a; }
.spin-stage { text-align: center; }
.spin-pointer-label { color: var(--muted); text-transform: uppercase; letter-spacing: 0.16em; font-size: 0.78rem; margin-bottom: 12px; }
.spin-canvas { max-width: 90vw; height: auto; }
.spin-caption { margin-top: 22px; font-family: var(--serif); font-size: 1.5rem; font-weight: 700; min-height: 1.6em; }
.spin-caption.win { font-size: 2.1rem; color: var(--accent); animation: pop 0.4s ease; }
@keyframes pop { 0% { transform: scale(0.6); opacity: 0; } 70% { transform: scale(1.15); } 100% { transform: scale(1); opacity: 1; } }

/* ---- theme picker popover ---- */
/* max-height + scroll: this used to only kick in under 640px width, but the
   thing that actually determines whether it overflows is the WINDOW'S
   HEIGHT, not its width — a desktop browser resized to half-screen (still
   well above 640px wide) can be short enough that the bottom of a long list
   (Create/Join/Reset/Leave, or every theme option) renders past the visible
   viewport with nothing to scroll it into view. Applies everywhere now. */
.theme-pop { position: fixed; z-index: 120; min-width: 204px; max-height: 72vh; overflow-y: auto; background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--radius); padding: 6px; box-shadow: 0 16px 44px rgba(0, 0, 0, 0.45); animation: fade 0.15s ease; }
.theme-opt { display: flex; align-items: center; gap: 11px; width: 100%; background: none; border: none; color: var(--text); cursor: pointer; padding: 9px 10px; border-radius: var(--radius); font-size: 0.9rem; text-align: left; font-family: var(--sans); }
.theme-opt:hover { background: var(--panel-2); }
.theme-swatch { width: 26px; height: 26px; border-radius: 6px; flex: none; border: 1px solid rgba(128, 128, 128, 0.4); }
.theme-name { flex: 1; }
.theme-opt.active { color: var(--accent); font-weight: 700; }
.theme-opt.active .theme-swatch { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- clubs switcher popover ---- */
.clubs-pop { position: fixed; z-index: 120; min-width: 220px; max-width: 280px; max-height: 72vh; overflow-y: auto; background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--radius); padding: 6px; box-shadow: 0 16px 44px rgba(0, 0, 0, 0.45); animation: fade 0.15s ease; }
.clubs-pop-loading, .clubs-pop-empty { padding: 9px 10px; margin: 0; }
.clubs-pop-row { display: flex; align-items: center; justify-content: space-between; }
.clubs-pop-row .clubs-pop-item { flex: 1; min-width: 0; }
.clubs-pop-forget { background: none; border: none; color: var(--muted); cursor: pointer; padding: 6px 8px; font-size: 0.8rem; border-radius: var(--radius); flex-shrink: 0; }
.clubs-pop-forget:hover { color: var(--text); background: var(--panel-2); }
.clubs-pop-item { display: flex; flex-direction: column; width: 100%; background: none; border: none; color: var(--text); cursor: pointer; padding: 9px 10px; border-radius: var(--radius); font-size: 0.9rem; text-align: left; font-family: var(--sans); }
.clubs-pop-item:hover { background: var(--panel-2); }
.clubs-pop-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.clubs-pop-item small { color: var(--muted); font-size: 0.75rem; letter-spacing: 0.08em; }
.clubs-pop-divider { border-top: 1px solid var(--line); margin: 6px 2px; }
.clubs-pop-action { display: block; width: 100%; background: none; border: none; color: var(--text); cursor: pointer; padding: 9px 10px; border-radius: var(--radius); font-size: 0.9rem; text-align: left; font-family: var(--sans); }
.clubs-pop-action:hover { background: var(--panel-2); }
.clubs-pop-muted { color: var(--muted); }
.clubs-pop-muted:hover { color: var(--text); }

/* ---- group reset (unanimous consent) ---- */
#reset-banner { margin-bottom: 20px; }
.reset-box { background: var(--panel); border: 1px solid var(--accent); border-radius: var(--radius); padding: 18px 20px; }
.reset-head { font-family: var(--serif); font-size: 1.1rem; font-weight: 700; }
.reset-desc { color: var(--muted); font-size: 0.9rem; margin: 6px 0 10px; }
.reset-progress { font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--accent); margin-bottom: 12px; }
.reset-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }

/* ---- pending join requests (an existing member approves/declines) ---- */
#join-requests-banner { margin-bottom: 20px; }
.join-req-list { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.join-req-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.join-req-name { font-weight: 600; flex: 1; min-width: 0; }
/* "paste the link from your other device" — folded away until it's relevant */
.acct-paste { margin: 0 0 16px; border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 12px; }
.acct-paste > summary { cursor: pointer; font-size: 0.85rem; color: var(--muted); }
.acct-paste[open] > summary { margin-bottom: 8px; }
#wrong-dev-url { font-size: 0.75rem; }
/* Confirm dialogs are plain text (textContent, so user input can't inject
   markup) — pre-line keeps the paragraph breaks in the longer explanations. */
#confirm-text { white-space: pre-line; }
/* "a member of this name already exists" — sits under the row it belongs to */
.join-req-warn { font-size: 0.82rem; line-height: 1.55; color: var(--muted); border-left: 2px solid var(--line-strong); padding: 2px 0 2px 10px; margin: -2px 0 4px; }

/* ---- account-save nudge (dismissible; reuses .reset-box's frame) ---- */
#account-nudge { margin-bottom: 20px; }
.acct-nudge-box { border-color: var(--line-strong); }
.acct-nudge-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }

/* ---- "you're marked busy" reminder (reuses .reset-box's frame) ----
   Shows on every tab, not just the Busy modal/chip, because "why did my turn
   get skipped / why didn't this wait for me" is exactly the confusion this
   exists to head off — someone who set themselves busy weeks ago and forgot
   shouldn't have to go looking for the one chip that explains it. */
#busy-banner { margin-bottom: 20px; }
.busy-banner-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }

/* ---- account modal: Google + email, both optional ---- */
.btn-google { width: 100%; }
.account-or { display: flex; align-items: center; gap: 10px; margin: 14px 0; color: var(--faint); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.12em; }
.account-or::before, .account-or::after { content: ""; flex: 1; height: 1px; background: var(--line); }

/* ===========================================================================
   THEME 1 — DEFAULT (A24 editorial) : black & white, hairline rules,
   everything LEFT-aligned, oversized uppercase grotesque, square, lots of air.
   =========================================================================== */
[data-theme="a24"] {
  --bg: #ffffff; --bg-soft: #f4f4f4; --panel: #ffffff; --panel-2: #f4f4f4;
  --line: #111111; --line-strong: #000000; --text: #0a0a0a; --muted: #6b6b6b;
  --faint: #9a9a9a; --accent: #0a0a0a; --accent-hi: #000000; --accent-ink: #ffffff;
  --danger: #c0241c; --star-empty: #d6d6d6; --topbar-bg: rgba(255, 255, 255, 0.95);
  --radius: 0px; --maxw: 880px;
  --serif: "Archivo", system-ui, sans-serif;
  --sans: "Archivo", system-ui, sans-serif;
}
[data-theme="a24"] body { background: var(--bg); }
[data-theme="a24"] body::after {
  content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 3; opacity: 0.14;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='130' height='130'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
}
[data-theme="a24"] .topbar { padding: 18px 26px; border-bottom: 1px solid #111; }
[data-theme="a24"] .brand { font-weight: 800; text-transform: uppercase; letter-spacing: 0.02em; font-size: 1.3rem; }
[data-theme="a24"] h1, [data-theme="a24"] h2, [data-theme="a24"] h3, [data-theme="a24"] .film-title { text-transform: uppercase; font-weight: 800; letter-spacing: -0.01em; }
[data-theme="a24"] .film-banner, [data-theme="a24"] .sealed-banner, [data-theme="a24"] .tab,
[data-theme="a24"] .tile-label, [data-theme="a24"] .sup-label, [data-theme="a24"] .reset-progress,
[data-theme="a24"] .turn-order .small { text-transform: uppercase; letter-spacing: 0.16em; }
/* left-aligned, oversized editorial landing + film card */
[data-theme="a24"] .hero { text-align: left; margin: 16px 0 44px; }
[data-theme="a24"] .hero h1 { font-size: 3.4rem; line-height: 1.02; }
[data-theme="a24"] .lead { margin: 0; max-width: 600px; }
[data-theme="a24"] .lead + .lead { margin-top: 10px; }
[data-theme="a24"] .film-card { text-align: left; padding: 36px 30px; }
[data-theme="a24"] .film-title { font-size: 2.7rem; }
[data-theme="a24"] .film-meta, [data-theme="a24"] .deadline-row,
[data-theme="a24"] .round-progress, [data-theme="a24"] .watch-actions, [data-theme="a24"] .cal-row { justify-content: flex-start; }
[data-theme="a24"] .film-banner { display: block; border-bottom: 1px solid #111; padding-bottom: 10px; margin-bottom: 4px; }
[data-theme="a24"] .rp-count { font-size: 2.1rem; }
/* hairline cards, no shadow, square */
[data-theme="a24"] .card, [data-theme="a24"] .tile, [data-theme="a24"] .superlative,
[data-theme="a24"] .modal-box, [data-theme="a24"] .reset-box { border: 1px solid var(--line); box-shadow: none; }
[data-theme="a24"] .deadline-pill, [data-theme="a24"] .ack-pill, [data-theme="a24"] .sealed-banner,
[data-theme="a24"] .turn-chip, [data-theme="a24"] .react-chip, [data-theme="a24"] .react-toggle,
[data-theme="a24"] .rec-seed-chip, [data-theme="a24"] .streak-chip { border-radius: 0; }
[data-theme="a24"] .btn { border: 1px solid #111; text-transform: uppercase; letter-spacing: 0.1em; font-weight: 700; }
[data-theme="a24"] .btn:hover { color: #000; border-color: #000; }
[data-theme="a24"] .btn.primary { background: #0a0a0a; color: #fff; border-color: #0a0a0a; }
[data-theme="a24"] .btn.primary:hover { background: #000; color: #fff; }
[data-theme="a24"] a, [data-theme="a24"] .text-link { color: #0a0a0a; text-decoration: underline; }
[data-theme="a24"] .tabs { border-bottom: 1px solid #111; gap: 0; }
[data-theme="a24"] .tab { font-size: 0.82rem; padding: 14px 18px 12px; }
[data-theme="a24"] .tab.active { color: #000; border-bottom: 3px solid #000; }
[data-theme="a24"] #wheel-canvas { border: 1px solid #111; }
[data-theme="a24"] .tabs { counter-reset: a24tab; }
[data-theme="a24"] .tab::before { counter-increment: a24tab; content: counter(a24tab, decimal-leading-zero) "\00a0\00a0"; opacity: 0.4; }

/* ===========================================================================
   THEME 2 — CINEMA (festival print) : a grainy printed programme. Paper, double
   ink rules, condensed Oswald caps over serif, one burnt-orange ink, square.
   =========================================================================== */
[data-theme="festival"] {
  --bg: #ece2cd; --bg-soft: #f3ebd9; --panel: #f3ebd9; --panel-2: #e7dcc2;
  --line: #c8b994; --line-strong: #211c14; --text: #211c14; --muted: #6b6048;
  --faint: #9a8c6e; --accent: #c2482e; --accent-hi: #d4543a; --accent-ink: #f6efdd;
  --danger: #a8321f; --star-empty: #cabb96; --topbar-bg: rgba(236, 226, 205, 0.96);
  --radius: 0px; --maxw: 700px;
  --serif: Georgia, "Times New Roman", serif;
  --sans: Georgia, "Times New Roman", serif;
}
[data-theme="festival"] body {
  background-color: #ece2cd;
  background-image: radial-gradient(125% 130% at 50% 0%, #f1e8d6 0%, #e6dbc2 65%, #ddd0b4 100%);
}
[data-theme="festival"] body::after {
  content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 2; opacity: 0.12;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* masthead top bar */
[data-theme="festival"] .topbar { border-top: 5px solid var(--line-strong); border-bottom: 3px double var(--line-strong); }
[data-theme="festival"] .brand { font-family: "Oswald", system-ui, sans-serif; text-transform: uppercase; font-weight: 700; font-size: 1.5rem; letter-spacing: 0.06em; }
[data-theme="festival"] h1, [data-theme="festival"] h2, [data-theme="festival"] h3, [data-theme="festival"] .film-title {
  font-family: "Oswald", system-ui, sans-serif; text-transform: uppercase; font-weight: 600; letter-spacing: 0.02em;
}
[data-theme="festival"] .film-banner, [data-theme="festival"] .sealed-banner, [data-theme="festival"] .tab,
[data-theme="festival"] .sup-label, [data-theme="festival"] .tile-label, [data-theme="festival"] .reset-progress,
[data-theme="festival"] .rp-label, [data-theme="festival"] .turn-order .small {
  font-family: "Oswald", system-ui, sans-serif; text-transform: uppercase; letter-spacing: 0.16em;
}
[data-theme="festival"] .film-title { font-size: 2.5rem; }
/* double-ruled print blocks */
[data-theme="festival"] .card, [data-theme="festival"] .tile, [data-theme="festival"] .superlative,
[data-theme="festival"] .modal-box, [data-theme="festival"] .reset-box {
  border: 3px double var(--line-strong);
}
[data-theme="festival"] .film-card { border-width: 3px; }
[data-theme="festival"] .film-banner { display: inline-block; border-top: 1px solid var(--line-strong); border-bottom: 1px solid var(--line-strong); padding: 4px 0; }
[data-theme="festival"] .btn { border: 1px solid var(--line-strong); }
[data-theme="festival"] .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
[data-theme="festival"] .btn.primary:hover { background: var(--accent-hi); border-color: var(--accent-hi); color: var(--accent-ink); }
[data-theme="festival"] .deadline-pill, [data-theme="festival"] .sealed-banner, [data-theme="festival"] .ack-pill,
[data-theme="festival"] .turn-chip, [data-theme="festival"] .react-chip, [data-theme="festival"] .react-toggle,
[data-theme="festival"] .rec-seed-chip, [data-theme="festival"] .streak-chip { border-radius: 0; }
[data-theme="festival"] .deadline-pill { border-color: var(--accent); color: var(--accent); }
[data-theme="festival"] .tabs { border-top: 1px solid var(--line-strong); border-bottom: 3px double var(--line-strong); }
[data-theme="festival"] .tab.active { border-bottom: 3px solid var(--accent); }
[data-theme="festival"] #wheel-canvas { border: 3px double var(--line-strong); }

/* ===========================================================================
   THEME 3 — WEB 1.0 (Strokes / Win95) : cobalt desktop, every card
   is a beveled WINDOW with a navy title bar, folder tabs, pixel headings, bright
   wheel, hot-red CTA. Surface-aware text (light on desktop, dark in windows).
   =========================================================================== */
[data-theme="strokes"] {
  --bg: #0a1aa8; --bg-soft: #ffffff; --panel: #ececec; --panel-2: #dcdcd4;
  --line: #808080; --line-strong: #000000; --text: #111111; --muted: #555555;
  --faint: #777777; --accent: #cc1f1f; --accent-hi: #e23b2f; --accent-ink: #ffffff;
  --danger: #cc0000; --star-empty: #b0b0b0; --topbar-bg: #0a1aa8;
  --radius: 0px; --maxw: 820px;
  --serif: "Pixelify Sans", "Courier New", monospace;
  --sans: "Times New Roman", Times, serif;
}
[data-theme="strokes"] body {
  background-color: #0a1aa8;
  background-image: repeating-conic-gradient(#0a1aa8 0% 25%, #0c1cb6 0% 50%);
  background-size: 6px 6px;
}
[data-theme="strokes"] body::after {
  content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 3; opacity: 0.07;
  background-image: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0.55) 0 1px, transparent 1px 3px);
}
[data-theme="strokes"] .topbar { color: #fff; border-bottom: 2px solid #000; }
[data-theme="strokes"] .brand, [data-theme="strokes"] .group-name { color: #fff; }
/* windows: beveled panel + navy title bar + faux close button */
[data-theme="strokes"] .card, [data-theme="strokes"] .modal-box, [data-theme="strokes"] .reset-box {
  position: relative; padding-top: 36px; background: var(--panel); color: #111;
  border: 2px solid; border-color: #ffffff #808080 #808080 #ffffff;
  box-shadow: 0 0 0 1px #000, 3px 3px 0 rgba(0, 0, 0, 0.35);
}
[data-theme="strokes"] .card::before, [data-theme="strokes"] .modal-box::before, [data-theme="strokes"] .reset-box::before {
  content: ""; position: absolute; top: 3px; left: 3px; right: 3px; height: 22px;
  background: linear-gradient(90deg, #000088, #1840d8);
}
[data-theme="strokes"] .modal-box::after, [data-theme="strokes"] .reset-box::after {
  content: "X"; position: absolute; top: 5px; right: 6px; width: 18px; height: 16px;
  background: #d4d0c8; color: #000; font-family: var(--sans); font-size: 11px; font-weight: 700;
  line-height: 14px; text-align: center; border: 1px solid; border-color: #ffffff #808080 #808080 #ffffff;
}
[data-theme="strokes"] .tile, [data-theme="strokes"] .superlative {
  background: var(--panel); color: #111; border: 2px solid; border-color: #ffffff #808080 #808080 #ffffff;
}
[data-theme="strokes"] .btn, [data-theme="strokes"] .chip {
  background: #d4d0c8; color: #000; border: 2px solid; border-color: #ffffff #808080 #808080 #ffffff;
}
[data-theme="strokes"] .btn:hover, [data-theme="strokes"] .chip:hover { color: #000; }
[data-theme="strokes"] .btn:active, [data-theme="strokes"] .chip:active { border-color: #808080 #ffffff #ffffff #808080; }
[data-theme="strokes"] .btn.primary { background: var(--accent); color: #fff; border-color: #f08a7e #7a0f0f #7a0f0f #f08a7e; }
[data-theme="strokes"] .btn.primary:hover { background: var(--accent-hi); color: #fff; }
[data-theme="strokes"] input, [data-theme="strokes"] textarea { background: #fff; color: #111; border: 2px solid; border-color: #808080 #ffffff #ffffff #808080; }
/* selects get the same inset-field treatment as inputs (they were the one
   form control left flat/panel-grey on a grey window) */
[data-theme="strokes"] select { background: #fff; color: #111; border: 2px solid; border-color: #808080 #ffffff #ffffff #808080; border-radius: 0; }
/* the Win95 theme is the squarest of the three — no round pills anywhere
   (a24/festival square these same classes in their own blocks above) */
[data-theme="strokes"] .deadline-pill, [data-theme="strokes"] .ack-pill, [data-theme="strokes"] .turn-chip,
[data-theme="strokes"] .react-chip, [data-theme="strokes"] .react-toggle, [data-theme="strokes"] .rec-seed-chip,
[data-theme="strokes"] .streak-chip { border-radius: 0; }
/* recommendation cards read as small raised panels, like .tile */
[data-theme="strokes"] .rec { background: var(--panel-2); border: 2px solid; border-color: #ffffff #808080 #808080 #ffffff; }
[data-theme="strokes"] .rec:active { border-color: #808080 #ffffff #ffffff #808080; }
/* autocomplete results = a Win95 menu: white inset list, navy highlight */
[data-theme="strokes"] .tmdb-results { background: #fff; border: 2px solid; border-color: #808080 #ffffff #ffffff #808080; }
[data-theme="strokes"] .tmdb-item:hover, [data-theme="strokes"] .tmdb-item:focus-visible { background: #000080; color: #fff; }
[data-theme="strokes"] .tmdb-item:hover .muted, [data-theme="strokes"] .tmdb-item:focus-visible .muted,
[data-theme="strokes"] .tmdb-item:hover .tmdb-item-avail, [data-theme="strokes"] .tmdb-item:focus-visible .tmdb-item-avail { color: #c8d0ff; }
/* the emoji picker pop-over as a raised Win95 menu (hard offset shadow) */
[data-theme="strokes"] .react-picker { background: #d4d0c8; border: 2px solid; border-color: #ffffff #808080 #808080 #ffffff; box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4); }
[data-theme="strokes"] .emoji-sheet-panel { background: #d4d0c8; border: 2px solid; border-color: #ffffff #808080 #808080 #ffffff; border-radius: 0; box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4); }
[data-theme="strokes"] .emoji-grid button:hover, [data-theme="strokes"] .emoji-tab.active { background: #000080; }
/* Achievement toast as a raised Win95 popup notification (hard offset shadow,
   beveled edge) instead of the soft drop-shadow the other two themes use. */
[data-theme="strokes"] .ach-toast { background: #d4d0c8; border: 2px solid; border-color: #ffffff #808080 #808080 #ffffff; box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.4); }
[data-theme="strokes"] a, [data-theme="strokes"] .text-link { color: #0000cc; text-decoration: underline; }
[data-theme="strokes"] a:visited { color: #551a8b; }
/* folder tabs */
[data-theme="strokes"] .tabs { border-bottom: 2px solid #000; gap: 3px; }
[data-theme="strokes"] .tab {
  background: #d4d0c8; color: #000; border: 2px solid; border-color: #ffffff #808080 #808080 #ffffff;
  border-bottom: none; margin-bottom: 0; padding: 9px 14px;
}
[data-theme="strokes"] .tab.active { background: #ececec; color: #000; border-bottom: none; }
[data-theme="strokes"] #wheel-canvas { background: #fff; border: 2px solid; border-color: #808080 #ffffff #ffffff #808080; }
/* light text where it sits on the cobalt desktop (outside windows) */
[data-theme="strokes"] #screen-landing .hero h1 { color: #fff; }
[data-theme="strokes"] #screen-landing .lead { color: #d7dcff; }
[data-theme="strokes"] #landing-error, [data-theme="strokes"] .error.center { color: #ffc2c2; }
[data-theme="strokes"] .wheel-status { color: #d7dcff; }
[data-theme="strokes"] .wheel-status-note { color: #d7dcff; } /* same spot, now .reveal-note-based — still sits on the cobalt desktop, not a window */
[data-theme="strokes"] .turn-order .small { color: #cfd6ff; }
[data-theme="strokes"] .turn-arrow { color: #aab0ff; }
[data-theme="strokes"] .turn-chip { color: #fff; border-color: #fff; }
[data-theme="strokes"] .turn-chip.current { background: var(--accent); border-color: #fff; color: #fff; }
[data-theme="strokes"] .tabpane > .muted { color: #d7dcff; }
/* Jump to top/bottom — shown only while a club is actually on screen (the
   sibling combinator below, rather than JS show/hide, since #jump-nav sits
   as a plain sibling of #screen-app inside <main> and this already tracks
   whichever of them the routing functions toggle .hidden on) AND the tab
   underneath is actually tall enough to need it (see the ResizeObserver in
   wireStaticUI, which toggles .jump-nav-inactive). Solid background rather
   than translucent — a see-through button kept catching whatever scrolled
   underneath it, which read as more distracting than a plain quiet one. The
   max()/env() pair keeps it clear of the rounded corner and home indicator
   when installed as a standalone app, without changing anything for the
   plain-browser case (env() is just 0 there). */
#jump-nav { display: none; }
#screen-app:not(.hidden) ~ #jump-nav {
  display: flex;
  position: fixed;
  right: max(26px, env(safe-area-inset-right) + 16px);
  bottom: max(26px, env(safe-area-inset-bottom) + 16px);
  z-index: 80;
  /* Same iOS Safari toolbar issue as .modal above: without its own
     compositing layer, a fixed bottom-anchored button can visibly lag at
     the wrong spot for a moment while the toolbar is mid-show/hide. */
  transform: translateZ(0);
}
#screen-app:not(.hidden) ~ #jump-nav.jump-nav-inactive { display: none; }
.jump-btn {
  width: 38px; height: 38px; border-radius: var(--radius); display: flex; align-items: center; justify-content: center;
  background: var(--panel); color: var(--muted); border: 1px solid var(--line);
  font-size: 1rem; line-height: 1; cursor: pointer;
}
.jump-btn:hover { border-color: var(--accent); color: var(--accent); }
[data-theme="strokes"] #screen-app:not(.hidden) ~ #jump-nav { bottom: max(50px, env(safe-area-inset-bottom) + 42px); } /* clears the taskbar below */

/* Web 1.0 taskbar + Start menu (shown only in this theme) */
#taskbar { display: none; }
[data-theme="strokes"] #taskbar {
  display: flex; align-items: center; gap: 8px;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 90; height: 34px; padding: 3px 6px;
  background: #c0c0c0; border-top: 2px solid #ffffff;
}
[data-theme="strokes"] .start-btn {
  font-family: "Pixelify Sans", "Courier New", monospace; font-weight: 700; font-size: 0.85rem;
  background: #d4d0c8; color: #000; border: 2px solid; border-color: #ffffff #808080 #808080 #ffffff;
  padding: 4px 12px; cursor: pointer;
}
[data-theme="strokes"] .start-btn:active { border-color: #808080 #ffffff #ffffff #808080; }
[data-theme="strokes"] .taskbar-clock {
  margin-left: auto; padding: 4px 12px; font-size: 0.82rem; color: #000; font-family: "Times New Roman", Times, serif;
  border: 2px solid; border-color: #808080 #ffffff #ffffff #808080; background: #c0c0c0;
}
.start-menu { display: none; }
[data-theme="strokes"] .start-menu:not(.hidden) {
  display: block; position: fixed; left: 6px; bottom: 38px; z-index: 95; min-width: 168px; padding: 3px;
  background: #c0c0c0; border: 2px solid; border-color: #ffffff #808080 #808080 #ffffff; box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4);
}
[data-theme="strokes"] .start-menu button {
  display: block; width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  padding: 7px 12px; font-family: "Times New Roman", Times, serif; font-size: 0.9rem; color: #000;
}
[data-theme="strokes"] .start-menu button:hover { background: #000080; color: #ffffff; }

/* ===========================================================================
   DARK MODE — a [data-mode="dark"] variant for each theme (toggled separately)
   =========================================================================== */
[data-mode="dark"] .watch-logo, [data-mode="dark"] .rec-poster { background: var(--panel-2); }
/* "Can / can't stream" colours need lighter shades on a dark panel for legible
   contrast. The Web 1.0 windows stay light in dark mode, so they keep the deeper
   originals. */
[data-mode="dark"]:not([data-theme="strokes"]) .movie-avail.ok,
[data-mode="dark"]:not([data-theme="strokes"]) .tmdb-item-avail.ok { color: #5fc98a; }
[data-mode="dark"]:not([data-theme="strokes"]) .movie-avail.warn,
[data-mode="dark"]:not([data-theme="strokes"]) .tmdb-item-avail.warn { color: #e0b75f; }

/* Default — monochrome, inverted */
[data-theme="a24"][data-mode="dark"] {
  --bg: #111111; --bg-soft: #1a1a1a; --panel: #171717; --panel-2: #202020;
  --line: #363636; --line-strong: #606060; --text: #f0f1f3; --muted: #999999;
  --faint: #5f5f5f; --accent: #f0f1f3; --accent-hi: #ffffff; --accent-ink: #111111;
  --danger: #ef6b62; --star-empty: #363636; --topbar-bg: rgba(17, 17, 17, 0.92);
}
[data-theme="a24"][data-mode="dark"] .topbar,
[data-theme="a24"][data-mode="dark"] .film-banner,
[data-theme="a24"][data-mode="dark"] .tabs { border-color: var(--line); }
[data-theme="a24"][data-mode="dark"] .btn { border-color: var(--line-strong); }
[data-theme="a24"][data-mode="dark"] .btn:hover { color: var(--accent); border-color: var(--accent); }
[data-theme="a24"][data-mode="dark"] .btn.primary,
[data-theme="a24"][data-mode="dark"] .btn.primary:hover { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
[data-theme="a24"][data-mode="dark"] a,
[data-theme="a24"][data-mode="dark"] .text-link { color: var(--text); }
[data-theme="a24"][data-mode="dark"] .tab.active { color: var(--text); border-bottom-color: var(--text); }
[data-theme="a24"][data-mode="dark"] #wheel-canvas { border-color: var(--line); }

/* Cinema — dark warm paper, same red/gold ink */
[data-theme="festival"][data-mode="dark"] {
  --bg: #241d15; --bg-soft: #2c241a; --panel: #2c241a; --panel-2: #362b1f;
  --line: #5a4d36; --line-strong: #d8c9a6; --text: #efe4ce; --muted: #b7a986;
  --faint: #8a7c5e; --accent: #e0673f; --accent-hi: #ec7a52; --accent-ink: #f6efdd;
  --danger: #e8745c; --star-empty: #5a4d36; --topbar-bg: rgba(36, 29, 21, 0.95);
}
[data-theme="festival"][data-mode="dark"] body { background: var(--bg); }

/* Web 1.0 — a dark desktop; the Win95 windows stay light by design */
[data-theme="strokes"][data-mode="dark"] { --bg: #05083a; }
[data-theme="strokes"][data-mode="dark"] body {
  background-color: #05083a;
  background-image: repeating-conic-gradient(#05083a 0% 25%, #0a0e5a 0% 50%);
  background-size: 6px 6px;
}

/* ===========================================================================
   Mobile / responsive — sits last so it applies across every theme
   =========================================================================== */

/* Below 1000px, even the now-short always-visible row (brand, club identity,
   Me, Menu) can run out of room, so it's allowed to wrap. Menu itself and the
   dropdown it opens are unconditional now (see the base rules above) — this
   block only holds what's genuinely narrow-width-specific. */
@media (max-width: 1000px) {
  .topbar { flex-wrap: wrap; }
  /* Without this, the brand competes with the club chips for line 1 — a SHORT
     club name fits next to it, bumping the rest down to its own row alone; a
     LONG one doesn't fit next to it at all, so the whole chip group wraps
     together instead. Two different-looking layouts depending on name length.
     Forcing the brand onto its own row always leaves the chip group (name/
     code/invite/Menu) together on row 2, so it's consistent. */
  .brand { flex-basis: 100%; }
  .copy-label { display: none; }
  .group-name { max-width: 50vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Hidden on narrow screens — competing with the club name/code/invite
     chips for the same row was what broke the mobile layout. Not moved into
     the Menu dropdown, so renaming yourself isn't reachable at all on mobile
     right now; flagged in HANDOFF as a follow-up if that turns out to matter. */
  #who-am-i { display: none; }
}

@media (max-width: 640px) {
  .topbar { padding: 10px 14px; gap: 8px 10px; }
  .brand { font-size: 1.15rem; }
  .chip { padding: 6px 10px; font-size: 0.78rem; }

  /* iOS Safari auto-zooms the whole page when focusing any form control with a
     computed font-size under 16px, and doesn't zoom back out — so every select
     must be >=1rem on phones (the two below are 0.85rem on desktop). */
  .rec-seed, .region-row select, .wheel-filter-select, .feedback-select { font-size: 1rem; }

  :root { --main-pad: 14px; }
  #main { padding: 24px var(--main-pad) 64px; }
  .tabs { margin-top: -24px; } /* matches #main's top padding above */
  .card { padding: 17px; }
  .card-collapse { padding: 0; }
  .card-collapse > summary { padding: 17px; }
  .card-collapse-body { padding: 0 17px 17px; }
  .film-card { padding: 24px 16px; }
  [data-theme="a24"] .film-card { padding: 24px 18px; }
  .no-film-subtext:last-child, [data-theme="a24"] .no-film-subtext:last-child { margin-top: 24px; }
  [data-theme="strokes"] .card, [data-theme="strokes"] .modal-box, [data-theme="strokes"] .reset-box { padding-top: 34px; }

  .hero { margin: 12px 0 26px; }
  .hero h1 { font-size: 1.9rem; }
  [data-theme="a24"] .hero { text-align: center; }
  [data-theme="a24"] .hero h1 { font-size: 2.2rem; }
  [data-theme="a24"] .film-card { text-align: center; }
  [data-theme="a24"] .film-meta, [data-theme="a24"] .deadline-row,
  [data-theme="a24"] .round-progress, [data-theme="a24"] .watch-actions, [data-theme="a24"] .cal-row { justify-content: center; }
  [data-theme="a24"] .mn-prompt { align-items: center; text-align: center; }
  [data-theme="a24"] .movie-night { text-align: center; }
  [data-theme="a24"] .mn-prompt .mn-form, [data-theme="a24"] .movie-night .mn-actions, [data-theme="a24"] .movie-night .mn-change-row { justify-content: center; }
  .lead { font-size: 1rem; }
  .film-title { font-size: 1.6rem; }
  [data-theme="a24"] .film-title { font-size: 1.7rem; }
  [data-theme="festival"] .film-title { font-size: 1.9rem; }
  .film-title, .movie-title, .lb-title, .watched-head h3, [data-section="wheel-recs"] h3, .spin-caption { overflow-wrap: anywhere; }

  /* On phones, keep poster + title on the first line and drop the action
     buttons to their own full-width line (right-aligned, as on desktop), so a
     long title never gets crushed into a narrow column next to the buttons. */
  .movie-row { gap: 8px 10px; padding: 12px 4px; }
  .movie-actions { flex-basis: 100%; justify-content: flex-end; }

  /* Four short tabs fit on one line. Never make the main navigation a
     horizontal scroller: it looked like a floating white box in the cinema
     theme, could be dragged sideways, and could leave tabs off-screen. */
  .tabs { gap: 0; padding-inline: 14px; overflow: hidden; }
  .tab { white-space: normal; padding: 10px 4px; font-size: 0.78rem; }
  [data-theme="a24"] .tab::before { display: none; }
  .saved-clubs-list { grid-template-columns: 1fr; }

  .round-progress { gap: 24px; }
  .film-meta { gap: 10px 14px; }
  .deadline-edit { max-width: 100%; }

  .stats-tiles { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .tile { padding: 16px 10px; }
  .tile-value { font-size: 1.7rem; }
  .superlatives { gap: 10px; }
  .people-table th, .people-table td { padding: 7px 3px; font-size: 0.78rem; }
  .people-table th { font-size: 0.54rem; letter-spacing: 0; text-transform: none; }

  .spin-stage { padding: 0 14px; }
  .modal-box { padding: 22px; }
}
