/* Plex-inspired dark theme */
:root {
  --bg: #1a1a1a;
  --surface: #262626;
  --surface-hover: #303030;
  --border: #3a3a3a;
  --text: #eeeeee;
  --text-muted: #999999;
  --accent: #e5a00d;         /* Plex gold */
  --accent-text: #1a1a1a;
  --ok: #27a557;             /* "in library" green */
  --done: #9b6dd6;           /* finished show: watched to the end */
  --busy: #4a9eda;           /* in progress: downloading right now */
  --radius: 8px;
  /* Controls that hold a word: pills, chips, badges, tabs, the search field.
     They were all `border-radius: var(--radius-control)` — a capsule, which forces horizontal
     padding to clear the curve and leaves the label floating in the middle of
     a shape wider than it needs to be. A softened rectangle takes the same
     text in less room and reads as a button rather than as a lozenge.
     Anything genuinely round — an avatar, the progress bar's cap — keeps its
     own 50%/999px and is not on this token. */
  --radius-control: 10px;

  /* ---- spacing scale -------------------------------------------------------
     Every gap on the site comes from this list. It exists because the site had
     twenty different values in it — 3, 5, 7, 9, 11, 13, 18, 22, 28, 30 — each
     one arrived at by nudging a single component until it looked right on the
     page it was on, which is exactly why nothing lined up with anything else.

     Use them in this order of preference and do not invent a value in between:
       --sp-1  4px   inside a control (a pill's padding, an icon's offset)
       --sp-2  8px   between things that belong together (a row's buttons)
       --sp-3  12px  a heading to its own content; between rows
       --sp-4  16px  between blocks inside one section
       --sp-5  24px  the top of a page's content; between sections
       --sp-6  32px  between two sections that are about different things
       --sp-7  40px  the bottom of a page, so the last row is not on the edge
  */
  --sp-0: 2px;   /* hairline: inside a badge or pill, never between blocks */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 40px;

  /* The page's side margin. Every full-width bar and container uses it,
     so the top bar's brand, the tabs, a section heading and a card's left
     edge all sit on one line down the page. */
  --gutter: 24px;

  font-size: 15px;
}

* { box-sizing: border-box; }

/* `hidden` has to actually hide. Any author rule that sets `display` beats the
   browser's own [hidden] { display: none } — author styles win over UA styles
   at equal specificity — so every .foo { display: flex } silently disarms the
   attribute on that element. That has been rediscovered one component at a
   time (the tab menu, the power button, the torrent filters); say it once for
   the whole app instead. */
[hidden] { display: none !important; }

/* The sticky footer needs a body that is at least a viewport tall. Three
   declarations because the first two are fallbacks, not decoration:
   `height: 100%` on <html> is what makes the percentage below resolve at all,
   `100vh` covers browsers without dvh, and `100dvh` is the only one that is
   right on a phone whose toolbars change the viewport. */
html { height: 100%; }

body {
  display: flex;
  flex-direction: column;
  min-height: 100%;
  min-height: 100vh;
  min-height: 100dvh;
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* ---- header ---- */
.topbar {
  background: #121212;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 10;
}
.topbar-inner {
  max-width: 960px;
  margin: 0 auto;
  padding: var(--sp-4) var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.brand h1 { margin: 0; font-size: 1.25rem; font-weight: 700; letter-spacing: .3px; }
/* Two-tone title: first word white, second gold. */
.brand h1 #brand2 { color: var(--accent); }
/* The title is the way home from every page. */
.brand-home { text-decoration: none; color: var(--text); }
.brand-home:hover h1 { opacity: .85; }
.topbar-right { display: flex; align-items: center; gap: var(--sp-3); }
.meta { color: var(--text-muted); font-size: .8rem; text-align: right; }

/* Phones: title (+ Plex pill) on the top row, then the language selector and
   the account (name, role, links) together on ONE row below it. Gaps are
   tightened so a typical name fits without wrapping to a third line. */
@media (max-width: 600px) {
  .topbar-inner { flex-direction: column; align-items: stretch; gap: var(--sp-2); }
  /* Language pills left, account (sign in / log out) pushed to the right. */
  .topbar-right { justify-content: space-between; flex-wrap: wrap; gap: var(--sp-2); }
  .topbar-right .account { gap: var(--sp-2); font-size: .78rem; row-gap: var(--sp-1); }
  .topbar-right .lang { gap: var(--sp-1); }
  .topbar-right .lang button { padding: var(--sp-1) var(--sp-2); }
  .meta { text-align: left; }
}

/* Plex heartbeat pill; hidden until the tracker writes status.json. */
.plex-status {
  font-size: .72rem;
  font-weight: 700;
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--radius-control);
  border: 1px solid;
  white-space: nowrap;
}
.plex-status.up { color: var(--ok); border-color: var(--ok); }
.plex-status.down { color: #e0654f; border-color: #e0654f; }
/* the status dot breathes so the pill feels "alive" */
.plex-status .dot {
  display: inline-block;
  animation: plex-pulse 1.8s ease-in-out infinite;
}
@keyframes plex-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: .3; transform: scale(.82); }
}
@media (prefers-reduced-motion: reduce) {
  .plex-status .dot { animation: none; }
}

/* ---- filter bar ---- */
.filters { background: var(--bg); }
.filters-inner {
  max-width: 960px;
  margin: 0 auto;
  padding: var(--sp-4) var(--gutter) var(--sp-1);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
}
.account { display: flex; align-items: center; gap: var(--sp-3); font-size: .8rem; }
.account a { color: var(--accent); text-decoration: none; font-weight: 700; }
/* Who you are, in the header and on a request row. Gold and bold, and
   nothing about your rights — see topbar.js. */
.who-name { color: var(--accent); font-weight: 700; font-size: 1.02rem; }
.req-by { color: var(--accent); font-weight: 700; font-size: .88rem; }

.banner {
  max-width: 960px;
  margin: var(--sp-3) auto 0;
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid #e0654f;
  border-radius: var(--radius);
  color: #e0654f;
  font-size: .85rem;
}

.lang { display: flex; gap: var(--sp-1); }
.lang button {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-muted);
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--radius-control);
  font: inherit;
  font-size: .75rem;
  font-weight: 700;
  cursor: pointer;
}
.lang button.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
}

.filter-box { margin: var(--sp-1) 0 var(--sp-1); }
.filter-box .search { width: 100%; }

.search {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-control);
  font: inherit;
  font-size: .85rem;
  width: 220px;
}
.search:focus { outline: none; border-color: var(--accent); }

/* ---- second-level page tabs ------------------------------------------------
   A narrower bar under the site's top bar, built by adminnav.js on the admin
   pages and usernav.js on the user pages. The top bar is the same everywhere
   and stays that way; this is a section saying which of its pages you are on. */
.pagenav {
  background: #161616;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 9;
}
.pagenav-inner {
  max-width: 960px;
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: flex;
  gap: var(--sp-1);
  overflow-x: auto;            /* phones: scroll rather than wrap */
  scrollbar-width: none;
}
.pagenav-inner::-webkit-scrollbar { display: none; }
.pagenav a {
  padding: var(--sp-3) var(--sp-4);
  color: var(--text-muted);
  text-decoration: none;
  font-size: .88rem;
  font-weight: 700;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
}
.pagenav a.active {
  color: var(--text);
  border-bottom-color: var(--accent);
}
/* On a pointer screen the label IS the tab: there is room for the whole name
   and an icon beside it would be decoration. Both the icon and the short name
   are in the markup regardless, because the bottom bar on a phone needs them
   and a tab that rewrote itself at a breakpoint would need a resize listener
   to get back. */
.pagenav a .navicon,
.pagenav a .navshort { display: none; }
@media (hover: hover) and (pointer: fine) {
  .pagenav a:hover { color: var(--text); }
}

/* ---- content ---- */
main { max-width: 960px; margin: 0 auto; padding: var(--sp-5) var(--gutter) var(--sp-7); }

.status { color: var(--text-muted); text-align: center; padding: var(--sp-7) 0; }
.status.error { color: #e0654f; }
.quiet {
  color: var(--text-muted);
  font-size: .85rem;
  padding: var(--sp-2) 0 var(--sp-0);
}

/* ---- the landing page ----------------------------------------------------
   Hero (you) on top, then two columns: Up next above My shows on the left,
   requests on the right. One column on a phone. */
/* Every page's content sits in one of these, and they all have the same
   measure and the same breathing room. They had 10px, 18px and 22px tops and
   two different bottoms, which is most of why the pages felt unrelated. */
.public-wrap,
.requests-wrap,
.hero,
.home-main,
.admin-wrap {
  /* `width` as well as `max-width`, because body is a flex column and the
     `margin: 0 auto` below cancels the stretch a flex child would otherwise
     get — leaving these shrink-to-fit. Every one of them happened to contain
     something wide enough to reach 960 anyway, so nothing looked wrong until
     the request list became a grid: `1fr` columns have no intrinsic width to
     push with, so it collapsed to three narrow ones in the middle of a
     1280px screen. This is a no-op for the other four. */
  width: 100%;
  max-width: 960px;
  margin: 0 auto;
  padding: var(--sp-5) var(--gutter) var(--sp-7);
}
/* The request list's own page. It was capped at 760px back when it really was
   one column of rows; it is a poster grid now, and 760px of a 1440px screen
   was most of why it looked like a phone that had been stretched. Same
   measure as every other page. */
.public-wrap .day-nav-title { margin: 0 0 var(--sp-3); font-size: 1.15rem; }

.hero { padding-bottom: var(--sp-5); }
.hero-id { display: flex; align-items: center; gap: var(--sp-4) var(--sp-5); flex-wrap: wrap; }
.hero .p-ident { min-width: 0; flex: 1 1 200px; }
/* Pushed to the right of the name and sized by content — the numbers used to
   be a full-width band underneath, which left this whole side empty. */
.hero .p-stats { flex: 0 1 auto; justify-content: flex-end; }
.hero .p-stat { flex: 0 0 auto; min-width: 84px; }
/* The disclosure is a phone thing — a desktop has the width to just show
   them, so the button is hidden and .open is never consulted. */
.stats-btn { display: none; }

/* ---- stats: hidden for now ------------------------------------------------
   Both the strip and the button that opens it on a phone. The numbers still
   come down in the profile payload and app.js still builds them — this is a
   curtain, not a removal, so bringing them back is deleting these two lines.
   They are counted off My shows today, which measures how diligently somebody
   keeps a list rather than what they have watched. */
.p-stats, .stats-btn { display: none !important; }
@media (max-width: 700px) {
  /* Full width under the name rather than four boxes squeezed beside it. */
  .hero .p-stats { flex-basis: 100%; justify-content: flex-start; }
  .hero .p-stat { flex: 1 1 84px; }
}

/* Only on ?u=: whose page this is, and the way back. */
.viewing {
  max-width: 960px;
  margin: var(--sp-3) auto 0;
  padding: var(--sp-2) var(--gutter);
  background: rgba(229, 160, 13, .10);
  border-left: 3px solid var(--accent);
  color: var(--text-muted);
  font-size: .85rem;
}
.viewing a { color: var(--accent); font-weight: 700; text-decoration: none; }
.viewing a:hover { text-decoration: underline; }
/* Nothing on somebody else's page may be pressed — see startReadOnly(). */
.home :disabled { cursor: not-allowed; }

/* ---- Up next -------------------------------------------------------------
   A strip you scroll sideways, UNDER the shelf it belongs to: My shows is the
   day-to-day page and this is the occasional glance, so the shelf gets the
   top of the screen. Not a grid — "what is coming" is a short list read left
   to right, and wrapping it into rows makes it compete with the shelf above
   for the same shape. */
/* Under the shelf it belongs to. No rule between them: they are two halves of
   the same question — what you have not watched, and what has not been made —
   and a line said they were unrelated. The gap above is the shelf's own
   bottom margin; adding one here too is what made it 56px. */
.upnext { margin: 0; padding-top: 0; }
.upnext-head { display: flex; align-items: baseline; gap: var(--sp-2); }
.upnext-head h2 { margin: 0; font-size: 1.15rem; font-weight: 700; }
.upnext-count {
  background: var(--accent);
  color: var(--accent-text);
  border-radius: var(--radius-control);
  padding: 1px var(--sp-2);
  font-size: .78rem;
  font-weight: 700;
}
/* The strip and its two arrows. position: relative so the arrows can sit
   over the ends of the scroller rather than stealing width from it. */
.upnext-scroller { position: relative; }
/* A real control, not a hint. These were a 55%-opacity gradient washed over
   the poster behind them — almost invisible, and since the scrollbar went
   they are the only thing saying the strip continues. A solid disc with a
   border reads as something to press. */
.upnext-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: rgba(18, 18, 18, .92);
  color: var(--text);
  cursor: pointer;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .45);
}
.upnext-arrow.left { left: calc(var(--sp-1) * -1); }
.upnext-arrow.right { right: calc(var(--sp-1) * -1); }
.upnext-arrow::before {
  content: "\2039";
  font-size: 1.7rem;
  line-height: 1;
  margin-top: -4px;              /* the glyph sits high in its own box */
}
.upnext-arrow.right::before { content: "\203a"; }
@media (hover: hover) and (pointer: fine) {
  .upnext-arrow:hover { border-color: var(--accent); color: var(--accent); }
}

.upnext-strip {
  display: flex;
  /* The same gap as every tile grid on the site — this strip is the one place
     that had its own. */
  gap: var(--sp-4);
  overflow-x: auto;
  padding: var(--sp-3) 0 var(--sp-2);
  scroll-snap-type: x proximity;
  /* No scrollbar: the arrows say there is more, and a bar that only shows up
     mid-drag was never telling anybody anything. Touch keeps the swipe. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.upnext-strip::-webkit-scrollbar { display: none; }
.upnext-card {
  flex: 0 0 140px;
  scroll-snap-align: start;
  /* The day band is pinned over the top of the poster, the way My shows pins
     its episode code — so this is what it is pinned to. */
  position: relative;
}
/* ---- the card cap --------------------------------------------------------
   One band across the top of a poster, and the SAME band on all three pages
   that have posters: which episode is next (My shows), which day it lands (Up
   next), where a request has got to. They sit directly above and below each
   other on the landing page, so three treatments of one idea read as three
   unrelated things.

   The band is always the same solid dark ground. Colour is carried by the
   TEXT — gold for today or coming, green for already there — which is the one
   part that has to differ, and the part that carries the meaning. It used to
   be the band itself that was tinted, at 16% opacity, which next to My shows'
   solid bar made the same-height band look lighter and smaller.

   `.req-list .req-badge` deliberately, not `.req-badge`: the filter pills
   above the list are the same class and are still pills. */
.w-topcode,
.upnext-day,
.req-list .req-badge {
  position: absolute;
  left: 0; right: 0; top: 0;
  padding: var(--sp-2);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .3px;
  /* Two layers, always: a dark base so any poster underneath stays readable,
     and a wash of whatever this cap is reporting on top of it. One mechanism —
     a state sets --cap-ink and --cap-wash, and both the text and the ground
     follow from the same colour. */
  background-color: rgba(0, 0, 0, .62);
  background-image: linear-gradient(var(--cap-wash, transparent),
                                    var(--cap-wash, transparent));
  color: var(--cap-ink, #fff);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .9);
}
/* The states, once, for every cap on the site. rgba rather than the token
   because a wash needs the colour at 20% and a hex cannot be faded in place. */
.w-topcode,
.upnext-day.soon,
.req-list .req-badge.added {
  --cap-ink: var(--accent);
  --cap-wash: rgba(229, 160, 13, .20);
}
.upnext-day.in,
.req-list .req-badge.watched {
  --cap-ink: var(--ok);
  --cap-wash: rgba(39, 165, 87, .20);
}
.req-list .req-badge.downloading {
  --cap-ink: var(--busy);
  --cap-wash: rgba(74, 158, 218, .20);
}
.req-list .req-badge.waiting {
  --cap-ink: var(--done);
  --cap-wash: rgba(155, 109, 214, .20);
}
.req-list .req-badge.rejected {
  --cap-ink: #e0654f;
  --cap-wash: rgba(224, 101, 79, .20);
}
.req-list .req-badge.requested { --cap-ink: var(--text-muted); }
.upnext-day {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  text-transform: uppercase;
  --cap-ink: var(--text-muted);
  white-space: nowrap;
}
/* Coming / there / neither: the states above colour the first two. A show the
   server does not have at all keeps the plain dark band — there is no promise
   to make about it — and today is gold without a wash, because "today" is
   about when, not about whether Plex has it. */
.upnext-day.is-today { --cap-ink: var(--accent); }
/* Inside the band the marker is a word, not a pill: the band is already the
   coloured thing, and a bordered capsule inside a tinted strip is two
   containers saying one thing.
   Written against .upnext-card as well so it outweighs `.tile .t-plex` — the
   two are equally specific otherwise, and that one comes later in the file,
   which left the capsule in place and squeezed the day into an ellipsis. */
.upnext-card .upnext-day .t-plex {
  margin: 0;
  padding: 0;
  border: none;
  border-radius: 0;
  font-size: .68rem;
  color: inherit;
}
.upnext-when { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* The poster comes second, under the band. */

/* The shelves and the strip, full width. This was a two-column grid with the
   request list in a fixed 340px second column; the requests moved to their own
   page and the column went with them, but the track did not — which left a
   third of every screen empty.
   No padding-top: the hero directly above it already ends with one. */
.home-main { padding-top: 0; }

/* tile view: poster, name, episode code, in-Plex state */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: var(--sp-4);
}
/* ---- the tile ------------------------------------------------------------
   ONE tile, on every page that has one: the airing grid, My shows, Up next,
   the request list. A surface panel with a 2:3 poster at the top and whatever
   that page has to say underneath it.

   These were three boxes doing the same job. My shows was the odd one: the
   card itself had no box at all, and its POSTER carried the radius and a 1px
   border, so a shelf of shows read as bordered posters loose on the page
   while the strip below it read as cards. What each page puts under the
   poster still differs — that is the only thing that should. */
.tile,
.w-card,
.req-list li {
  position: relative;
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
  padding-bottom: var(--sp-2);
}
/* Show poster, portrait (2:3); a plain dark placeholder until the image
   loads, or when there is none. */
.ep-thumb,
.w-art,
.req-list img {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 3;
  overflow: hidden;
  object-fit: cover;
  background: #141414;
}
.ep-thumb img, .w-art img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.tile .t-name {
  font-weight: 700;
  font-size: .85rem;
  padding: var(--sp-2) var(--sp-2) 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.tile .t-code { color: var(--accent); font-size: .75rem; font-weight: 700; padding: var(--sp-0) var(--sp-2) var(--sp-2); }
.tile .t-plex {
  display: inline-block;
  margin: 0 var(--sp-3);
  font-size: .65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .5px;
  padding: var(--sp-0) var(--sp-2);
  border-radius: var(--radius-control);
  border: 1px solid var(--border);
  color: var(--text-muted);
}
.tile .t-plex.in { color: var(--ok); border-color: var(--ok); }
/* Coming, not missing: the show is on the server, this episode is not yet. */
.tile .t-plex.soon { color: var(--accent); border-color: var(--accent); }
/* tiles that link into Plex (in-library shows) */
a.tile { color: inherit; text-decoration: none; display: block; }

/* The request list. It was an aside in a second column until it got its own
   page (requests.html); these are the rules it kept. */
/* ONE section heading for the whole site. The admin pages had .section-title
   (30px above, a rule under it), the request page had its own h2 with the
   same rule but no gap above, and the two shelf headings had neither — three
   treatments for the same thing, which is what made the pages look like they
   came from different places. */
.section-title,
.requests-wrap h2,
.upnext-head h2,
.watching-head h2 {
  margin: 0 0 var(--sp-3);
  font-size: 1.15rem;
  font-weight: 700;
}
.section-title,
.requests-wrap h2 {
  border-bottom: 1px solid var(--border);
  padding-bottom: var(--sp-2);
}
/* Between sections. The first on a page does not need it — the container's
   own top padding is already there. */
.section-title { margin-top: var(--sp-6); }
.req-form { display: flex; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.req-form input {
  flex: 1;
  /* An <input> carries an intrinsic min-content width of roughly 20 characters
     unless told otherwise, which is enough to overflow a narrow column. */
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  padding: var(--sp-2) var(--sp-3);
  border-radius: 8px;
  font: inherit;
  font-size: .9rem;
}
.req-form input:focus { outline: none; border-color: var(--accent); }
.req-form button {
  background: var(--accent);
  border: none;
  color: var(--accent-text);
  font: inherit;
  font-weight: 700;
  padding: var(--sp-2) var(--sp-4);
  border-radius: 8px;
  cursor: pointer;
}
/* the requester's own comment — green so it stands out from metadata */
.req-list .r-note { color: var(--ok); font-size: .82rem; font-style: italic; }
/* :empty rather than a reserved line — see .manual-error in admin.css. */
.req-error { color: #e0654f; font-size: .85rem; margin-bottom: var(--sp-2); }
.req-error:empty { display: none; }
/* ---- the request list ----------------------------------------------------
   A poster grid on a screen with room, a list of rows on a phone.

   It was one layout for both: a 34px thumbnail and a line of text, which is
   right on 375px and looks like a phone left running on a desktop. The poster
   is the thing you recognise a title by, so on a wide screen it leads.

   Same DOM either way — requestRow() builds one shape and CSS decides. The
   status badge and the delete sit over the poster here and back on the row on
   a phone, which is why they are positioned rather than re-ordered. */
.req-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  /* The same track and the same gap as .watching-row, so a request card and a
     My shows card are the same object on two pages rather than two sizes of
     nearly-the-same thing. */
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: var(--sp-4);
}

.req-list img {
  display: block;
  width: 100%;
  aspect-ratio: 2/3;
  object-fit: cover;
  background: #141414;
}
/* The same inset as .tile .t-name and .w-name — this is the tile's body, and
   it had its own 12px, which also doubled up with the tile's own bottom
   padding. */
.req-list .r-title { padding: var(--sp-2) var(--sp-2) 0; min-width: 0; }
.req-list .r-title a { color: var(--text); text-decoration: none; }
.req-list .r-meta { color: var(--text-muted); font-size: .78rem; }
.req-badge {
  font-size: .68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .6px;
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--radius-control);
  border: 1px solid var(--border);
  color: var(--text-muted);
  white-space: nowrap;
}
.req-list .r-comment { color: var(--accent); font-size: .78rem; font-style: italic; }
/* The delete ✕ carries .req-badge as well (see requests.js), so its box, its
   radius, its padding and its type all come from there. This is only what a
   BUTTON needs that a span does not. */
.req-list .r-delete {
  background: none;
  font-family: inherit;
  cursor: pointer;
}
/* The last cell of the preview: "Show all (12)", shaped like the tiles it
   completes rather than a line of text under the grid. It carries the tile
   box already (.req-list li), so this is only the button inside it — stretched
   to the full cell so the whole tile is the target. */
.req-list .req-showall { display: flex; padding: 0; }
.req-list .req-showall button {
  flex: 1;
  min-width: 0;
  /* Centred in the whole cell, not sitting at the top of it — the tile is as
     tall as a poster and a label pinned to its ceiling reads as a mistake. */
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--sp-3);
  background: none;
  border: none;
  font: inherit;
  font-weight: 700;
  color: var(--text-muted);
  cursor: pointer;
}

/* The status is a CAP now, not a pill — the shared band at the top of this
   file. What is left here is unmaking the pill it still is everywhere else:
   the outline and the rounded ends belong to the filter chips above the list,
   which are the same class and are still chips. The status colour survives as
   the text colour, which is how Up next's band says the same kind of thing. */
.req-list .req-badge {
  border: none;
  border-radius: 0;
  white-space: normal;
  text-align: center;
}
/* The withdraw ✕ stays a pill, in the card's bottom corner — far from the cap
   so a long status ("tagasi lükatud") has the whole width. It carries
   .req-badge as well, so it has to opt out of every part of the cap above. */
.req-list .r-delete {
  position: absolute;
  top: auto;
  left: auto;
  bottom: var(--sp-2);
  right: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  background: rgba(12, 12, 12, .78);
  backdrop-filter: blur(3px);
  text-shadow: none;
}
.req-badge.requested { border-color: var(--text-muted); }
/* Downloading is its own thing, not a shade of "done": blue says in progress
   where gold says landed, and the two used to be the same pill. */
.req-badge.downloading { border-color: var(--busy); color: var(--busy); }
/* waiting: not out yet — nobody's fault, nothing to chase. Purple keeps it
   clearly apart from "in progress" gold and "done" green. */
.req-badge.waiting { border-color: var(--done); color: var(--done); }
/* Landed, and still waiting for you to play it — the same gold as anything
   else that wants your attention. The ones already played carry .watched
   below and stay green, which is what makes the two tellable apart now they
   share a status. */
.req-badge.added { border-color: var(--accent); color: var(--accent); }
.req-badge.watched { border-color: var(--ok); color: var(--ok); }
.req-badge.rejected { border-color: #e0654f; color: #e0654f; }
/* Profile filter pills: the same badge, but a button. A <button> brings its
   own font and background, so both are reset — and the shorthand takes the
   size with it, hence the repeats. */
.req-badge.f {
  font: inherit;
  font-size: .68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .6px;
  background: none;
  cursor: pointer;
}
/* Selected: a thicker ring in the pill's own colour, with the padding pulled
   in by the same pixel so the row does not shift as filters change. */
.req-badge.f.on { border-width: 2px; padding: var(--sp-0) var(--sp-2); }
/* Dimming the rest is what makes this read as a filter and not as counts that
   happen to be clickable. */
.p-chips.filtering .req-badge.f:not(.on) { opacity: .45; }
/* The whole heading is the hit target — a caret alone is a hard tap on a
   phone, and the count is part of what you are pressing towards. */
/* Logged-in day pager: one day per view, arrows page left/right. */
/* "Updated …" under the airing title, left-aligned. */
.airing-meta { margin: var(--sp-1) 0 0; text-align: left; }

/* Airing header: title (+ updated) left, date-between-arrows pager right.
   No "today" button — the date label itself jumps to today. */
.day-nav {
  max-width: 960px;
  margin: 0 auto var(--sp-4);
  padding: 0 var(--gutter);
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--sp-3);
}
.day-nav-left { justify-self: start; min-width: 0; }
.day-nav-title { margin: 0; font-size: 1.15rem; font-weight: 700; }
.day-picker {
  justify-self: end;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.day-nav button {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  font: inherit;
  font-size: 1.1rem;
  padding: var(--sp-2) var(--sp-4);
  border-radius: 8px;
  cursor: pointer;
}
.day-nav button:disabled { opacity: .35; cursor: default; }
/* The date is a button (jumps to today) styled to read like a plain label.
   Its width is FIXED, not min-width: "Today" and "08.08.26" are different
   lengths, and letting the button size to its content moved everything to its
   left every time you paged a day — on a phone that meant the title beside it
   flipping between two and three wrapped rows. Sized to hold the longest
   thing it shows (the date), at both breakpoints. */
.day-nav .day-label {
  width: 92px;
  flex: none;
  padding: var(--sp-1) var(--sp-2);
  text-align: center;
  background: none;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--accent);
  font-size: .92rem;
  font-weight: 700;
  letter-spacing: .5px;
  white-space: nowrap;
}
@media (hover: hover) and (pointer: fine) {
  .day-nav .day-label:hover { border-color: var(--border); }
}
@media (max-width: 600px) {
  .day-nav { gap: var(--sp-2); }
  /* Deliberately NOT narrowed on mobile — a label that resizes with its
     content is the whole bug this is fixing, and it bites hardest here. */

  /* Tighter gaps so the two primary shelves + "⋯" hold one row on a phone.
     BOTH shelves: this addressed #watching-tabs by id, so the films shelf —
     same markup, same class, different id — kept the desktop padding and the
     two rows of tabs came out different sizes. The padding is the touch
     block's job at the end of the file; only the gap belongs here. */
  .watching-tabs { gap: var(--sp-1); }

  .search { width: 100%; }

  /* Poster tiles roughly half-size on phones -> more per row. */
  .tile-grid { grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: var(--sp-2); }
  .tile { padding-bottom: var(--sp-2); }
  /* Sized for a 124px card. These numbers were written when a phone tile was
     84px wide and they stayed behind when the cards grew — a show's name at
     .68rem on a card half again as wide reads as fine print. */
  .tile .t-name { font-size: .82rem; padding: var(--sp-2) var(--sp-2) 0; }
  .tile .t-code { font-size: .78rem; padding: 1px var(--sp-2) var(--sp-2); }
  .tile .t-plex { font-size: .52rem; margin: 0 var(--sp-2); padding: 1px var(--sp-2); }

}
/* Hover only with a real pointer: a touch screen keeps :hover on the last
   tapped spot, so a re-rendered element there inherits stale colour. */
@media (hover: hover) {
  .account a:hover { text-decoration: underline; }
  .lang button:hover { color: var(--text); border-color: var(--text-muted); }
  a.tile:hover { background: var(--surface-hover); }
  a.tile:hover .t-name { color: var(--accent); }
  .req-list .r-title a:hover { color: var(--accent); }
  .req-list .req-showall button:hover { color: var(--text); }
  .req-list .r-delete:hover { color: #e0654f; border-color: #e0654f; }
  .p-more:hover { color: var(--text); border-color: var(--text-muted); }
  .search-results .sr-title a:hover { color: var(--accent); }
  .day-nav button:hover:not(:disabled) { background: var(--surface-hover); border-color: var(--text-muted); }
}

/* ---- My Library shelves on the main page -------------------------------- */
/* Lives inside the left column, so it shares the airing list's width and
   never runs under the requests panel. */
/* Breathing room above the buttons so they don't sit flush against the bar.
   Padding, not margin: margin collapses up through the column and leaves no
   gap at all. */
/* No padding-top: the switch directly above already ends with a gap, and the
   two together were making the shelf sit 28px clear of its own control.
   The bottom margin is the ONLY gap down to Up next — the strip adds nothing
   of its own, or the two stack into 56px of nothing between two halves of the
   same thought. */
.watching { margin: 0 0 var(--sp-5); padding-top: 0; }
.watching-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);            /* Add-a-show and Filter sit together next to the heading */
  flex-wrap: wrap;
}
.watching-head h2 { margin: 0; font-size: 1.15rem; font-weight: 700; }

/* The library switch: shows | search | films. Fixed sides — pressing a name
   selects that half rather than swapping the two, so neither ever moves and
   you can aim for one without reading it. Both sides are the same element in
   the same size the heading used to be; only the colour says which is live. */
.lib-switch {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* The gap above comes from the hero's padding; this is only the gap down to
     the shelf it switches between. */
  margin: 0 0 var(--sp-3);
  flex-wrap: wrap;
}
/* The two shared buttons, immediately after the pair of names. They used to
   be pushed to the far end with margin-left: auto, which on a 960px page put
   them most of a screen away from the thing they act on. */
.lib-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.lib-side {
  /* Shrinks before the row wraps rather than pushing the actions off the end. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: none;
  border: none;
  padding: var(--sp-1) 0;
  font: inherit;
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--text-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  -webkit-tap-highlight-color: transparent;
}
.lib-side:hover { color: var(--text); }
.lib-side.active { color: var(--text); border-bottom-color: var(--accent); }

/* Phones: the names stay on their sides and the search drops below them,
   rather than all three squeezing onto one row. */
@media (max-width: 520px) {
  .lib-switch { gap: var(--sp-2); }
  .lib-side { font-size: .95rem; }
  .lib-actions { gap: var(--sp-2); }
  /* No font-size here: shrinking these to .7rem was how a touch screen got a
     44px button with a tiny word in the middle of it. The touch block at the
     end of the file sizes them, and this one is MORE specific, so it would
     have quietly won. */
}
.w-add {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-muted);
  font: inherit;
  font-size: .85rem;
  font-weight: 700;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-control);
  cursor: pointer;
}
.w-add:hover { color: var(--text); border-color: var(--accent); }

.watching-tabs { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-3); }
.watching-tabs button {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-muted);
  font: inherit;
  font-size: .85rem;
  font-weight: 700;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-control);
  cursor: pointer;
}
.watching-tabs button.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
}
/* How many shows are on that shelf. Dimmed and slightly smaller: it travels
   with the label without competing with it for the pill's attention. */
.tab-count { margin-left: 6px; opacity: .65; font-size: .92em; }
/* On the gold active pill, opacity would grey it out — lighten instead. */
.watching-tabs button.active .tab-count { opacity: .75; }
/* In the "⋯" menu the labels are left-aligned rows, so the count goes to the
   far right where a count belongs. */
.tab-more-menu button .tab-count { float: right; margin-left: 10px; }

/* "⋯" overflow: the secondary shelves live in a small dropdown so the tab
   row stays one line even on a phone. */
.tab-more { position: relative; display: inline-flex; }
.tab-more-btn { line-height: 1; }
.tab-more-menu {
  position: absolute;
  top: calc(100% + 6px);
  /* ⋯ sits at the right end of the row, so open leftward to stay on-screen. */
  right: 0;
  z-index: 20;
  min-width: 170px;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, .45);
}
/* Menu items read as a list, not pills. */
/* .watching-tabs is in the selector only to out-specify the touch block at the
   end of the file, which centres every shelf pill — these are the same
   elements (layoutTabs moves them here, it does not clone them), so an equally
   specific rule would lose on source order and the labels would stay centred
   in the dropdown. */
.watching-tabs .tab-more-menu button {
  width: 100%;
  text-align: left;
  border-color: transparent;
  border-radius: 6px;
  /* These are the SAME elements as the inline shelf pills — layoutTabs moves
     them in here rather than cloning — so they inherit the pill's centring
     and `text-align` alone cannot undo it. In a dropdown the labels have to
     start on the same left edge or the odd one out reads as a mistake. */
  justify-content: flex-start;
}
.watching-tabs .tab-more-menu button:not(.active):hover { border-color: var(--border); }

.w-addform { margin-top: var(--sp-3); }
.w-addrow { display: flex; gap: var(--sp-2); }
.w-addrow input {
  flex: 1;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  padding: var(--sp-2) var(--sp-3);
  border-radius: 6px;
  font: inherit;
  font-size: 16px;      /* 16px stops iOS zooming the page on focus */
}
.w-addrow input:focus { outline: none; border-color: var(--accent); }
.w-addrow button, .w-addbtn {
  background: var(--accent);
  border: none;
  color: var(--accent-text);
  font: inherit;
  font-weight: 700;
  padding: var(--sp-2) var(--sp-4);
  border-radius: 6px;
  cursor: pointer;
}
.w-error { color: #e0654f; font-size: .8rem; padding: var(--sp-1) 0; }
.w-error:empty { display: none; }
.w-error.ok { color: var(--ok); }

/* TV Time import panel — same shape as the add-a-show form, plus an
   explanation and a way to say no. */
.w-importform .w-importhint {
  color: var(--text-muted);
  font-size: .82rem;
  line-height: 1.45;
  padding-bottom: var(--sp-2);
  max-width: 640px;
}
.w-importform .w-addrow { flex-wrap: wrap; align-items: center; }
.w-importform input[type=file] {
  flex: 1 1 220px;
  min-width: 0;
  color: var(--text-muted);
  font: inherit;
  font-size: .82rem;
  padding: 0;
  border: none;
  background: none;
}
.w-importform input[type=file]::file-selector-button {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  font: inherit;
  font-size: .82rem;
  padding: var(--sp-2) var(--sp-3);
  border-radius: 6px;
  margin-right: 10px;
  cursor: pointer;
}
/* Opting out is a quiet action, not a call to action. */
.w-importform button.w-skip {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-muted);
}
@media (hover: hover) and (pointer: fine) {
  .w-importform button.w-skip:hover { color: var(--text); border-color: var(--text-muted); }
}
@media (max-width: 600px) {
  .w-importform .w-addrow { flex-direction: column; align-items: stretch; }
  /* column flex would stretch the file input to fill the row otherwise */
  .w-importform input[type=file] { flex: 0 0 auto; }
  .w-importform .w-addrow button { width: 100%; }
}
.w-empty { color: var(--text-muted); font-size: .85rem; padding: var(--sp-4) 0; }

/* Cards sit on the same grid as the airing tiles, so both read as one page. */
.watching-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: var(--sp-4);
  padding: var(--sp-3) 0 var(--sp-1);
  align-items: start;
}
.w-card { min-width: 0; }
/* The tile that just changed: a brief gold ring + a nudge, so the eye is
   drawn to the one episode code / progress bar that moved. */
.w-card.just-changed {
  animation: w-changed .9s ease-out;
}
.w-card.just-changed .w-nextcode {
  animation: w-code-in .45s cubic-bezier(.2,.7,.3,1);
}
@keyframes w-changed {
  0%   { box-shadow: 0 0 0 0 rgba(229, 160, 13, .85); }
  35%  { box-shadow: 0 0 0 3px rgba(229, 160, 13, .85); }
  100% { box-shadow: 0 0 0 0 rgba(229, 160, 13, 0); }
}
/* the new "next up" code slides up into place as the old one is replaced */
@keyframes w-code-in {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .w-card.just-changed,
  .w-card.just-changed .w-nextcode { animation: none; }
}
/* Which card's list is open — ringed card + gold title. On the card now that
   the poster has no border of its own. */
.w-card.open {
  box-shadow: 0 0 0 2px var(--accent);
}
.w-card.open .w-name { color: var(--accent); }
/* Only what the shared poster above does not already say: the cap and the
   tick are pinned inside this, not inside the card. */
.w-art { position: relative; }

/* Poster controls. Hover effects are gated behind a real pointer: on a
   phone the :hover state sticks to whatever was tapped and then gets reused
   by the element that re-renders in its place, which looked like the tick
   jumping to the next show. */
.w-tick {
  border: none;
  font: inherit;
  line-height: 1;
  cursor: pointer;
  background: rgba(0, 0, 0, .72);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  -webkit-tap-highlight-color: transparent;
}
/* The arrow that used to sit up here is gone: the poster IS the expander now
   (see showCard), which turns the biggest thing on the card into its own
   target instead of hanging a 28px button off it. */
.w-art { cursor: pointer; }
/* The next episode, across the top of the poster on a band of its own. It
   used to share the bottom bar with the tick; at three cards to a row there
   was not enough width for both, and the code is a label while the tick is a
   control — they do not belong on the same line anyway.

   A flat translucent band, not a gradient. A fade has no bottom edge, so the
   text sat in a wash that thinned out under it and the code read as part of
   the artwork; a band is a strip the label lives on. */
/* The cap over the poster. Sized to match .upnext-day exactly — the two
   strips sit one above the other on the landing page, and a difference of a
   few pixels between them reads as a mistake rather than as a distinction. */
/* Everything else about this cap — where it sits, its size, its ground — is
   the shared rule at the top of the file. This is only what My shows' one
   needs that the others do not. */
.w-topcode { pointer-events: none; }   /* the poster under it opens the seasons */
.w-topcode:empty { display: none; }

.w-nextbar {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-2);
  background: linear-gradient(to top, rgba(0, 0, 0, .82), rgba(0, 0, 0, 0));
}
.w-nextcode {
  /* Sized AND coloured by the band around it (.w-topcode), not on its own, so
     this cap and Up next's are the same object. */
  font-weight: 700;
  color: inherit;
  letter-spacing: .3px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .9);
}
.w-tick { width: 34px; height: 34px; font-size: 1.05rem; margin-left: auto; }
.w-tick.done { background: var(--accent); color: var(--accent-text); cursor: default; }
/* instant feedback on tap, before the server round trip returns */
.w-tick:active:not(:disabled) { transform: scale(.86); }
.w-tick.ticking { animation: w-tick-pop .4s ease-out; }
@keyframes w-tick-pop {
  0%   { transform: scale(1); background: var(--accent); color: var(--accent-text); }
  45%  { transform: scale(1.28); background: var(--accent); color: var(--accent-text); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .w-tick.ticking { animation: none; }
}
.w-tick:disabled { opacity: .55; cursor: default; }
@media (hover: hover) and (pointer: fine) {
  a.w-name:hover { color: var(--accent); }
  .w-tick:hover:not(:disabled):not(.done) {
    background: var(--accent);
    color: var(--accent-text);
  }
}

/* A card's title. It became a LINK to IMDb, so it has to be told not to look
   like one — the browser default was drawing every show name blue and
   underlined against a dark poster. */
a.w-name { color: inherit; text-decoration: none; }
.w-name {
  /* Inside the panel now, so it is inset like .tile .t-name rather than
     sitting flush against the page. */
  padding: var(--sp-2) var(--sp-2) 0;
  font-weight: 700;
  font-size: .85rem;
  line-height: 1.25;
  /* Two lines maximum, so every card in a row is the same height. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Film cards reuse the show card wholesale — same grid, same poster, same
   title, same round buttons on the same strip. Only two differences: the tick
   stays pressable once green (a film can be un-watched, where a show's next
   episode simply moves on), and the left slot holds remove instead of an
   episode code. */
.w-tick.seen { background: var(--ok); color: #fff; }
.w-tick.w-drop { margin-left: 0; margin-right: auto; }
.w-tick.w-drop:hover:not(:disabled) { background: #e0654f; color: #fff; }

/* The import offer inside an empty shelf, with its opt-out. Louder than the
   heading button, because on an empty library it is the only thing there is
   to do — and the opt-out beside it stays quiet, because declining should be
   available without being suggested. */
.w-empty-import {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin-top: var(--sp-3);
}
.w-empty-go {
  background: none;
  border: 1px solid var(--accent);
  border-radius: var(--radius-control);
  color: var(--accent);
  font: inherit;
  font-size: .82rem;
  font-weight: 700;
  padding: var(--sp-2) var(--sp-4);
  cursor: pointer;
}
.w-empty-go:hover { background: var(--accent); color: var(--accent-text); }
.w-empty-no {
  background: none;
  border: none;
  color: var(--text-muted);
  font: inherit;
  font-size: .8rem;
  padding: var(--sp-2) var(--sp-1);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.w-empty-no:hover { color: var(--text); }

/* Progress: gold in progress, green caught-up on a running show, purple
   when the show has ended too. */
.w-bar {
  /* Inset to match the title above it — both sit inside the tile's panel now,
     and a bar running edge to edge under an inset name reads as two different
     containers. */
  margin: var(--sp-1) var(--sp-2) 0;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
  overflow: hidden;
}
.w-bar span { display: block; height: 100%; background: var(--accent); }
.w-bar span.complete { background: var(--ok); }
.w-bar span.finished { background: var(--done); }

/* Expanded seasons; spans the row so the grid isn't torn apart. */
.w-seasons {
  grid-column: 1 / -1;
  margin: var(--sp-1) 0 var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: .85rem;
}
.w-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-2);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--border);
}
.w-panel-title { font-weight: 700; font-size: .95rem; flex: 1; min-width: 0; }
/* Drop a show from your list — destructive, so it reads red and confirms. */
.w-panel-remove {
  flex: none;
  background: transparent;
  border: 1px solid #e0654f;
  color: #e0654f;
  font: inherit;
  font-size: .72rem;
  font-weight: 700;
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--radius-control);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.w-panel-remove:disabled { opacity: .5; cursor: default; }
@media (hover: hover) and (pointer: fine) {
  .w-panel-remove:hover { background: #e0654f; color: #fff; }
}
.w-panel-close {
  flex: none;
  width: 30px; height: 30px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: .8rem;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) and (pointer: fine) {
  .w-panel-close:hover { color: var(--accent); border-color: var(--accent); }
}
/* One collapsible block per season, so it is always obvious which episodes
   belong to which season. */
.w-season {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  background: none;
  border: none;
  border-radius: 6px;
  color: var(--accent);
  font: inherit;
  font-weight: 700;
  font-size: .8rem;
  text-transform: uppercase;
  letter-spacing: .5px;
  padding: var(--sp-2) var(--sp-2);
  margin-top: var(--sp-1);
  cursor: pointer;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.w-season.open { background: rgba(229, 160, 13, .10); }
.w-caret { width: 18px; flex: none; font-size: 1.15rem; line-height: 1; text-align: center; }
.w-season-name { flex: 1; }
.w-season-count {
  color: var(--text-muted);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}
@media (hover: hover) and (pointer: fine) {
  .w-season:hover { background: rgba(229, 160, 13, .16); }
}

.w-eps { list-style: none; margin: 0 0 var(--sp-2); padding: 0 0 0 var(--sp-5); }
.w-eps li {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border-bottom: 1px solid rgba(255, 255, 255, .05);
  color: var(--text-muted);
}
.w-eps li:last-child { border-bottom: none; }
.w-eps li.seen { color: var(--text); }
.w-eps li.unaired { opacity: .5; }
.w-eplabel { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.w-epcode { color: var(--accent); font-weight: 700; font-size: .78rem; }
/* Air date on an episode that hasn't aired yet. */
.w-epdate { margin-left: 8px; font-size: .74rem; color: var(--text-muted); }
/* The panel title links to IMDb. */
a.w-panel-title { color: inherit; text-decoration: none; }
a.w-panel-title:hover { color: var(--accent); text-decoration: underline; }
/* Same round tick as the poster, one size down. */
.w-eptick { position: static; width: 28px; height: 28px; font-size: .85rem; flex: none; }

@media (max-width: 620px) {
  .watching-row { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
  .w-eps { padding-left: var(--sp-2); }
}

/* Show-search hits (add form). */
.search-results { list-style: none; padding: 0; margin: var(--sp-2) 0 var(--sp-3); }
.search-results li {
  display: flex; align-items: center; gap: var(--sp-3); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-1);
}
.search-results img {
  width: 30px; aspect-ratio: 2/3; object-fit: cover;
  border-radius: 4px; background: #141414;
}
/* min-width:0 so a long title shrinks instead of pushing the row wider, and
   overflow-wrap so a release-style name with no spaces breaks rather than
   running off the edge. */
.search-results .sr-title { flex: 1; min-width: 0; overflow-wrap: anywhere; }
/* The title links out to IMDb (or TMDB) — styled to read as the title it
   already was, not as a blue link in the middle of a result row. */
.search-results .sr-title a { color: inherit; text-decoration: none; }
.search-results .sr-meta { color: var(--text-muted); font-size: .78rem; }
/* The row's own action ("Add" / "Request"). Styled here so every search box —
   My shows, requests, the admin panel's tracking list — gets the same row
   without each one restating it. */
.search-results button {
  background: var(--accent); border: none; color: var(--accent-text);
  font: inherit; font-weight: 700; padding: var(--sp-2) var(--sp-4); border-radius: 6px;
  cursor: pointer; flex: none;
}
.search-results button:disabled { opacity: .55; cursor: default; }

/* The request comment, opened inside the row you pressed Request on. It used
   to be one box above the results: by the time you had scrolled to the title
   you wanted, it was off-screen and people simply forgot it existed. Wrapping
   the row is what lets it take a full line of its own under the title. */
.search-results li.sr-row { flex-wrap: wrap; }
/* Wraps: on a narrow screen the two buttons drop under the field rather than
   squeezing it to nothing or forcing the row wider than the card. */
.sr-noterow { display: flex; flex-wrap: wrap; gap: var(--sp-2); width: 100%; margin-top: var(--sp-2); }
.sr-note {
  /* 140px basis with a 120px floor: below that the buttons wrap onto their own
     line rather than the field collapsing to a slot too narrow to read what
     you typed. 120 + the row's padding still fits the narrowest phone. */
  flex: 1 1 140px; min-width: 120px;
  background: var(--bg); border: 1px solid var(--accent); color: var(--text);
  padding: var(--sp-2) var(--sp-3); border-radius: 6px; font: inherit;
  /* 16px UNCONDITIONALLY, not inside a phone media query. iOS zooms the page
     whenever a focused field is under 16px, and it does that at any viewport
     width — so a rule guarded by max-width:600px still let this zoom in
     landscape or on a big phone. There is no width at which a smaller comment
     box is worth the page lurching, so the condition is gone. */
  font-size: 16px;
}
.sr-note:focus { outline: none; }
.search-results .sr-cancel {
  background: none; border: 1px solid var(--border); color: var(--text-muted);
  font-weight: 400; padding: var(--sp-2) var(--sp-3);
}

/* How far the downloads behind a request have got. Thin and quiet: it is a
   background fact about someone else's work, not a control. Green at 100%,
   where it then stays — see request_download in storage.py. */
.r-progress { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-1); }
.r-bar {
  flex: 1;
  min-width: 0;
  height: 3px;
  border-radius: 999px;
  background: var(--border);
  overflow: hidden;
}
.r-bar i { display: block; height: 100%; background: var(--accent); }
.r-bar.done i { background: var(--ok); }
.r-pct {
  flex: none;
  font-size: .7rem;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;   /* the bar must not jog as it counts */
}
.r-pct.done { color: var(--ok); }

/* ---- profile pieces --------------------------------------------------------
   These were a page of their own. They are the landing page's hero and the
   filter pills above its request list now — the separate profile page is
   gone, because two views of one person is how the two drift apart. Written
   to be read by the person themselves, never as an administrative screen,
   which is why nothing had to change when they moved. */
.p-avatar {
  flex: none;
  width: 64px; height: 64px;
  border-radius: 50%;
  background: var(--surface);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.6rem; font-weight: 700; color: var(--text-muted);
  overflow: hidden;
}
.p-avatar img { width: 100%; height: 100%; object-fit: cover; }
.p-ident { min-width: 0; }
.p-ident h1 { margin: 0; font-size: 1.4rem; font-weight: 700; }
.p-tags { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-1); }
.p-tag {
  font-size: .68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .6px; padding: 1px var(--sp-2); border-radius: var(--radius-control);
  border: 1px solid var(--border); color: var(--text-muted);
}
.p-joined { margin-top: var(--sp-1); color: var(--text-muted); font-size: .8rem; }

.p-stats { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.p-stat {
  flex: 1 1 120px;
  background: var(--surface); border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-0);
}
.p-stat strong { font-size: 1.25rem; font-weight: 700; color: var(--accent); }
.p-stat span { color: var(--text-muted); font-size: .76rem; }

.p-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-bottom: var(--sp-3); }
/* How much of what they asked for they actually played, in three bands:
   under 50% red, 50-75% amber, above 75% green. The amber is Plex gold rather
   than a fourth colour — the palette already means "in progress" with it.
   Neutral is the fallback for a score that somehow matched no band. */
.p-score { border-color: var(--text-muted); color: var(--text); }
/* The score under your name is a LINK to the list it describes, so it needs
   telling not to look like one — the browser default was drawing it blue and
   underlined. Same thing that caught the card titles. */
a.req-badge { text-decoration: none; }
.p-score.low { border-color: #e0654f; color: #e0654f; }
.p-score.mid { border-color: var(--accent); color: var(--accent); }
.p-score.good { border-color: var(--ok); color: var(--ok); }
/* Per-request, under the title. The unplayed state is deliberately dim: it is
   information, not a complaint. */
/* Opens the rest of the request list. Quiet: the list is the content, this is
   only a way to see more of it. */
.p-more {
  margin-top: var(--sp-2);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  color: var(--text-muted);
  font: inherit;
  font-size: .78rem;
  padding: var(--sp-1) var(--sp-4);
  cursor: pointer;
}

/* The notification switch, in the profile header. A real switch rather than a
   button: it is a state, not an action, and the whole row is the hit target. */
.p-notify {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-2); cursor: pointer;
  font-size: .8rem; color: var(--text-muted);
}
.p-notify input {
  appearance: none; -webkit-appearance: none;
  position: relative; flex: none; margin: 0;
  /* Sized against the controls beside it. At 34x20 it read as a stray
     detail next to 36px pills rather than as this row's switch. */
  width: 48px; height: 28px; border-radius: 999px;
  border: 1px solid var(--border); background: none; cursor: pointer;
}
.p-notify input::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--text-muted);
  transition: left .15s ease, background .15s ease;
}
.p-notify input:checked { border-color: var(--ok); }
.p-notify input:checked::after { left: 23px; background: var(--ok); }
.p-notify input:disabled { opacity: .6; cursor: default; }
/* Set by topbar.js when the browser refuses — the reason is in the title. */
.p-notify input.bad { border-color: #e0654f; }
.p-notify input.bad::after { background: #e0654f; }

/* Requests: movie disc/digital release date, or a show's first air date. */
.r-dvd { color: var(--accent); font-size: .76rem; font-weight: 700; }

/* Attribution footer — same on every page, built by topbar.js.
   Deliberately quiet: it is a legal requirement, not a design element. */
/* Whatever a page's main block is called, it takes the slack. This and the
   footer's auto top margin do the same job on purpose: the margin alone is
   one `margin-top` override away from quietly not working, which is how the
   footer ended up mid-screen on /admin/airing the first time. */
main, .home { flex: 1 0 auto; }

.attribution {
  max-width: 1100px;
  /* auto, not a fixed top margin: it is the other half of the push (see the
     rule above and `body`). */
  margin: auto auto var(--sp-5);
  flex: none;
  padding: var(--sp-4) var(--sp-5) 0;
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: .76rem;
  line-height: 1.5;
}
.attribution p { margin: 0 0 var(--sp-1); }

/* iOS zooms the page when a focused field is under 16px.
   Each selector here deliberately OUT-SPECIFIES the component rule that sets
   the smaller size, rather than relying on being last in the file: .sr-note
   is declared at .85rem further up, and when both were plain .sr-note the one
   later in the file won — which is how the search-result comment box ended up
   zooming on every tap. Specificity does not care where a rule sits. */
@media (max-width: 600px) {
  .search, #req-input { font-size: 16px; }
}

/* ===========================================================================
   TOUCH
   ===========================================================================
   Everything below is about a finger rather than a mouse. It is deliberately
   one block at the end of the file, not adjustments scattered through it: the
   phone rules that already existed were per-component nudges, which is how
   the site ended up with a 25x34 "mark watched" button as its main verb.

   Two different questions, two different queries:

     (pointer: coarse)   what is touching the screen. Sizes.
     (max-width: 700px)  how much room there is. Layout.

   A tablet with a mouse gets the sizes and not the layout; a narrow desktop
   window gets the layout and not the sizes. Conflating them is why a phone
   breakpoint used to be the only handle for both.
   ======================================================================== */

/* ---- the safe area -------------------------------------------------------
   viewport-fit=cover (every page's viewport meta) lets the page draw into the
   rounded corners and under the status bar, which is what makes an installed
   PWA look like an app rather than a web page in a box. It also makes it OUR
   job to keep content out from under the hardware, which is what these insets
   are. Needed regardless of the cover: apple-mobile-web-app-status-bar-style
   is black-translucent, so iOS already drew the top bar under the clock. */
.topbar { padding-top: env(safe-area-inset-top); }
.topbar-inner,
.pagenav-inner {
  padding-left: max(var(--gutter), env(safe-area-inset-left));
  padding-right: max(var(--gutter), env(safe-area-inset-right));
}

@media (pointer: coarse) {
  /* No scrollbars anywhere. A touch screen has no pointer to grab one with,
     so a bar is a strip of chrome that only ever appears mid-gesture and then
     takes width from the content. Every sideways-scrolling component here was
     already hiding its own one at a time; this is that decision made once. */
  * { scrollbar-width: none; -ms-overflow-style: none; }
  *::-webkit-scrollbar { width: 0; height: 0; display: none; }

  /* ---- the 44px floor ----------------------------------------------------
     Apple's minimum, and the one number that fixes most of a touch UI. Set
     once, against every control on the site, so it cannot drift back: the
     audit before this found twelve different sub-44px targets, the smallest
     13x14.

     min-height rather than height so a control that is legitimately taller
     (a textarea, a wrapped row) keeps its size. */
  button, select, input:not([type=checkbox]):not([type=file]), .lib-side,
  .w-addbtn, .dl-chip-go, .dl-chip-x,
  .account a, .topbar a {
    min-height: 44px;
  }
  /* .w-add, .p-more, .req-badge.f and the shelf tabs are deliberately NOT in
     that list — see the rule below. They are pills whose type sets their
     height, and forcing 44 on top of it is what left a third of each capsule
     empty under the word. */
  /* Buttons that are a row of text want their height as padding, so the label
     stays centred; the flex ones already centre their own content. */
  .account a, .topbar a, .lib-side { display: inline-flex; align-items: center; }

  /* 5 + 7 — a pill grows to 44px by gaining padding, and .75rem text inside
     44px of button is a small word marooned in a big shape. If the control is
     finger-sized then so is its label: the type goes up with the box.
     The two library actions also take a shared width, so "Add" and "Filter"
     are the same button with different words rather than two sizes of one
     idea. */
  .watching-tabs button, .w-add, .p-more, .req-badge.f {
    /* NOT min-height: these size themselves from their own type, and forcing
       44 on top of that is what put a third of each capsule below the text.
       They clear 38px, which is a target on any thumb. */
    min-height: 0;
    padding: var(--sp-2) var(--sp-3);
    font-size: .95rem;
  }
  .lib-side { font-size: .95rem; }
  /* Tappable, but not shouting: these are counts, and five of them at full
     size needed three rows to themselves. */
  .req-badge.f { font-size: .82rem; padding: var(--sp-2) var(--sp-3); }
  /* The ✕ needs the floor lifted and NOTHING else: it is a .req-badge, so its
     box comes from that rule. Adding it to the selector above gave it the
     tab's padding and type instead, and `padding: inherit` to undo that takes
     the LI's padding, not the badge's — both make it a different shape from
     the badge it sits beside, which is the whole thing being fixed. */
  .r-delete { min-height: 0; min-width: 0; }
  /* NOT the library's two actions. Add and Filter are things you reach for
     occasionally; at 44px and 88px wide they took a row of their own away
     from the shelf, which is the thing the page is for. They keep a 36px
     target — below the floor, deliberately, and the only place on the site
     that is. */
  .lib-actions .w-add { min-height: 36px; font-size: .78rem; padding: 0 var(--sp-3); }
  .watching-tabs button { display: inline-flex; align-items: center; justify-content: center; }

  /* The round controls on a poster — the tick, the expander, the drop. These
     are the main verbs of My shows and were the smallest things on the card.
     flex: none, or the row squashes them out of round: at three cards per row
     the tick was coming out 27 wide and 44 tall. */
  .w-tick { width: 44px; height: 44px; font-size: 1.15rem; flex: none; }
  .w-tick.w-eptick { width: 40px; height: 40px; }

  /* A film's strip is two round buttons and nothing else, so it stays one
     row — the remove on the left, the tick on the right, in line with each
     other. Without this the shows' rule applied to it and knocked the ✕ out
     of line. */
  .w-nextbar-pair { justify-content: space-between; }

  /* A checkbox cannot grow without looking wrong, so the LABEL around it
     becomes the target instead. NOT .p-notify's, which is not a box at all —
     it is a 34x20 track with a knob drawn by ::after, and squaring it left
     the knob sitting outside its own outline. */
  .account label, .u-feature, .p-notify { min-height: 44px; display: inline-flex; align-items: center; }
  input[type=checkbox]:not(.p-notify input) { width: 22px; height: 22px; }

  /* Small print stays small; its hit area does not. */
  /* NOT .r-delete: it is a .req-badge now and takes that pill's size, the
     same deliberate exception as the library's Add and Filter. Forcing 44
     square on it is exactly what made it tower over the badge beside it. */
  .req-dl-off, .tab-more-btn, .w-panel-close, .w-panel-remove {
    min-width: 44px; min-height: 44px;
  }

  /* A name that opens a page is a destination, not body copy. The links
     INSIDE a row (a title linking to IMDb) are deliberately left alone: the
     row is the target and a 44px line of text would just be a gap. */
  .u-name { min-height: 44px; display: inline-flex; align-items: center; }

  /* Nothing on a touch screen should react to a hover it cannot have. The
     existing @media (hover: hover) blocks handle the colours; this handles
     the one thing they cannot — a tap that lands on a scrolling gesture. */
  .tile, .w-card, .upnext-card { -webkit-tap-highlight-color: transparent; }
}

@media (max-width: 700px) {
  /* A narrower margin. 24px each side takes 48 of a 375px screen — an eighth
     of the width — which a desktop can afford and a phone cannot. Everything
     that indents from the edge reads this token, so this one line moves the
     lot: the header, the tab bar, every page container and the sheet. */
  :root { --gutter: 14px; }

  /* ---- navigation moves to the thumb -------------------------------------
     The tab bar is the same element and the same markup as on a desktop —
     usernav.js and adminnav.js know nothing about this. It simply stops being
     a strip under the header and becomes a bar along the bottom, because the
     top 130px of a phone is the hardest place on the screen to reach and the
     bottom is where the hand already is.

     The admin panel has five tabs and they did not fit: three were reachable
     and Settings and Mover were off the right-hand edge, behind a horizontal
     scroll nobody would guess was there. As a bottom bar they share the width
     evenly and all five are visible at once. */
  /* The height of the bar, and the room the page leaves for it — ONE value,
     because they have to agree and the page cannot see the bar to measure it:
     56px of tabs, plus a strip underneath them.

     That strip is a literal 20px, not env(safe-area-inset-bottom). The page
     no longer declares viewport-fit=cover — it lays out inside the safe areas
     instead, which is what finally put this bar on the bottom of the screen —
     and the insets all read 0 as a result. So there is nothing left to ask
     the phone for, and the clearance has to be stated.

     20 rather than the 34 iOS reserves: 34 left a visibly empty band under
     the tabs, and the whole point of a bottom bar is that it is AT the
     bottom. 20 is enough to keep the outer two tabs off a rounded corner and
     the labels clear of the home indicator, without the band. */
  :root {
    --tabbar-clear: 20px;
    --tabbar: calc(56px + var(--tabbar-clear));
  }
  .pagenav {
    position: fixed;
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 20;
    background: #101010;
    border-top: 1px solid var(--border);
    border-bottom: none;
    padding-bottom: var(--tabbar-clear);
    /* Its full height from the FIRST frame, before it has any tabs in it.
       The 56px used to live only on the links (.pagenav a), so until the nav
       scripts had built them the bar was nothing but its own safe-area
       padding — 35px instead of 90 — and the 55px the page had correctly
       reserved for it showed as a band of background under the footer. It
       closed itself when the tabs arrived, which looked like the footer
       dropping into place on the first scroll. */
    min-height: var(--tabbar);
  }
  .pagenav-inner {
    padding: 0;
    gap: 0;
    overflow: visible;          /* five tabs share the width; nothing scrolls */
  }
  .pagenav a {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    padding: var(--sp-2) var(--sp-1) var(--sp-1);
    min-height: 56px;
    font-size: .62rem;
    letter-spacing: .2px;
    text-transform: uppercase;
    border-bottom: none;
    /* The active tab is marked by colour and a filled icon, not by an
       underline: an underline at the bottom of the screen reads as a border
       on the bar rather than as a state. */
  }
  .pagenav a .navicon { display: block; width: 22px; height: 22px; flex: none; }
  .pagenav a.active { color: var(--accent); }
  /* Long names do not fit five-up. The short ones come from the nav scripts. */
  .pagenav a .navlong { display: none; }
  .pagenav a .navshort { display: block; }

  /* The bar floats over the page, so the page has to end above it — exactly
     its own height and no more. The gap between the last line of the
     attribution and the bar is the footer's own bottom margin, which every
     page already has.
     This was stated twice, 250 lines apart in this same query, the second
     time as `56px + var(--sp-5) + inset`. Two clearances for one bar left the
     footer 47px short of the bottom on a page with nothing to scroll. */
  body { padding-bottom: var(--tabbar); }
  /* ...and the footer ends exactly there, against the bar. Its own bottom
     margin — which is the right thing on a desktop, where the window edge is
     below it — put 24px of nothing between the last line of the attribution
     and the tab bar, on a screen where the bar already provides the edge. */
  .attribution { margin-bottom: 0; }

  /* ---- the header stops being half the screen ----------------------------
     It was two rows and 88px: brand + heartbeat, then language, name, role,
     Admin and Log out — six small targets in the least reachable place. The
     account row moved into a sheet behind the avatar (topbar.js), leaving one
     row of about 44px. */
  .topbar-inner { flex-direction: row; align-items: center; gap: var(--sp-3); padding-top: var(--sp-2); padding-bottom: var(--sp-2); }
  .topbar-right { flex: none; }
  .brand h1 { font-size: 1.05rem; }

  /* ---- the hero stops pushing everything below the fold ------------------
     Avatar, name and score on one line; the five stat cards become one
     scrollable row instead of a 3+2 block. Measured before: the first show
     card began 481px down, 0.59 of a screen. */
  .hero { padding-top: var(--sp-4); padding-bottom: var(--sp-4); }
  .hero-id { gap: var(--sp-3); }
  /* Avatar, name and the Stats button share line one; the stats themselves
     take line two when opened.
     `flex: 1 1 0` with min-width: 0 is what keeps the button ON that line —
     the base rule gives the name block a 200px BASIS, and 48 + 200 + a 90px
     button does not fit 327px, so the button was wrapping to a line of its
     own and sitting out on the right with nothing beside it. */
  .hero .p-ident { flex: 1 1 0; min-width: 0; }
  .stats-btn { flex: none; }
  .p-avatar { width: 48px; height: 48px; font-size: 1.2rem; }
  .p-ident h1 { font-size: 1.15rem; }
  .p-joined { display: none; }          /* the one fact nobody scrolls for */
  /* Behind the button, and gone entirely until it is pressed — a row of
     five cards under the name was pushing the shelf half a screen down.
     Once it IS pressed you get all five at once, wrapped onto as many rows as
     they need. They used to be one nowrap row you scrolled sideways, which
     hid three of them behind a gesture inside a panel that was already behind
     a button — two things to discover before you can read a number. */
  .hero .p-stats {
    display: none;
    flex-wrap: wrap;
    margin-top: var(--sp-3);
  }
  .hero .p-stats.open { display: flex; }
  .hero .p-stat { flex: 1 1 auto; min-width: 96px; }
  .stats-btn {
    display: inline-flex;
    align-items: center;
    flex: none;
    margin-left: auto;
    min-height: 44px;
    padding: 0 var(--sp-3);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
    color: var(--text-muted);
    font: inherit;
    font-size: .92rem;
    font-weight: 700;
    cursor: pointer;
  }
  .stats-btn[aria-expanded="true"] { color: var(--accent); border-color: var(--accent); }
  .hero .p-stats::-webkit-scrollbar { display: none; }
  .hero .p-stat { flex: 0 0 auto; min-width: 0; padding: var(--sp-2) var(--sp-3); }
  .hero .p-stat strong { font-size: 1.05rem; }

  .watching-row { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-2); }
  /* No arrows on a phone: you swipe, and a card cut off by the screen edge
     says there is more far better than a button does. The arrows exist for a
     mouse, which has no swipe.
     Not a whole number of cards across, deliberately — at exactly three the
     strip ends flush with the edge and looks like everything there is. */
  .upnext-arrow { display: none; }
  .upnext-strip { gap: var(--sp-2); }
  .upnext-card { flex: 0 0 38%; }

  /* The shelf's own bottom margin is the gap now, on every screen — the phone
     override that used to be needed here has moved into the base rules. */

  /* The attribution is required by TMDB's terms, so it stays on the page —
     but it does not need to be 106px of it above a fixed bar.
     margin-top stays AUTO: that is what pushes it to the bottom of a short
     page, and setting a value here is what left it floating mid-screen on
     /admin/airing with 321px of nothing under it. */
  .attribution { font-size: .68rem; }
}

/* ---- the account sheet ---------------------------------------------------
   Built by topbar.js on a phone, behind the avatar button. Bottom-anchored
   because that is where the thumb is and where every sheet on a phone comes
   from; rows are full width and 52px, which is the opposite of the 28x14
   links they replace. */
.account-btn { display: none; }          /* pointer screens keep the row */

.sheet {
  border: none;
  padding: 0;
  margin: 0 0 0 auto;
  width: 100%;
  max-width: 100%;
  background: transparent;
  color: var(--text);
}
.sheet::backdrop { background: rgba(0, 0, 0, .55); }
.sheet[open] {
  position: fixed;
  inset: auto 0 0 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--gutter) calc(var(--sp-3) + env(safe-area-inset-bottom));
  background: var(--surface);
  border-top: 1px solid var(--border);
  border-radius: 16px 16px 0 0;
}
.sheet-head {
  color: var(--accent);
  font-weight: 700;
  font-size: 1.05rem;
  padding: var(--sp-2) 0;
}
.sheet-rows { display: flex; flex-direction: column; gap: var(--sp-2); }
.sheet-row,
.sheet-close {
  display: flex;
  align-items: center;
  min-height: 52px;
  padding: 0 var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
}
.sheet-row.danger { color: #e0654f; }
.sheet-close { justify-content: center; color: var(--text-muted); }
/* Language as one row split in two, not two small pills. */
.sheet-lang { display: flex; gap: var(--sp-2); }
.sheet-lang button {
  flex: 1;
  min-height: 52px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text-muted);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.sheet-lang button.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
}

@media (max-width: 700px) {
  /* The row's job moves to the sheet, and the avatar takes its place. */
  .topbar-right .lang,
  .topbar-right .account { display: none; }
  .account-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Square, or the border-radius draws an oval. Both numbers are the touch
       floor: raising only the height is what made it an egg. */
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--accent);
    font: inherit;
    font-weight: 700;
    cursor: pointer;
  }
}

@media (max-width: 700px) {
  /* The library switch becomes two rows.
     Four full-size controls — two shelf names and two actions — need about
     380px on one line and there are 327. They were overlapping each other by
     five or six pixels, which is what "Minu filmid" being clipped actually
     was. So: the two shelves share row one, the two actions share row two,
     and every one of them is a real target instead of four squeezed ones. */
  .lib-switch { row-gap: var(--sp-2); }
  .lib-side { flex: 1 1 0; justify-content: center; }
  .lib-actions { flex: 1 1 100%; order: 1; justify-content: stretch; }
  .lib-actions .w-add { flex: 1 1 0; min-width: 0; }
}

@media (max-width: 700px) {
  /* A request row stacks. It was one nowrap line — poster, title, status
     badge, delete — so on 327px the title got whatever was left after the
     other three, and "Parasite (2019)" came out three words tall with its
     note in a column two syllables wide. Poster and title take the first
     line; the badge and the delete take the second. */
  /* Poster and title on the first line, status and delete on the second —
     ALWAYS, not "when the badge happens to be too long to fit". That was the
     old rule and it gave one list two layouts, decided by the length of a
     word in the current language.
     The break is forced with a zero-height spacer rather than a width floor:
     flex-basis: 100% on the title would wrap the title itself below the
     poster and leave it alone on a line. */
  /* Back to a row. The wide screen's poster grid is two cards across on
     375px and scrolls for ever, and a thumbnail says enough when the title is
     right beside it. Same DOM either way, so this only has to put back what
     the grid changed: the badge and the delete come out of the poster's
     corners and back onto the row. */
  .req-list { display: block; }
  .req-list li {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-4);
    margin-bottom: var(--sp-1);
  }
  .req-list img { width: 34px; border-radius: 4px; }
  /* Back to pills. The wide screen turns the status into a cap across the top
     of a poster, which means unmaking the pill — outline, rounded ends, its
     own width. On a row there is no poster to cap, so all of that comes back
     and the two of them sit on the line as they always did. */
  .req-list .req-badge,
  .req-list .r-delete {
    position: static;
    background: none;
    backdrop-filter: none;
    /* Width and style only — NOT the shorthand, which would reset the colour
       and paint every status the same grey. Each one's own border-color is
       further down the file and wins. */
    border-width: 1px;
    border-style: solid;
    border-radius: var(--radius-control);
    padding: var(--sp-1) var(--sp-2);
    text-shadow: none;
    color: var(--cap-ink, var(--text-muted));
  }
  /* The ✕ takes its outline from its own colour, so it is the same weight of
     object as the pills beside it rather than a borderless glyph. */
  .req-list .r-delete { border-color: currentColor; }
  .req-list .req-badge { text-align: center; width: auto; }
  /* Status on the title's line, hard right. It spent a while stacked onto a
     line of its own, which was solving a squeeze the badges caused when they
     were capsule-shaped and wider; at the current size the title keeps enough
     room. The badge never shrinks — a truncated status is worse than a
     wrapped title — so the title is the one that gives. */
  .req-list .r-title { flex: 1 1 0; min-width: 0; padding: 0; }
  /* Sentence case on a phone, not SHOUTING WITH LETTER-SPACING. Uppercase
     plus .5px of tracking makes "tagasi lükatud" 119px of a 327px row, and
     everything it takes comes out of the title beside it — which was down to
     79px, four words tall. The same word cased normally is a third narrower
     and no harder to read.
     EVERY badge, not just the ones in the list: the filter pills above it are
     the same component, and leaving them shouting while the rows below them
     did not made one thing look like two. */
  .req-badge, .req-badge.f { text-transform: none; letter-spacing: 0; }
  .req-list .req-badge { flex: none; margin-left: auto; font-size: .78rem; }
}

@media (max-width: 700px) {
  /* The switch is one row again: the two shelf names, and two compact
     actions beside them. They were forced onto a second row by being 88px
     wide and 44 tall, which is more prominence than "Add" and "Filter"
     deserve on the page whose job is the shelf underneath. */
  .lib-actions { flex: 0 0 auto; order: 0; justify-content: center; }
  .lib-actions .w-add { flex: 0 0 auto; }
  .lib-side { flex: 1 1 0; }
}
