/* Dustplex — dark, Plex-adjacent theme. No external fonts or frameworks. */

:root {
  --bg: #0d0f12;
  --bg-raised: #15181d;
  --bg-hover: #1c2027;
  --border: #262b33;
  --border-strong: #363d47;
  --text: #e8eaed;
  --text-dim: #9aa2ae;
  --text-faint: #6b7480;
  --accent: #e5a00d;
  --accent-hover: #f2b134;
  --sonarr: #35c5f4;
  --radarr: #f5a623;
  --danger: #e5484d;
  --success: #46a758;
  --radius: 10px;
  --radius-lg: 16px;
  --shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
  --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

[hidden] {
  display: none !important;
}

button {
  font-family: inherit;
  font-size: inherit;
}

/* Buttons ------------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 9px 16px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-raised);
  color: var(--text);
  cursor: pointer;
  font-weight: 500;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
  white-space: nowrap;
}

.btn:hover:not(:disabled) {
  background: var(--bg-hover);
  border-color: var(--text-faint);
}

.btn:active:not(:disabled) {
  transform: translateY(1px);
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #17130a;
  font-weight: 600;
}

.btn--primary:hover:not(:disabled) {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

.btn--plex {
  background: var(--accent);
  border-color: var(--accent);
  color: #17130a;
  font-weight: 600;
  font-size: 16px;
  padding: 13px 28px;
  width: 100%;
}

.btn--plex:hover:not(:disabled) {
  background: var(--accent-hover);
}

.btn--ghost {
  background: transparent;
  border-color: var(--border);
  color: var(--text-dim);
}

.btn--ghost:hover:not(:disabled) {
  color: var(--text);
  background: var(--bg-hover);
}

.btn--small {
  padding: 6px 12px;
  font-size: 13px;
}

.btn--icon {
  padding: 6px 12px;
  font-size: 20px;
  line-height: 1;
}

.btn--danger {
  border-color: rgba(229, 72, 77, 0.4);
  color: #ff8a8d;
}

.btn--danger:hover:not(:disabled) {
  background: rgba(229, 72, 77, 0.12);
  border-color: var(--danger);
}

/* Login -------------------------------------------------------------------- */

.login {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: calc(24px + env(safe-area-inset-top, 0px)) calc(24px + env(safe-area-inset-right, 0px))
    calc(24px + env(safe-area-inset-bottom, 0px)) calc(24px + env(safe-area-inset-left, 0px));
  background: radial-gradient(1200px 600px at 50% -10%, #1d2129 0%, var(--bg) 60%);
}

.login__card {
  width: 100%;
  max-width: 400px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 40px 32px;
  text-align: center;
  box-shadow: var(--shadow);
}

.login__mark {
  width: 56px;
  height: 56px;
  margin: 0 auto 20px;
  display: grid;
  place-items: center;
  background: var(--accent);
  color: #17130a;
  border-radius: 14px;
  font-size: 24px;
}

.login__title {
  margin: 0 0 8px;
  font-size: 26px;
  letter-spacing: -0.02em;
}

.login__tagline {
  margin: 0 0 28px;
  color: var(--text-dim);
  font-size: 14px;
}

.login__hint {
  margin: 18px 0 0;
  font-size: 13px;
  color: var(--text-faint);
  min-height: 20px;
}

.login__hint.is-error {
  color: #ff8a8d;
}

/*
 * The sign-in card centres its contents, which is right for a title and a
 * button but wrong for a form: labels belong above their field, on the left.
 */
.login__card .form {
  text-align: left;
  margin-top: 16px;
}

.login__card .form__actions {
  margin-top: 4px;
}

.login__card .form .btn {
  width: 100%;
}

#admin-toggle {
  margin-top: 16px;
}

.login__card--wide {
  max-width: 560px;
}

.profiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(108px, 1fr));
  gap: 14px;
  margin-top: 8px;
}

.profile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  padding: 16px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.profile:hover {
  background: var(--bg-hover);
  border-color: var(--accent);
}

.profile__avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--bg-hover);
  border: 1px solid var(--border-strong);
}

.profile__initial {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--bg-hover);
  border: 1px solid var(--border-strong);
  font-size: 24px;
  font-weight: 600;
  color: var(--text-dim);
}

.profile__name {
  font-size: 13.5px;
  font-weight: 500;
  text-align: center;
  word-break: break-word;
}

.profile__tag {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-faint);
}

/* Top bar ------------------------------------------------------------------ */

.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/*
 * The version, and nothing else.
 *
 * `margin-top: auto` in the column flex parent pins it to the bottom of short
 * pages instead of leaving it floating mid-screen. It carries the same
 * safe-area insets as .main so it clears the home indicator.
 */
.appfoot {
  margin-top: auto;
  padding: 18px 20px calc(18px + env(safe-area-inset-bottom, 0px));
  text-align: center;
}

.appfoot__version {
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--text-faint);
  /* Present when looked for, unobtrusive when not. */
  opacity: 0.65;
}

/*
 * The bar clears the iOS status bar on its own.
 *
 * The page is served with `viewport-fit=cover` and a translucent status bar,
 * which is what lets the app's own background run to the top of the screen —
 * but it also means the layout viewport starts *underneath* the clock, signal
 * and battery. Without the inset the brand and the avatar sit right on top of
 * them.
 *
 * The base padding is held in custom properties so the breakpoints below can
 * change it without every one of them having to restate the env() arithmetic
 * and risk dropping it. `env()` resolves to 0px on anything without a notch,
 * so this costs desktop nothing.
 */
.topbar {
  --topbar-pad-y: 12px;
  --topbar-pad-x: 20px;

  display: flex;
  align-items: center;
  gap: 20px;
  padding: calc(var(--topbar-pad-y) + env(safe-area-inset-top, 0px))
    calc(var(--topbar-pad-x) + env(safe-area-inset-right, 0px)) var(--topbar-pad-y)
    calc(var(--topbar-pad-x) + env(safe-area-inset-left, 0px));
  border-bottom: 1px solid var(--border);
  background: rgba(13, 15, 18, 0.92);
  backdrop-filter: blur(12px);
  position: sticky;
  top: 0;
  z-index: 20;
  flex-wrap: wrap;
  transform: translateY(0);
  /* The arriving half of the pair; the leaving half is with .is-hidden. */
  transition: transform 0.2s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.topbar__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.topbar__mark {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  background: var(--accent);
  color: #17130a;
  border-radius: 8px;
  font-size: 13px;
}

.tabs {
  display: flex;
  gap: 4px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 3px;
}

.tab {
  padding: 7px 16px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  font-weight: 500;
  transition: background 0.15s ease, color 0.15s ease;
}

.tab:hover {
  color: var(--text);
}

.tab[aria-selected='true'] {
  background: var(--bg-hover);
  color: var(--text);
}

.topbar__user {
  position: relative;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  border: 0;
  background: none;
  padding: 0;
  color: inherit;
  font-family: inherit;
  cursor: pointer;
}

.topbar__username {
  color: var(--text-dim);
  font-size: 14px;
}

/* Who is watching, right now ------------------------------------------------ */

/*
 * A little television hung off the top-right corner of the wordmark, like a
 * stamp on it rather than a thing of its own in the bar.
 *
 * Positioned against the brand, whose right edge is the end of the name — so
 * it stays pinned to the last letter however the name is rendered, and it costs
 * the header no width because it is out of flow. Overhanging by less than the
 * bar's own gap, so it cannot touch what comes next.
 */
.topbar__brand {
  position: relative;
}

.tvbadge {
  position: absolute;
  top: -9px;
  right: -13px;
  padding: 0;
  border: 0;
  background: none;
  line-height: 0;
  cursor: pointer;
}

.tvbadge__set {
  width: 21px;
  height: 21px;
  overflow: visible;
}

.tvbadge__body {
  fill: var(--accent);
}

/*
 * The screen is darker than the header behind it and outlined by the body
 * around it, which is what keeps a 21px television from reading as a smudge.
 */
.tvbadge__screen {
  fill: #14100a;
}

.tvbadge__aerial {
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  fill: none;
}

.tvbadge__count {
  fill: var(--accent);
  font-family: inherit;
  font-size: 9.4px;
  font-weight: 800;
  letter-spacing: -0.02em;
}

/* Three characters in the space of two. */
.tvbadge__count.is-wide {
  font-size: 7px;
  letter-spacing: -0.05em;
}

.tvbadge:hover .tvbadge__body {
  fill: #f0b52f;
}

/*
 * The bubble under it. Fixed, and placed from the badge's measured position
 * rather than from the badge itself — the badge is a button, and a button
 * cannot hold a block of text as a child.
 */
.tvbadge__pop {
  position: fixed;
  z-index: 40;
  transform: translateX(-50%);
  max-width: min(280px, calc(100vw - 24px));
  padding: 9px 13px;
  border-radius: var(--radius);
  border: 1px solid rgba(229, 160, 13, 0.4);
  background: rgba(17, 20, 25, 0.97);
  backdrop-filter: blur(10px);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.55);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  text-align: center;
}

.avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--border-strong);
}

/*
 * Bottom inset clears the home indicator; the side insets matter in landscape,
 * where the notch eats into one edge of the content column. Held in custom
 * properties for the same reason as the top bar: the breakpoints restate the
 * padding, and this way they cannot silently drop the insets doing it.
 */
.main {
  --main-pad-top: 20px;
  --main-pad-x: 20px;
  --main-pad-bottom: 20px;

  flex: 1;
  width: 100%;
  max-width: 1240px;
  margin: 0 auto;
  padding: var(--main-pad-top) calc(var(--main-pad-x) + env(safe-area-inset-right, 0px))
    calc(var(--main-pad-bottom) + env(safe-area-inset-bottom, 0px))
    calc(var(--main-pad-x) + env(safe-area-inset-left, 0px));
}

/* Toolbar ------------------------------------------------------------------ */

.toolbar {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-bottom: 16px;
}

.toolbar__nav {
  display: flex;
  align-items: center;
  gap: 8px;
}

.toolbar__title {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  min-width: 190px;
  letter-spacing: -0.01em;
}

/* "This week" under the date range, so which week is never in doubt. */
.toolbar__title[data-relative]::after {
  content: attr(data-relative);
  display: block;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-top: 2px;
}

/* Week strip --------------------------------------------------------------- */

.weekbar {
  margin-bottom: 20px;
}

.weekbar__days {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 6px;
}

.weekday {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 10px 2px 8px;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-raised);
  color: var(--text-dim);
  font-family: inherit;
  cursor: pointer;
}

.weekday:hover {
  background: var(--bg-hover);
}

.weekday__name {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.weekday__num {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
  line-height: 1;
}

/* Reserved whether or not there are dots, so the row does not jog as you
   move between weeks. */
.weekday__dots {
  display: flex;
  gap: 3px;
  height: 6px;
  align-items: center;
}

/*
 * Two different ideas, told apart by fill against outline.
 *
 * Today is a fact about the calendar and must be unmistakable at a glance, so
 * it is a filled block. The open day is a choice the reader just made, so it is
 * outlined. They are usually the same day — the week opens on today — and when
 * they are not, both still read for what they are.
 */
.weekday.is-today {
  background: var(--accent);
  border-color: var(--accent);
}

.weekday.is-today .weekday__name {
  color: rgba(0, 0, 0, 0.62);
}

.weekday.is-today .weekday__num {
  color: #14100a;
}

.weekday.is-selected {
  border-color: var(--accent);
  background: rgba(229, 160, 13, 0.14);
  color: var(--text);
}

/* Today and open at once: keep the filled block and ring it, rather than
   letting the outline state repaint the one thing that must not move. */
.weekday.is-today.is-selected {
  background: var(--accent);
  box-shadow: 0 0 0 2px rgba(229, 160, 13, 0.4);
}

.weekday.is-today.is-selected .weekday__name {
  color: rgba(0, 0, 0, 0.62);
}

.weekday.is-today.is-selected .weekday__num {
  color: #14100a;
}

.agenda__day.is-today .agenda__date strong {
  color: var(--accent);
}

.agenda__today {
  color: var(--accent);
  font-weight: 600;
}

.toolbar__filters {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.toolbar__spacer {
  flex: 1;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 13px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg-raised);
  color: var(--text-dim);
  font-size: 13px;
  cursor: pointer;
  user-select: none;
}

.chip input {
  accent-color: var(--accent);
  margin: 0;
  cursor: pointer;
}

/* Coloured per feed, so the swatch is set inline from the feed's colour. */
.chip__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
}

/*
 * The Following chip sits apart from the per-feed chips: those pick which feeds
 * to draw, this one narrows to one person's shows.
 */
.chip--following {
  border-color: var(--border-strong);
}

.chip--following.is-empty {
  opacity: 0.65;
}

.chip__count {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--bg);
  background: var(--accent);
  border-radius: 999px;
  padding: 0 6px;
  min-width: 17px;
  text-align: center;
}

.chip__count:empty {
  display: none;
}

.follow-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  max-height: 320px;
  overflow-y: auto;
}

.follow-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}

.follow-row:last-child {
  border-bottom: 0;
}

.follow-row--empty {
  color: var(--text-faint);
  font-size: 13px;
  line-height: 1.6;
  display: block;
}

.follow-row__title {
  flex: 1;
}

.filter-chips {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.chip:has(input:checked) {
  color: var(--text);
  border-color: var(--border-strong);
}

.segmented {
  display: inline-flex;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 3px;
  gap: 2px;
}

.segmented--wide {
  margin-bottom: 18px;
}

.segmented__btn {
  padding: 6px 14px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 13px;
}

.segmented__btn:hover {
  color: var(--text);
}

.segmented__btn.is-active {
  background: var(--bg-hover);
  color: var(--text);
}

/* Banner ------------------------------------------------------------------- */

.banner {
  margin: 0 0 16px;
  padding: 11px 15px;
  border: 1px solid rgba(229, 160, 13, 0.3);
  background: rgba(229, 160, 13, 0.08);
  border-radius: var(--radius);
  color: #f0c765;
  font-size: 13.5px;
}

.banner.is-error {
  border-color: rgba(229, 72, 77, 0.35);
  background: rgba(229, 72, 77, 0.08);
  color: #ff9598;
}

/* Calendar grid ------------------------------------------------------------ */

.calendar {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--bg-raised);
}

.calendar__weekdays,
.calendar__grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}

.calendar__weekdays {
  border-bottom: 1px solid var(--border);
  background: #12151a;
}

.calendar__weekday {
  padding: 9px 10px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  text-align: center;
}

.day {
  min-height: 116px;
  padding: 7px;
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow: hidden;
}

.day:nth-child(7n) {
  border-right: 0;
}

.day--outside {
  background: rgba(0, 0, 0, 0.22);
}

.day--outside .day__number {
  color: var(--text-faint);
  opacity: 0.55;
}

.day--today .day__number {
  background: var(--accent);
  color: #17130a;
  font-weight: 700;
}

.day__number {
  align-self: flex-start;
  min-width: 23px;
  height: 23px;
  padding: 0 6px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  font-size: 12.5px;
  color: var(--text-dim);
}

.event {
  display: block;
  width: 100%;
  text-align: left;
  padding: 4px 7px;
  border: 0;
  border-left: 3px solid var(--border-strong);
  border-radius: 5px;
  background: var(--bg-hover);
  color: var(--text);
  font-size: 12px;
  line-height: 1.35;
  cursor: pointer;
  overflow: hidden;
  transition: background 0.12s ease;
}

.event:hover {
  background: #262c35;
}

.event__title {
  display: block;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.event__meta {
  display: block;
  color: var(--text-faint);
  font-size: 11px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.day__more {
  border: 0;
  background: none;
  color: var(--text-faint);
  font-size: 11.5px;
  padding: 2px 7px;
  cursor: pointer;
  text-align: left;
}

.day__more:hover {
  color: var(--text);
}

/* Agenda ------------------------------------------------------------------- */

.agenda {
  display: flex;
  flex-direction: column;
  gap: 22px;
}

.agenda__day {
  display: grid;
  grid-template-columns: 110px 1fr;
  gap: 18px;
  align-items: start;
}

.agenda__date {
  position: sticky;
  top: 76px;
  color: var(--text-dim);
  font-size: 13px;
  padding-top: 6px;
}

.agenda__date strong {
  display: block;
  color: var(--text);
  font-size: 19px;
  line-height: 1.2;
}

.agenda__items {
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.agenda__item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  padding: 11px 14px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-raised);
  color: var(--text);
  cursor: pointer;
}

.agenda__item:hover {
  background: var(--bg-hover);
}

.agenda__item-main {
  flex: 1;
  min-width: 0;
}

.agenda__item-title {
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.agenda__item-sub {
  color: var(--text-dim);
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.agenda__item-time {
  color: var(--text-faint);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  flex: none;
}

/* Search + grid ------------------------------------------------------------ */

.search {
  display: flex;
  gap: 10px;
  margin-bottom: 16px;
}

.search__field {
  position: relative;
  flex: 1;
  display: flex;
  /* An <input> has an intrinsic width from its `size` attribute, and a flex
     item will not shrink below its intrinsic width unless told to. Without
     this the field holds the row open and pushes the Search button off the
     right edge of a phone. */
  min-width: 0;
}

/*
 * Our own clear button rather than the one type="search" is supposed to give
 * us: WebKit's ::-webkit-search-cancel-button does not render on iOS Safari at
 * all, which is exactly where a clear button is worth most. It is hidden on
 * the platforms that do draw one, so nobody gets two.
 */
.search__input::-webkit-search-cancel-button {
  display: none;
}

.search__clear {
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  /* 34px square: comfortably past the 24px minimum for a touch target, while
     still sitting inside the field. */
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-faint);
  font-size: 21px;
  line-height: 1;
  cursor: pointer;
}

.search__clear:hover,
.search__clear:focus-visible {
  color: var(--text);
  background: var(--bg-raised);
}

.search__input {
  flex: 1;
  min-width: 0;
  /* Right padding leaves room for the clear button. */
  padding: 11px 44px 11px 15px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-raised);
  color: var(--text);
  font-size: 15px;
  font-family: inherit;
}

.search__input:focus {
  outline: none;
  border-color: var(--accent);
}

.search__input::placeholder {
  color: var(--text-faint);
}

/*
 * Never let iOS zoom the page when a field takes focus.
 *
 * Safari zooms in on any focused text control whose font is under 16px, and
 * then leaves the page scaled — which reads as the layout jumping. 16px is the
 * exact threshold, so this is the whole fix.
 *
 * Scoped to coarse pointers so the smaller type stays on desktop, and written
 * against the element types rather than our classes so a field added later
 * cannot reintroduce it.
 */
@media (pointer: coarse) {
  input[type='text'],
  input[type='search'],
  input[type='password'],
  input[type='email'],
  input[type='number'],
  input[type='url'],
  input:not([type]),
  textarea,
  select {
    font-size: 16px;
  }
}

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
  gap: 18px;
}

.card {
  border: 0;
  padding: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.card__poster {
  position: relative;
  aspect-ratio: 2 / 3;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg-raised);
  border: 1px solid var(--border);
}

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

.card:hover .card__poster {
  border-color: var(--border-strong);
}

.card__placeholder {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  color: var(--text-faint);
  font-size: 12px;
  padding: 12px;
  text-align: center;
}

.card__badge {
  position: absolute;
  top: 7px;
  left: 7px;
  padding: 3px 8px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  backdrop-filter: blur(6px);
}

.card__badge--available {
  background: rgba(70, 167, 88, 0.9);
  color: #06210c;
}

.card__badge--pending {
  background: rgba(229, 160, 13, 0.92);
  color: #17130a;
}

.card__badge--processing {
  background: rgba(53, 197, 244, 0.9);
  color: #04222c;
}

/*
 * Asked for, but nobody has said yes yet.
 *
 * Paler and outlined rather than the solid amber of an approved request, so
 * the two are told apart at poster size before either is read — one is a state
 * that is going somewhere, the other is a state that is waiting on a person.
 *
 * It is the only badge that runs to two lines, so it drops the pill shape and
 * caps its width instead of stretching across the poster it sits on.
 */
.card__badge--waiting {
  display: block;
  max-width: calc(100% - 14px);
  border-radius: 9px;
  background: rgba(28, 22, 6, 0.86);
  border: 1px solid rgba(229, 160, 13, 0.75);
  color: #f0c765;
  line-height: 1.25;
}

.card__badge__note {
  display: block;
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.02em;
  opacity: 0.85;
}

.card__title {
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card__meta {
  font-size: 12px;
  color: var(--text-faint);
  /*
   * A fixed line box, so a card carrying an age rating is exactly as tall as
   * one without. The boxed chip is taller than bare text, and left to itself
   * it pushes its own card down by three pixels — which across a grid reads as
   * rows that do not line up.
   */
  line-height: 17px;
  min-height: 17px;
}

/* Pager -------------------------------------------------------------------- */

/*
 * The strip under the grid while the next page is on its way.
 *
 * Only ever visible for the moment a fetch is in flight — the grid keeps
 * everything above it — so it is a note that more is coming rather than a
 * spinner standing in for the page.
 */
.grid__more {
  display: flex;
  justify-content: center;
  padding: 22px 0 8px;
}


/* Modal -------------------------------------------------------------------- */

.modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: 20px;
}

.modal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.72);
  backdrop-filter: blur(3px);
}

.modal__panel {
  position: relative;
  width: 100%;
  max-width: 700px;
  max-height: min(86vh, 900px);
  overflow-y: auto;
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

/*
 * Back sits opposite Close, in the corner the sheet's own art runs behind, so
 * both need their own dark ground to stay legible over a bright poster.
 *
 * Absolute rather than sticky: the panel scrolls back to the top on every hop,
 * so Back is under your thumb the moment the new sheet arrives, and from there
 * it scrolls away with the art exactly as Close does.
 */
.modal__close,
.modal__back {
  position: absolute;
  top: 12px;
  z-index: 2;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  line-height: 1;
  cursor: pointer;
}

.modal__close {
  right: 12px;
  font-size: 22px;
}

.modal__back {
  left: 12px;
  /* The chevron sits high in its em box; nudged down so it looks centred in
     the circle rather than measured into it. */
  font-size: 26px;
  padding-bottom: 3px;
}

.modal__close:hover,
.modal__back:hover {
  background: rgba(0, 0, 0, 0.8);
}

/*
 * The banner is the title's own poster, zoomed past its edges and faded back so
 * it reads as atmosphere rather than a second image competing with the poster
 * sitting on top of it.
 *
 * The art lives on a pseudo-element so its opacity is its own: putting it on
 * .detail__backdrop would fade the gradient below at the same time, and the
 * gradient is what makes the banner meet the body instead of ending on a line.
 */
/*
 * One large piece of art behind the top of the sheet, fading the whole way out
 * before the text starts.
 *
 * It is taller than the content it sits behind and is not clipped to its own
 * box: the art runs on under the first rows and the gradient is what ends it,
 * so there is no edge anywhere — which is the point of dropping the small
 * poster that used to sit on top of it.
 */
.detail__backdrop {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  /*
   * Capped, but never more than a portion of the sheet. A fixed 340px covers
   * almost all of a short sheet — a film with a one-line status and three
   * buttons — and the fade then never completes inside the visible area,
   * leaving the buttons sitting on the picture.
   */
  height: min(400px, 58%);
  overflow: hidden;
  pointer-events: none;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}

.detail__backdrop::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--art);
  background-size: cover;
  /* Above centre: posters put the title and faces in the upper half. */
  background-position: center 26%;
  transform: scale(1.35);
  opacity: 0.5;
  filter: saturate(1.15);
}

/*
 * A long fade rather than a short one. Five stops instead of three: a straight
 * three-stop gradient over this distance shows its own midpoint as a band,
 * because perceived brightness does not track opacity linearly.
 */
.detail__backdrop::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    rgba(21, 24, 29, 0) 0%,
    rgba(21, 24, 29, 0.12) 30%,
    rgba(21, 24, 29, 0.42) 58%,
    rgba(21, 24, 29, 0.78) 80%,
    var(--bg-raised) 100%
  );
}

.detail__body {
  position: relative;
  z-index: 1;
  /* Named, because the cast strip has to cancel exactly this much to run out
     to the sheet's edges and no further. */
  --detail-pad-x: 26px;
  padding: 22px var(--detail-pad-x) 26px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* The art is positioned, so the sheet is its containing block. */
.detail {
  position: relative;
  /* A phone's number is the base; a wider screen has the room to give the
     poster a little more and takes it below. */
  --detail-poster-w: 176px;
}

/* Just enough of a run-up that the art reads as art before the poster starts. */
.detail--with-backdrop .detail__body {
  padding-top: 30px;
}

/*
 * The head is a column down the middle: poster, title, then the one line of
 * facts about it. Centred because it is a masthead — everything below it, the
 * description and the season list, goes back to reading left to right.
 */
.detail__head {
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
  text-align: center;
  position: relative;
  z-index: 1;
}

/*
 * Sharp, over the blown-up copy of itself. Sized in a variable so the two
 * breakpoints set one number instead of restating the box.
 */
.detail__poster {
  width: var(--detail-poster-w, 150px);
  aspect-ratio: 2 / 3;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid rgba(255, 255, 255, 0.12);
  /* Lifts it off a backdrop that is the same picture: without a shadow the
     two blur into one another exactly where the poster's edge should be. */
  box-shadow: 0 16px 38px rgba(0, 0, 0, 0.55);
  background: var(--bg-hover);
  margin-bottom: 4px;
}

.detail__title {
  margin: 0;
  text-wrap: balance;
  font-size: 23px;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.detail__meta {
  color: var(--text-dim);
  font-size: 13.5px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  justify-content: center;
}

/*
 * The separator hangs off the end of the item before it, not the start of the
 * one after. Both look the same on one line; on two, a leading "·" opens the
 * wrapped line with a mark that belongs to the line above.
 */
.detail__meta span:not(:last-child)::after {
  content: '·';
  margin-left: 8px;
  color: var(--text-faint);
}

.detail__overview {
  margin: 0;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.6;
}

.detail__status {
  padding: 11px 14px;
  border-radius: var(--radius);
  font-size: 13.5px;
  border: 1px solid var(--border);
  background: var(--bg-hover);
  color: var(--text-dim);
}

.detail__status--available {
  border-color: rgba(70, 167, 88, 0.35);
  background: rgba(70, 167, 88, 0.1);
  color: #7fd18d;
}

.detail__status--pending {
  border-color: rgba(229, 160, 13, 0.35);
  background: rgba(229, 160, 13, 0.1);
  color: #f0c765;
}

/*
 * Both rows sit under the centred masthead, so they are centred too, and both
 * are above the fading art — hence the stacking context.
 */
.detail__actions,
.detail__scores {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
  position: relative;
  z-index: 1;
}

.detail__scores {
  gap: 8px 10px;
}

/*
 * Square, so the heart is a heart rather than a pill with a heart in it, and
 * the same height as the buttons beside it.
 */
.btn--heart {
  font-size: 18px;
  line-height: 1;
  padding: 0;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.btn--heart.is-following {
  color: var(--accent);
  border-color: rgba(229, 160, 13, 0.45);
  background: rgba(229, 160, 13, 0.12);
}

/* Cast, crew, and titles like this one -------------------------------------- */

.people,
.similar {
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.strip__head {
  margin: 0;
  font-size: 11.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
}

/*
 * Runs out to the sheet's edges and scrolls sideways.
 *
 * The negative margin cancels the body's padding and the matching padding puts
 * it back inside the scroller, so the first card still lines up with the text
 * above it while the row itself reaches the edge. Without that, the strip stops
 * short of both sides and reads as a row that has ended rather than one that
 * carries on.
 */
.people__row,
.similar__row {
  display: flex;
  /* Natural height, not stretched to the tallest card — the default `stretch`
     lets one long name set a floor that every other card then pads out to. */
  align-items: flex-start;
  margin: 0 calc(var(--detail-pad-x) * -1);
  padding: 2px var(--detail-pad-x) 4px;
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /*
   * No scroll snapping. It looks like the obvious thing for a row of cards, but
   * snap points are measured from the padding box, so the first card snaps
   * flush to the container's edge — which means the strip loads 26px already
   * scrolled, with the first card half cut off by the padding it was supposed
   * to sit inside. A strip you glance at does not need paging anyway.
   */
}

.people__row {
  gap: 14px;
}

.similar__row {
  gap: 12px;
}

.people__row::-webkit-scrollbar,
.similar__row::-webkit-scrollbar {
  display: none;
}

/*
 * The <li> exists only to keep the <ul> valid — the tile is the button inside
 * it. Left as a list-item it lays out a line box of its own around the button
 * and stands 30px taller than its contents, which shows up as a band of empty
 * space under the poster names.
 */
.similar__row > li {
  flex: none;
  display: flex;
}

.simtile {
  flex: none;
  width: 92px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.simtile__art {
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-hover);
}

/* The name inside the frame, for a title TMDB has no poster of. Not a blank
   rectangle: the title exists, the picture is what is missing. */
.simtile__art--blank {
  display: grid;
  place-items: center;
  padding: 8px;
  text-align: center;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text-faint);
}

/*
 * Two lines, fixed, with the rest elided.
 *
 * A row is only as tall as its tallest card, so one long title on three lines
 * leaves a band of empty space under all eleven others. Fixing the height makes
 * every card the same; clamping stops a very long one from being cut off
 * mid-line with no sign that anything is missing. The whole name is on the
 * button's tooltip either way, and the poster above it is doing most of the
 * identifying.
 */
.simtile__name {
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.3;
  height: 2.6em;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

.simtile:hover .simtile__art,
.simtile:focus-visible .simtile__art {
  border-color: var(--accent);
}

.person {
  flex: none;
  width: 74px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  text-align: center;
}

.person__face {
  width: 62px;
  height: 62px;
  border-radius: 50%;
  object-fit: cover;
  /* Faces are in the upper part of a portrait crop; centring one vertically
     lands on the chin. */
  object-position: center 22%;
  background: var(--bg-hover);
  border: 1px solid var(--border);
}

/* Initials rather than an empty circle: no photograph on TMDB is a gap in
   TMDB, and a blank frame says the person is missing instead. */
.person__face--blank {
  display: grid;
  place-items: center;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--text-faint);
}

/* Two lines' worth of room whether the name needs it or not, so the roles
   underneath sit on one line across the strip instead of stepping up and down
   with the length of the name above each. */
.person__name {
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.25;
  min-height: 2.5em;
}

.person__role {
  font-size: 10.5px;
  line-height: 1.25;
  color: var(--text-faint);
}

/* Quiet, and away from the buttons that do something. */
.detail__foot {
  display: flex;
  justify-content: center;
  margin-top: 2px;
}

.seasons {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.seasons__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  background: #12151a;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  color: var(--text-dim);
}

.seasons__list {
  max-height: 230px;
  overflow-y: auto;
}

.season {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 14px;
  border-bottom: 1px solid var(--border);
  font-size: 13.5px;
  cursor: pointer;
}

.season:last-child {
  border-bottom: 0;
}

.season:hover {
  background: var(--bg-hover);
}

.season input {
  accent-color: var(--accent);
  margin: 0;
}

.season__name {
  flex: 1;
}

.season__count {
  color: var(--text-faint);
  font-size: 12.5px;
}

.season__have {
  color: var(--success);
  font-size: 12px;
}

/* The seasons list grows a lot once episodes can be expanded inside it. */
.seasons__list {
  max-height: 340px;
}

.season-row {
  border-bottom: 1px solid var(--border);
}

.season-row:last-child {
  border-bottom: 0;
}

.season-row .season {
  border-bottom: 0;
}

.season__state {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 999px;
  white-space: nowrap;
}

.season__state--available {
  color: var(--success);
  background: rgba(111, 206, 128, 0.12);
}

.season__state--partial {
  color: var(--accent);
  background: rgba(229, 160, 13, 0.14);
}

.season__state--pending {
  color: #8ab4f8;
  background: rgba(138, 180, 248, 0.12);
}

.season__state--missing {
  color: var(--text-faint);
  background: rgba(255, 255, 255, 0.05);
}

.season__toggle {
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  padding: 3px 9px;
  border-radius: var(--radius);
  cursor: pointer;
}

.season__toggle:hover {
  color: var(--text);
  border-color: var(--accent);
}

/* Episodes -------------------------------------------------------------- */

.episodes {
  padding: 4px 14px 12px 38px;
  background: #0f1115;
}

.episodes__note {
  font-size: 12px;
  color: var(--text-faint);
  padding: 6px 0;
}

.episodes__note--error {
  color: var(--danger, #f2777a);
}

.episodes__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.episode {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 12.5px;
  padding: 4px 0;
  line-height: 1.4;
}

.episode__mark {
  width: 12px;
  flex: 0 0 12px;
  text-align: center;
  font-weight: 700;
}

.episode--have .episode__mark {
  color: var(--success);
}

.episode--missing .episode__mark {
  color: var(--text-faint);
}

.episode--unknown .episode__mark {
  color: var(--text-faint);
}

.episode__number {
  flex: 0 0 34px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.episode__name {
  flex: 1;
  color: var(--text-dim);
}

.episode--have .episode__name {
  color: var(--text);
}

.episode__date {
  color: var(--text-faint);
  white-space: nowrap;
  font-size: 11.5px;
}

/* Ratings and trailer ---------------------------------------------------- */

/* Centred rather than on a baseline: an icon has no baseline to sit on, so
   the browser lines its bottom edge up with the digits and it rides low. */
.rt {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px 4px 7px;
  border-radius: 999px;
  font-size: 12.5px;
  border: 1px solid var(--border-strong);
}

.rt__icon {
  width: 17px;
  height: 17px;
  flex: none;
}

.rt__score {
  font-weight: 700;
  font-size: 14px;
}

.rt--fresh .rt__score {
  color: #fa320a;
}

.rt--rotten .rt__score {
  color: #0ac855;
}

.rt--audience .rt__score {
  color: #ffbb00;
}

/* The house's own score, shaped like the chips beside it so the two are read
   as the same kind of thing rather than compared on presentation. Its own name
   stays in words: there is no mark for it the way there is for a tomato. */
.rt--house .rt__score {
  color: var(--accent);
}

.rt--house {
  border-color: rgba(229, 160, 13, 0.4);
  padding-left: 10px;
}

.rt__label {
  color: var(--text-dim);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* An icon set into a button's text, sized and aligned to the words. */
.btn__glyph {
  width: 13px;
  height: 13px;
  fill: currentColor;
  margin-right: 5px;
  vertical-align: -1px;
}

/* A chip that is a link still has to look like the chip beside it that is not. */
a.rt {
  text-decoration: none;
  color: inherit;
}

a.rt:hover {
  border-color: var(--border-strong);
  background: var(--bg-hover);
}

.detail__trailer {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  overflow: hidden;
  background: #000;
}

.detail__trailer iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.event-detail {
  padding: 26px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.event-detail__badge {
  align-self: flex-start;
  padding: 4px 11px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.event-detail__title {
  margin: 0;
  font-size: 22px;
  line-height: 1.25;
}

.event-detail__sub {
  color: var(--text-dim);
  font-size: 14.5px;
}

.event-detail__when {
  color: var(--text-faint);
  font-size: 13.5px;
}

.event-detail__desc {
  margin: 0;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.6;
  white-space: pre-wrap;
  border-top: 1px solid var(--border);
  padding-top: 14px;
}

/* Requests list ------------------------------------------------------------ */

.requests {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.request {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-raised);
}

.request__poster {
  width: 44px;
  height: 66px;
  border-radius: 6px;
  object-fit: cover;
  background: var(--bg-hover);
  flex: none;
}

.request__main {
  flex: 1;
  min-width: 0;
}

.request__title {
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.request__meta {
  color: var(--text-faint);
  font-size: 12.5px;
}

.pill {
  padding: 4px 11px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  flex: none;
}

.pill--pending {
  background: rgba(229, 160, 13, 0.16);
  color: var(--accent);
}

.pill--approved {
  background: rgba(53, 197, 244, 0.16);
  color: var(--sonarr);
}

.pill--available {
  background: rgba(70, 167, 88, 0.16);
  color: #6fce80;
}

.pill--declined {
  background: rgba(229, 72, 77, 0.16);
  color: #ff8a8d;
}

/* Empty / loading ---------------------------------------------------------- */

.empty {
  grid-column: 1 / -1;
  padding: 56px 20px;
  text-align: center;
  color: var(--text-faint);
  font-size: 14px;
}

.skeleton {
  aspect-ratio: 2 / 3;
  border-radius: var(--radius);
  background: linear-gradient(100deg, var(--bg-raised) 30%, var(--bg-hover) 50%, var(--bg-raised) 70%);
  background-size: 220% 100%;
  animation: shimmer 1.3s infinite linear;
}

@keyframes shimmer {
  to {
    background-position: -220% 0;
  }
}

/* Bottom navigation -------------------------------------------------------- */

/*
 * Phones only. The four destinations sit where a thumb reaches, and the bar
 * gets out of the way while you are reading: on a downward scroll it draws
 * back — smaller, and more of the page showing through it — and returns to
 * full size the moment you scroll up.
 *
 * It never leaves. A bar that slides off has to be summoned back before it can
 * be used, and the scroll that summons it is the one you were using to read;
 * shrinking gives back most of the room while keeping every destination one
 * tap away.
 *
 * Hidden above 860px, where the top tabs are already comfortable.
 */
.bottomnav {
  display: none;
}

@media (max-width: 860px) {
  /*
   * Four numbers. The bar's own height, how far it is held in from the sides of
   * the screen, the least it will sit above the bottom, and the room the page
   * leaves clear beneath its content — which must be at least the height and
   * the bottom offset added together, or the last row ends up under the bar
   * where it is only ever visible at the very bottom of a page.
   */
  :root {
    /* 50px tap target + 4px padding either side + 2px border either side. */
    --bottomnav-h: 62px;
    /* Twice the 12px the content is inset by, so the bar reads as deliberately
       held in from the edge rather than as content that failed to line up. */
    --bottomnav-side: 24px;
    /*
     * The floor, not the offset. On a phone with a home indicator the safe-area
     * inset is already the strip the system wants left alone; this number takes
     * over where there is no inset at all, so the bar never sits flush against a
     * hardware edge.
     */
    --bottomnav-gap: 4px;
    /*
     * How far into the safe area the bar is allowed to settle.
     *
     * The inset is generous — it clears the home indicator and then some — and
     * the bar floating a full inset above it reads as stranded rather than as
     * anchored. This takes some of that back while still leaving the indicator
     * its own strip to live in.
     */
    --bottomnav-sink: 12px;
    --bottomnav-room: 70px;
  }

  /*
   * A floating row rather than a bar welded to the bottom edge: detached, the
   * app's own background runs underneath it and the page reads as taller than
   * it is. It also stops the bar competing with the home indicator for the
   * same strip of screen.
   *
   * Held in from both edges rather than sized to its contents. Four icons in a
   * pill only as wide as they need leaves them shoulder to shoulder in the
   * middle of the screen; spread across most of the width, each one sits in the
   * space it owns, which is the thing that makes four marks read as four
   * destinations.
   */
  .bottomnav {
    position: fixed;
    left: var(--bottomnav-side);
    right: var(--bottomnav-side);
    bottom: max(
      var(--bottomnav-gap),
      calc(env(safe-area-inset-bottom, 0px) - var(--bottomnav-sink))
    );
    z-index: 30;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0;
    padding: 4px;
    background: rgba(17, 20, 25, 0.86);
    backdrop-filter: blur(18px) saturate(1.2);
    /* A trace of the accent rather than the usual grey line: enough to pick the
       bar out from whatever is scrolling behind it, not enough to read as a
       highlighted or selected state. */
    border: 2px solid rgba(229, 160, 13, 0.34);
    border-radius: 22px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
    transform: scale(1);
    opacity: 1;
    /* Shrinks towards the edge it sits on, so the gap under it never changes
       and it reads as drawing back rather than drifting up. */
    transform-origin: bottom center;
    /*
     * Quick coming back, unhurried going away.
     *
     * A transition uses the timing of the state it is moving *to*, so these
     * durations are the ones that run when `is-compact` is removed — the
     * return. Reaching for navigation should feel immediate; retreating is not
     * something anyone asked for, so it can take its time and stay out of the
     * way of whatever is being read.
     */
    transition:
      transform 0.2s cubic-bezier(0.22, 0.61, 0.36, 1),
      opacity 0.2s ease;
  }

  /*
   * Smaller and thinner, not gone. Scaled rather than re-laid-out: changing the
   * padding and the icon size would animate layout on every scroll, and the
   * whole bar is one composited layer this way.
   *
   * 0.85 is as far down as it goes — the tap targets scale with it, and past
   * about this point four buttons across a phone stop being comfortably
   * separable.
   */
  .bottomnav.is-compact {
    transform: scale(0.85);
    opacity: 0.62;
    /* The retreating half of the pair above: slow, and easing out the whole
       way so it settles rather than snapping. */
    transition-duration: 0.55s;
    transition-timing-function: cubic-bezier(0.33, 0, 0.25, 1);
  }

  .bottomnav__tab {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 50px;
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    color: var(--text-faint);
    font-family: inherit;
    cursor: pointer;
  }

  /* With the words gone the icon is the whole button, so it gets the room the
     label used to take rather than staying at the size it was drawn beside it. */
  .bottomnav__icon {
    width: 30px;
    height: 30px;
    fill: currentColor;
    /* Over the oval below, which is drawn on the button itself. */
    position: relative;
    z-index: 1;
  }

  .bottomnav__tab.is-active {
    color: var(--accent);
  }

  /*
   * A grey oval behind the section you are in.
   *
   * The amber icon already says which one it is, but only once you are looking
   * straight at it — four marks in a row read as a set, and a set needs a
   * ground under the current one to be scanned rather than compared. Sized
   * inside the button rather than filling it, so it reads as a marker on the
   * bar instead of a button someone forgot to unpress.
   */
  .bottomnav__tab.is-active::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 74px;
    height: 46px;
    /*
     * Concentric with the bar rather than a pill in it.
     *
     * Two rounded boxes only look nested when the inner radius is the outer
     * one less the gap between them: 22px at the bar's edge, less its 2px
     * border, its 4px padding and the 2px this sits inside the tap target.
     * A fully rounded end on a 46px-tall box curves harder than the corner it
     * sits inside, which is what made it read as a separate shape laid on the
     * bar instead of part of it.
     */
    border-radius: 14px;
    background: rgba(255, 255, 255, 0.13);
  }

  /* One set of tabs at a time: the top row goes when the bottom bar is on. */
  .topbar .tabs {
    display: none;
  }

  /*
   * The header goes up while you read, on the same scroll that draws the bottom
   * bar back, and returns on the way up.
   *
   * Only here, and deliberately. Above this width the header still holds the
   * tabs — it is the whole of the navigation, with no bottom bar to fall back
   * on — and sliding it away would leave nowhere to go. Below it the header is
   * a logo and an avatar, and the bar at the other end of the screen is doing
   * the navigating.
   *
   * It is stuck to the top by the time this can fire (nothing hides inside the
   * first 60px), so the transform moves it off screen rather than tearing a gap
   * out of the top of the page.
   */
  .topbar.is-hidden {
    transform: translateY(-100%);
    /* Quicker than the bottom bar's retreat. That one stays on screen the whole
       time and can afford to take its time; this one is leaving, and a slow
       exit reads as a stutter while it is half off the edge. */
    transition-duration: 0.32s;
    transition-timing-function: cubic-bezier(0.33, 0, 0.25, 1);
  }

  /*
   * Room to scroll past the bar rather than under it: the bar and a little
   * breathing space, plus however far off the bottom it is actually sitting.
   * Measured at full size — the bar shrinks while you read, but it is full size
   * again by the time you have scrolled back to the thing that was under it.
   */
  .app {
    padding-bottom: calc(
      var(--bottomnav-room) +
        max(var(--bottomnav-gap), calc(env(safe-area-inset-bottom, 0px) - var(--bottomnav-sink)))
    );
  }
}

/*
 * A phone on its side has about 390px of height, and 60px of it is too much to
 * give a navigation bar. The icons and their tap targets tighten up; nothing
 * else changes, since there were never any labels to drop.
 */
@media (max-width: 860px) and (max-height: 460px) {
  :root {
    --bottomnav-h: 46px;
    /* On its side there is width to spare, so the bar is held in much further
       and still leaves each icon more room than it has upright. */
    --bottomnav-side: 20%;
    --bottomnav-gap: 3px;
    --bottomnav-sink: 14px;
    --bottomnav-room: 52px;
  }

  .bottomnav {
    padding: 3px;
    border-radius: 15px;
  }

  .bottomnav__tab {
    min-height: 38px;
  }

  /* Same arithmetic against the smaller bar: 15px, less its 2px border, 3px
     padding and the 2px inside the tap target. */
  .bottomnav__tab.is-active::before {
    width: 44px;
    height: 30px;
    border-radius: 8px;
  }

  .bottomnav__icon {
    width: 24px;
    height: 24px;
  }
}

/* Spinner ------------------------------------------------------------------ */

/* Read aloud, drawn nowhere. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/*
 * Shown wherever a panel is waiting on the network. The panels used to sit
 * empty until their data landed, which reads as a broken page rather than a
 * busy one — most of "the app is slow" was that silence.
 *
 * A ring with one lit quarter: a bare border-spin needs no image, no font and
 * no second element, and it stays legible at 20px.
 */
.spinner {
  display: grid;
  place-items: center;
  gap: 12px;
  padding: 28px 0;
  color: var(--text-faint);
  font-size: 13px;
}

.spinner::before {
  content: '';
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2.5px solid var(--border-strong);
  border-top-color: var(--accent);
  animation: spin 0.7s linear infinite;
}

/* Inside a poster grid the spinner should not stretch one cell. */
.spinner--grid {
  grid-column: 1 / -1;
}

/* For a small area — a row, a card — where the label would crowd it. */
.spinner--inline {
  padding: 14px 0;
}

.spinner--inline::before {
  width: 18px;
  height: 18px;
  border-width: 2px;
}

@keyframes spin {
  to {
    transform: rotate(1turn);
  }
}

@media (prefers-reduced-motion: reduce) {
  /*
   * The global reduced-motion rule collapses every animation to 0.01ms, which
   * would leave this frozen mid-turn and looking like a broken glyph. A slow
   * steady rotation still says "working" without the spin being the point.
   */
  .spinner::before {
    animation-duration: 1.8s !important;
  }
}

/* Admin -------------------------------------------------------------------- */

.main--admin {
  max-width: 940px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.panel {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-raised);
  padding: 20px 22px;
}

.panel__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}

.panel__title {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
}

/* Pushed right so that adding a third thing to the head puts it beside the
   note rather than stranding the note in the middle of the row. */
.panel__note {
  color: var(--text-faint);
  font-size: 12.5px;
  margin-left: auto;
}

/*
 * "See more" in the corner of a panel. A quiet link rather than a button: it
 * is a way further into the same thing, not an action on it, and a filled
 * button up here would outrank the panel it belongs to.
 */
.panel__more {
  border: 0;
  padding: 0;
  background: none;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
}

.panel__more:hover {
  text-decoration: underline;
}

/* Everything past the sixth is drawn and hidden, so opening the panel is a
   class away and the count in the button is a fact rather than a guess. */
.tiles--capped > li:nth-child(n + 7) {
  display: none;
}

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
}

.status {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 13px 15px;
  background: #12151a;
}

.status__head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 7px;
}

.status__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
}

.status__dot--ok {
  background: var(--success);
  box-shadow: 0 0 0 3px rgba(70, 167, 88, 0.16);
}

.status__dot--bad {
  background: var(--danger);
  box-shadow: 0 0 0 3px rgba(229, 72, 77, 0.16);
}

.status__name {
  font-weight: 600;
  font-size: 13.5px;
}

.status__line {
  color: var(--text-dim);
  font-size: 12.5px;
  line-height: 1.55;
  word-break: break-word;
}

.feed-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.feed {
  display: flex;
  align-items: flex-start;
  gap: 13px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #12151a;
}

.feed--editing {
  border-color: var(--border-strong);
}

.feed__swatch {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  margin-top: 5px;
  flex: none;
}

.feed__main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.feed__title {
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.feed__type {
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--bg-hover);
  color: var(--text-faint);
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.feed__type--off {
  background: rgba(229, 72, 77, 0.14);
  color: #ff8a8d;
}

.feed__url {
  color: var(--text-faint);
  font-size: 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.feed__health {
  font-size: 12px;
  color: var(--text-faint);
}

.feed__health--ok {
  color: #6fce80;
}

.feed__health--bad {
  color: #ff8a8d;
}

.feed__actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* Forms -------------------------------------------------------------------- */

.form {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.form__row {
  display: flex;
  gap: 13px;
  flex-wrap: wrap;
}

/*
 * One service's connection settings. A <fieldset> for the grouping semantics,
 * with the browser's default border and padding removed so it sits in the form
 * like any other row.
 */
.form__row--service {
  align-items: flex-end;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
  margin: 0;
}

.form__row--service > legend {
  padding: 0 6px;
  font-size: 13px;
  color: var(--text);
}

/* Checkboxes are content-sized, so they should not stretch like a text input. */
.form__row--service input[type='checkbox'] {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
  min-width: 180px;
}

.field--narrow {
  flex: 0 0 190px;
  min-width: 150px;
}

.field--tiny {
  flex: 0 0 84px;
  min-width: 84px;
}

.field__label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-dim);
}

.field__input {
  padding: 10px 13px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
  width: 100%;
}

.field__input:focus {
  outline: none;
  border-color: var(--accent);
}

.field__input--color {
  padding: 4px;
  height: 40px;
  cursor: pointer;
}

.field__hint {
  font-size: 12px;
  color: var(--text-faint);
  line-height: 1.5;
}

.form__actions {
  display: flex;
  align-items: center;
  gap: 11px;
  flex-wrap: wrap;
}

.form__result {
  font-size: 13px;
  color: var(--text-dim);
}

.form__result.is-ok {
  color: #6fce80;
}

.form__result.is-error {
  color: #ff8a8d;
}

@media (max-width: 700px) {
  .feed {
    flex-wrap: wrap;
  }

  .feed__actions {
    width: 100%;
    justify-content: flex-start;
  }
}

/* Toasts ------------------------------------------------------------------- */

.toasts {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 80;
  display: flex;
  flex-direction: column;
  gap: 9px;
  pointer-events: none;
}

/*
 * Above the bottom bar rather than across it. Toasts outrank the bar in the
 * stacking order, so at the default offset "Request sent" lands squarely on
 * top of it. Cleared at the bar's full height whatever size the bar currently
 * is: a toast lasts a couple of seconds, and having it jump as the bar draws
 * back would draw more attention than the message.
 */
@media (max-width: 860px) {
  .toasts {
    left: 12px;
    right: 12px;
    bottom: calc(
      var(--bottomnav-room) +
        max(var(--bottomnav-gap), calc(env(safe-area-inset-bottom, 0px) - var(--bottomnav-sink)))
    );
    align-items: stretch;
  }

  .toast {
    max-width: none;
  }
}

.toast {
  padding: 12px 17px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-raised);
  box-shadow: var(--shadow);
  font-size: 14px;
  max-width: 340px;
  animation: slide-in 0.2s ease;
}

.toast--success {
  border-color: rgba(70, 167, 88, 0.45);
}

.toast--error {
  border-color: rgba(229, 72, 77, 0.45);
}

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

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

@media (max-width: 860px) {
  /* Below this the tabs move to the bottom bar, so the header holds only the
     logo and the avatar and has no reason to be tall. The chip is 38px and is
     the tallest thing in it; 5px either side is enough to clear it without
     shrinking a tap target to save chrome. Landscape phones land in this band,
     where the vertical space matters most. */
  .topbar {
    gap: 12px;
    --topbar-pad-y: 5px;
    --topbar-pad-x: 14px;
  }

  .topbar__username {
    display: none;
  }

  .tabs {
    order: 3;
    width: 100%;
  }

  .tab {
    flex: 1;
    text-align: center;
    padding: 8px 8px;
  }

  .main {
    --main-pad-top: 14px;
    --main-pad-x: 14px;
    --main-pad-bottom: 14px;
  }

  .calendar {
    display: none;
  }

  .agenda__day {
    grid-template-columns: 1fr;
    gap: 8px;
  }

  .agenda__date {
    position: static;
    display: flex;
    align-items: baseline;
    gap: 8px;
  }

  .agenda__date strong {
    font-size: 16px;
  }

  /* Week and Agenda both work on a phone; only the month grid does not, so
     only its button goes. The toggle itself stays. */
  .segmented__btn--wide-only {
    display: none;
  }

  .grid {
    grid-template-columns: repeat(auto-fill, minmax(122px, 1fr));
    gap: 13px;
  }

  /* Shorter run-up on a phone, where the sheet has less height to give. */
  .detail--with-backdrop .detail__body {
    padding-top: 22px;
  }

  .detail {
    --detail-poster-w: 138px;
  }

  .detail__backdrop {
    height: min(320px, 54%);
  }

  .toolbar__title {
    font-size: 17px;
    min-width: 0;
    flex: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* Dashboard ---------------------------------------------------------------- */

.dash {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  align-items: start;
}

.dash__panel--wide {
  grid-column: 1 / -1;
}

/* Search sits across the top of the dashboard, above both columns. */
.dash__panel--find {
  grid-column: 1 / -1;
}

.dash__search {
  /* The margin below only earns its place when results are showing. */
  margin-bottom: 0;
}

.dash__searchnote {
  margin: 12px 0 0;
  font-size: 13px;
  color: var(--text-faint);
}

.grid--find {
  margin-top: 14px;
  /* Denser than the Request tab's grid: this is a lookup, not a browse, and
     the panel should not push the rest of the dashboard off the screen. */
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: 14px;
}

@media (max-width: 900px) {
  .dash {
    /* minmax(0, 1fr), not 1fr: a bare 1fr track has an automatic minimum of
       min-content, so the least breakable thing in any one panel sets the
       width of every panel and pushes the column past the screen. The
       two-column rule above already guards against this. */
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
  }
}

.dash__empty {
  color: var(--text-faint);
  font-size: 13px;
  line-height: 1.6;
  margin: 0;
}

/*
 * Poster tiles. auto-fill with a minimum rather than fixed counts, so the same
 * rule gives three across a phone and six across a desktop without a breakpoint
 * per size.
 */
.tiles {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  gap: 14px 12px;
}

@media (max-width: 420px) {
  .tiles {
    grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
    gap: 12px 10px;
  }
}

.tile {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.tile__art {
  position: relative;
  display: block;
  aspect-ratio: 2 / 3;
  border-radius: 8px;
  overflow: hidden;
  background: var(--bg-hover);
  border: 1px solid var(--border);
}

.tile__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Something legible when Radarr/Sonarr has no artwork for it. */
.tile__blank {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 8px;
  text-align: center;
  font-size: 11px;
  line-height: 1.35;
  color: var(--text-faint);
}

.tile__title {
  font-size: 12.5px;
  line-height: 1.3;
  color: var(--text);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.tile__line {
  font-size: 11.5px;
  color: var(--text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Sits on the art, so the words below stay to one short line. */
/*
 * The whole poster is the target. A transparent button rather than a click
 * handler on the tile, so it takes focus, answers the keyboard and announces
 * itself — and so the Review button in "Recently watched" can sit above it
 * instead of inside it.
 */
.tile__open {
  position: absolute;
  inset: 0;
  z-index: 1;
  border: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  border-radius: inherit;
}

.tile__open:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.tile__art:hover .tile__open {
  background: rgba(255, 255, 255, 0.06);
}

.tile__badge {
  /* Above the opener, so a badge that is itself a button still takes the tap. */
  z-index: 2;
  position: absolute;
  left: 5px;
  right: 5px;
  bottom: 5px;
  padding: 3px 6px;
  border-radius: 5px;
  font-size: 10.5px;
  font-weight: 600;
  text-align: center;
  backdrop-filter: blur(6px);
  background: rgba(13, 15, 18, 0.82);
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tile__badge.is-have {
  color: #7fe094;
}

.tile__badge.is-soon {
  color: var(--accent);
}

.tile__badge.is-waiting {
  color: #9cc4ff;
}

.tile__badge.is-unknown {
  color: var(--text-faint);
}

.tile__rank {
  position: absolute;
  top: 5px;
  left: 5px;
  min-width: 19px;
  height: 19px;
  padding: 0 5px;
  border-radius: 5px;
  background: rgba(13, 15, 18, 0.82);
  backdrop-filter: blur(6px);
  color: var(--text);
  font-size: 11px;
  font-weight: 700;
  display: grid;
  place-items: center;
  font-variant-numeric: tabular-nums;
}

/* The line under a tile is short, so its dot sits inline with the title. */
.tile__title .avail-dot {
  margin-right: 5px;
}

.dash__cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px;
}


@media (max-width: 700px) {
  .dash__cols {
    grid-template-columns: 1fr;
    gap: 18px;
  }
}

.dash__colhead {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 10px;
}

/* Availability: on Plex / missing / not out yet ---------------------------- */

:root {
  --avail-have: #46a758;
  --avail-missing: #e5484d;
  --avail-upcoming: #4c8dff;
}

.avail-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  margin-right: 6px;
  flex: none;
  vertical-align: baseline;
}

.avail-dot--on-plex {
  background: var(--avail-have);
}

.avail-dot--missing {
  background: var(--avail-missing);
}

.avail-dot--upcoming {
  background: var(--avail-upcoming);
}

/* The labelled form, for the event modal where there is room to explain. */
.avail {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  align-self: flex-start;
  padding: 5px 12px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
}

.avail__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
}

.avail--on-plex {
  color: var(--avail-have);
  background: rgba(70, 167, 88, 0.13);
}

.avail--on-plex .avail__dot {
  background: var(--avail-have);
}

.avail--missing {
  color: #ff8a8d;
  background: rgba(229, 72, 77, 0.13);
}

.avail--missing .avail__dot {
  background: var(--avail-missing);
}

.avail--upcoming {
  color: #8ab4f8;
  background: rgba(76, 141, 255, 0.13);
}

.avail--upcoming .avail__dot {
  background: var(--avail-upcoming);
}

/* Legend, so the dots are not a code nobody was given. */
.legend {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--text-faint);
}

.legend__item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* Episode list states in the series detail modal. */
.episode--upcoming .episode__mark {
  color: var(--avail-upcoming);
}

.episode--missing .episode__mark {
  color: var(--avail-missing);
}

.episode--have .episode__mark {
  color: var(--avail-have);
}

/* Four tabs have to fit a phone without pushing the sign-out button off. */
@media (max-width: 560px) {
  .topbar {
    gap: 10px;
    --topbar-pad-x: 12px;
  }

  .tabs {
    order: 3;
    width: 100%;
    justify-content: space-between;
  }

  .tab {
    padding: 7px 6px;
    font-size: 12.5px;
    flex: 1;
    /* Four labels on one row; wrapping one of them breaks the row's height. */
    white-space: nowrap;
  }

  .topbar__user {
    gap: 8px;
  }

  .main {
    --main-pad-top: 14px;
    --main-pad-x: 12px;
    --main-pad-bottom: 28px;
  }

  .panel {
    padding: 14px;
  }
}

/* Sub-tabs, reviews and stars ---------------------------------------------- */

.subtabs {
  display: flex;
  gap: 4px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--border);
}

.subtab {
  padding: 9px 15px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  font-weight: 500;
  font-size: 14px;
  margin-bottom: -1px;
}

.subtab:hover {
  color: var(--text);
}

.subtab[aria-selected='true'] {
  color: var(--text);
  border-bottom-color: var(--accent);
}

/* How many are waiting, next to the word rather than instead of it. */
.subtab__count {
  display: inline-block;
  min-width: 18px;
  margin-left: 6px;
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--accent);
  color: #17130a;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.45;
  text-align: center;
}

/* Notifications ------------------------------------------------------------- */

.notes {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.note {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 13px 2px;
  border-bottom: 1px solid var(--border);
}

.note:last-child {
  border-bottom: 0;
}

/*
 * Unread is an amber rule down the side rather than a different background.
 * A filled row reads as selected, and these are not a list you pick from —
 * the mark just says which ones are new since you last looked.
 */
.note.is-unread {
  box-shadow: inset 2px 0 0 var(--accent);
  padding-left: 12px;
}

.note__mark {
  flex: none;
  width: 9px;
  height: 9px;
  margin-top: 5px;
  border-radius: 50%;
  background: var(--text-faint);
}

.note__mark--on-plex {
  background: #46a758;
}

.note__mark--waiting {
  background: var(--accent);
}

.note__mark--upcoming {
  background: #3b82f6;
}

.note__main {
  flex: 1;
  min-width: 0;
}

.note__title {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.35;
}

.note__body {
  margin: 2px 0 0;
  font-size: 13px;
  color: var(--text-dim);
  line-height: 1.4;
}

.note__when {
  margin: 4px 0 0;
  font-size: 11.5px;
  color: var(--text-faint);
}

.note__dismiss {
  flex: none;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-faint);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}

.note__dismiss:hover {
  background: var(--bg-hover);
  color: var(--text);
}

/*
 * The red count on the profile chip.
 *
 * Red rather than the accent amber, which everything else here uses for "this
 * is the thing to press". This is not an invitation, it is a fact about what is
 * waiting, and it has to be picked out from a header that is already amber.
 */
.topbar__dot {
  position: absolute;
  top: -3px;
  right: -4px;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border-radius: 999px;
  border: 2px solid var(--bg);
  background: #e5484d;
  color: #fff;
  font-size: 10.5px;
  font-weight: 700;
  line-height: 13px;
  text-align: center;
}

/* Stars: filled to the rating, hollow after it. */
.stars {
  white-space: nowrap;
  letter-spacing: 1px;
  font-size: 13px;
}

.star {
  color: var(--border-strong);
}

.star.is-on {
  color: var(--accent);
}

.star-pick {
  display: flex;
  gap: 4px;
  margin: 4px 0 12px;
}

.star-btn {
  border: 0;
  background: transparent;
  color: var(--border-strong);
  font-size: 30px;
  line-height: 1;
  padding: 2px 4px;
  cursor: pointer;
  transition: color 0.12s ease, transform 0.08s ease;
}

.star-btn:hover {
  transform: scale(1.12);
}

.star-btn.is-on {
  color: var(--accent);
}

.star-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

.field__optional {
  color: var(--text-faint);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}

.reviews {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.review {
  display: flex;
  gap: 14px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-raised);
}

.review__art {
  width: 58px;
  flex: none;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--bg-hover);
}

.review__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
  align-items: flex-start;
}

.review__head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}

.review__title {
  font-weight: 600;
  font-size: 15px;
}

.review__year {
  color: var(--text-faint);
  font-weight: 400;
  font-size: 13px;
}

.review__by {
  font-size: 12.5px;
  color: var(--text-faint);
}

.review__text {
  margin: 2px 0 0;
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-dim);
  white-space: pre-wrap;
}

/* The dashboard's watch tiles use their badge as the review control. */
.tile__badge--action {
  border: 0;
  cursor: pointer;
  font: inherit;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--accent);
  width: auto;
}

.tile__badge--action:hover {
  color: var(--accent-hover);
  background: rgba(13, 15, 18, 0.94);
}

/* Categories, levels and the board ----------------------------------------- */

.cat {
  display: flex;
  align-items: center;
  gap: 12px;
  justify-content: space-between;
  padding: 5px 0;
  flex-wrap: wrap;
}

.cat__label {
  display: flex;
  flex-direction: column;
  min-width: 130px;
}

.cat__name {
  font-size: 14px;
  font-weight: 600;
}

.cat__hint {
  font-size: 11.5px;
  color: var(--text-faint);
}

.cat__overall {
  margin: 6px 0 10px;
  font-size: 13px;
  color: var(--text-dim);
}

.star-pick {
  margin: 0;
  gap: 2px;
}

.star-pick .star-btn {
  font-size: 24px;
  padding: 1px 3px;
}

/* Category averages on a review card. */
.catbars {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin: 2px 0;
}

.catbar {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11.5px;
  color: var(--text-faint);
}

.catbar__key {
  min-width: 46px;
}

.catbar__track {
  width: 52px;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
  overflow: hidden;
}

.catbar__fill {
  display: block;
  height: 100%;
  background: var(--accent);
}

.catbar__val {
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

/* Level -------------------------------------------------------------------- */

.level {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 16px;
}

.level__head {
  display: flex;
  align-items: center;
  gap: 12px;
}

.level__badge {
  width: 42px;
  height: 42px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 12px;
  background: var(--accent);
  color: #17130a;
  font-weight: 800;
  font-size: 18px;
}

.level__name {
  font-size: 16px;
  font-weight: 700;
}

.level__xp {
  font-size: 12.5px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.level__track {
  height: 6px;
  border-radius: 3px;
  background: var(--border);
  overflow: hidden;
}

.level__fill {
  display: block;
  height: 100%;
  background: var(--accent);
  transition: width 0.4s ease;
}

/* Leaderboard --------------------------------------------------------------- */

.board {
  list-style: none;
  margin: 0;
  padding: 0;
}

.board__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 10px;
  border-radius: var(--radius);
  font-size: 13.5px;
}

.board__row.is-me {
  background: rgba(229, 160, 13, 0.1);
}

.board__rank {
  width: 22px;
  flex: none;
  text-align: right;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.board__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.board__lvl {
  color: var(--text-dim);
  font-size: 12.5px;
  white-space: nowrap;
}

.board__xp {
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Achievements -------------------------------------------------------------- */

.badges {
  list-style: none;
  margin: 0 0 18px;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 8px;
}

.badge {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 11px 13px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-raised);
  opacity: 0.55;
}

.badge.is-earned {
  opacity: 1;
  border-color: rgba(229, 160, 13, 0.4);
}

.badge__mark {
  font-size: 18px;
  color: var(--border-strong);
}

.badge.is-earned .badge__mark {
  color: var(--accent);
}

.badge__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.badge__name {
  font-size: 13.5px;
  font-weight: 600;
}

.badge__detail {
  font-size: 12px;
  color: var(--text-faint);
}

.badge__xp {
  font-size: 12px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* Celebrations -------------------------------------------------------------- */

/*
 * Everything below is motion over information that is already on the page.
 * The blanket `prefers-reduced-motion` rule further up flattens every duration
 * to nothing, so each keyframe here is written with the *resting* state as its
 * end point — a `from` block and no `to`. Skipped, they land exactly where the
 * static CSS puts them rather than stranding an element at half opacity.
 */

.fx-layer {
  position: fixed;
  inset: 0;
  z-index: 120;
  pointer-events: none;
  overflow: hidden;
}

.spark {
  position: absolute;
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 50%;
  animation: spark-out 620ms cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
}

@keyframes spark-out {
  from {
    opacity: 1;
    transform: translate(0, 0) scale(1);
  }
  to {
    opacity: 0;
    transform: translate(var(--dx), var(--dy)) scale(0.35);
  }
}

.confetti {
  position: absolute;
  top: -14px;
  width: 8px;
  height: 13px;
  border-radius: 2px;
  animation-name: confetti-fall;
  animation-timing-function: cubic-bezier(0.25, 0.6, 0.4, 1);
  animation-fill-mode: forwards;
}

@keyframes confetti-fall {
  from {
    opacity: 1;
    transform: translate(0, 0) rotate(0deg);
  }
  to {
    opacity: 0;
    transform: translate(var(--drift), 105vh) rotate(var(--spin));
  }
}

/* Picking stars ------------------------------------------------------------- */

.star-pick {
  /* Sweeping sideways across the row sets the rating; a vertical drag is still
     the page scrolling, which is how a phone has to behave. */
  touch-action: pan-y;
  user-select: none;
}

.star-btn {
  -webkit-tap-highlight-color: transparent;
}

.star-btn.is-pop {
  animation: star-pop 340ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes star-pop {
  0% {
    transform: scale(0.55) rotate(-22deg);
  }
  55% {
    transform: scale(1.38) rotate(6deg);
  }
  100% {
    transform: scale(1) rotate(0deg);
  }
}

.star-btn:active {
  transform: scale(0.88);
}

/* A category that still has no score, once you have tried to post. */
.cat.is-asking {
  animation: cat-ask 480ms ease;
  border-radius: 8px;
}

@keyframes cat-ask {
  0%,
  100% {
    transform: translateX(0);
    background: transparent;
  }
  20% {
    transform: translateX(-5px);
    background: rgba(229, 72, 77, 0.14);
  }
  55% {
    transform: translateX(5px);
    background: rgba(229, 72, 77, 0.14);
  }
}

.cat.is-rated .cat__name::after {
  content: '✓';
  margin-left: 6px;
  color: var(--accent);
  font-size: 11px;
  animation: tick-in 260ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes tick-in {
  from {
    opacity: 0;
    transform: scale(0.2);
  }
}

.cat__overall.is-done {
  color: var(--accent);
  font-weight: 600;
}

.cat__overall.is-pop {
  animation: overall-pop 420ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes overall-pop {
  0% {
    transform: scale(1);
  }
  45% {
    transform: scale(1.16);
  }
  100% {
    transform: scale(1);
  }
}

/* Ready to post: a slow breath rather than a flash, so it invites without
   demanding. */
.btn.is-ready {
  animation: ready-glow 2.4s ease-in-out infinite;
}

@keyframes ready-glow {
  0%,
  100% {
    box-shadow: 0 0 0 0 rgba(240, 173, 42, 0);
  }
  50% {
    box-shadow: 0 0 0 5px rgba(240, 173, 42, 0.16);
  }
}

.btn.is-sending {
  position: relative;
  color: transparent;
  animation: none;
}

.btn.is-sending::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 16px;
  height: 16px;
  border: 2px solid rgba(23, 19, 10, 0.35);
  border-top-color: #17130a;
  border-radius: 50%;
  animation: spin 620ms linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* The reward panel ---------------------------------------------------------- */

.reward {
  text-align: center;
  padding: 6px 2px 2px;
  animation: reward-in 320ms cubic-bezier(0.34, 1.4, 0.64, 1);
}

@keyframes reward-in {
  from {
    opacity: 0;
    transform: scale(0.94) translateY(10px);
  }
}

.reward__kicker {
  margin: 0;
  font-size: 11.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.reward__stars {
  display: flex;
  justify-content: center;
  gap: 3px;
  margin: 10px 0 4px;
}

.reward__star {
  font-size: 30px;
  line-height: 1;
  color: var(--border-strong);
  animation: star-drop 420ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

.reward__star.is-on {
  color: var(--accent);
}

@keyframes star-drop {
  from {
    opacity: 0;
    transform: translateY(-16px) scale(0.4) rotate(-30deg);
  }
}

.reward__title {
  margin: 4px 0 0;
  font-size: 21px;
}

.reward__note {
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--text-dim);
}

.reward__xp {
  margin: 14px 0 12px;
  font-size: 30px;
  font-weight: 800;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  animation: xp-rise 520ms cubic-bezier(0.34, 1.5, 0.64, 1) 120ms both;
}

@keyframes xp-rise {
  from {
    opacity: 0;
    transform: translateY(16px) scale(0.8);
  }
}

.reward__level {
  display: flex;
  align-items: center;
  gap: 12px;
  text-align: left;
  margin-bottom: 8px;
}

.reward__levelbody {
  min-width: 0;
}

.level__badge.is-flip {
  animation: badge-flip 620ms cubic-bezier(0.34, 1.5, 0.64, 1);
}

@keyframes badge-flip {
  0% {
    transform: rotateY(0deg) scale(1);
  }
  50% {
    transform: rotateY(180deg) scale(1.3);
    box-shadow: 0 0 0 8px rgba(240, 173, 42, 0.2);
  }
  100% {
    transform: rotateY(360deg) scale(1);
  }
}

.reward__levelup {
  margin: 12px 0 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--accent);
  animation: levelup-in 500ms cubic-bezier(0.34, 1.5, 0.64, 1) 700ms both;
}

@keyframes levelup-in {
  from {
    opacity: 0;
    letter-spacing: 0.4em;
  }
}

.reward__badges {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.reward__badge {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 11px 13px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: rgba(240, 173, 42, 0.09);
  text-align: left;
  animation: badge-slide 520ms cubic-bezier(0.34, 1.5, 0.64, 1) both;
}

@keyframes badge-slide {
  from {
    opacity: 0;
    transform: translateX(-26px) scale(0.94);
  }
}

.reward__badgemark {
  font-size: 20px;
  color: var(--accent);
  flex: none;
}

.reward__badgebody {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.reward__badgename {
  font-size: 14px;
  font-weight: 700;
}

.reward__badgedetail {
  font-size: 12px;
  color: var(--text-faint);
}

.reward__badgexp {
  font-size: 13px;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.reward__done {
  margin-top: 16px;
  width: 100%;
}

/* Standing card ------------------------------------------------------------- */

.board__row {
  animation: board-in 340ms ease both;
}

@keyframes board-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

.level__fill {
  /* Long enough to read as filling up rather than snapping. */
  transition: width 0.75s cubic-bezier(0.22, 0.8, 0.3, 1);
}

/* Phones -------------------------------------------------------------------- */

@media (max-width: 860px) {
  /* Bigger targets: a five-way choice repeated five times is the most
     finger-dependent thing in the app. */
  .star-pick .star-btn {
    font-size: 29px;
    padding: 5px 7px;
  }

  .reward__star {
    font-size: 34px;
  }

  .reward__xp {
    font-size: 36px;
  }
}

/* Reviewing by season ------------------------------------------------------- */

.seasons__hint {
  color: var(--text-faint);
  font-size: 11.5px;
  margin-left: 8px;
}

/* What the house gave this season, beside what it costs to request it. */
.season__score {
  color: var(--accent);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.season__toggle--review {
  color: var(--accent);
  border-color: rgba(240, 173, 42, 0.4);
}

/* On a review card, which season it was about. */
.review__season {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--accent);
  border: 1px solid rgba(240, 173, 42, 0.35);
  border-radius: 999px;
  padding: 1px 7px;
  white-space: nowrap;
}

@media (max-width: 860px) {
  .seasons__hint {
    display: block;
    margin: 2px 0 0;
  }

  /* The hint pushes the heading onto two lines, which was leaving "Select all"
     floating beside the wrap. Stack them instead. */
  .seasons__head {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }

  /* Four controls plus a checkbox will not sit on one phone-width line, so let
     the row wrap rather than squeezing the tap targets. Scoped to a season row:
     the "Select all" label in the heading is the same class and must not split
     from its own checkbox. */
  .season-row .season {
    flex-wrap: wrap;
    row-gap: 8px;
    column-gap: 8px;
  }

  /* Checkbox and name keep the first line; the state, score and both buttons
     drop to a second one rather than shrinking below a thumb's width. */
  .season-row .season__name {
    flex: 1 0 70%;
  }

  .season-row .season__state {
    font-size: 10.5px;
  }

  .season-row .season__toggle {
    padding: 5px 10px;
  }
}

/* Notifications ------------------------------------------------------------- */

.notify {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.notify__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.notify__lead {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

/* State as a chip as well as a word, so it reads at a glance. */
.notify__state {
  flex: none;
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  color: var(--text-faint);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.notify__state.is-on {
  border-color: rgba(70, 167, 88, 0.5);
  background: rgba(70, 167, 88, 0.14);
  color: var(--success);
}

.notify__where,
.notify__hint {
  font-size: 13px;
  color: var(--text-dim);
  min-width: 0;
}

.notify__hint {
  margin: 0;
}

.notify__kinds {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  border-top: 1px solid var(--border);
  padding-top: 10px;
}

.notify__label {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 8px 6px;
  border-radius: 8px;
  cursor: pointer;
}

.notify__label:hover {
  background: var(--bg-hover);
}

.notify__label input {
  accent-color: var(--accent);
  /* Line the box up with the first line of the label rather than its middle. */
  margin: 3px 0 0;
  flex: none;
  width: 17px;
  height: 17px;
}

.notify__kindname {
  display: block;
  font-size: 13.5px;
  font-weight: 600;
}

.notify__kinddetail {
  display: block;
  font-size: 12px;
  color: var(--text-faint);
}

.notify__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.notify__devices {
  font-size: 12px;
  color: var(--text-faint);
}

/* When push cannot work here, the reason gets the space, not a dead button. */
.notify__wall {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-hover);
  padding: 12px 14px;
}

.notify__walltitle {
  margin: 0 0 4px;
  font-size: 13.5px;
  font-weight: 700;
}

.notify__walldetail {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-dim);
}

@media (max-width: 860px) {
  .notify__row {
    flex-wrap: wrap;
  }

  .notify__row .btn {
    flex: 1 0 100%;
  }
}

/* Following a film ---------------------------------------------------------- */

/* Said where the spent Request button is, so nobody has to infer it. */
.detail__followhint {
  margin: 10px 0 0;
  padding: 10px 12px;
  border: 1px solid rgba(240, 173, 42, 0.3);
  border-radius: var(--radius);
  background: rgba(240, 173, 42, 0.07);
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-dim);
}

.event-detail__hint {
  margin: 12px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-faint);
}

.event-detail__hint strong {
  color: var(--text-dim);
}

/* Film or show, on each row of the manage list. */
.follow-row__kind {
  flex: none;
  padding: 1px 8px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* Profile ------------------------------------------------------------------- */

/* The chip is a control now, so it has to look like one and take a tap. */
.topbar__user {
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 4px 10px 4px 4px;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: background 0.14s ease, border-color 0.14s ease;
}

.topbar__user:hover {
  background: var(--bg-hover);
  border-color: var(--border);
}

.topbar__user[aria-current='page'] {
  border-color: var(--accent);
  background: rgba(229, 160, 13, 0.1);
}

.topbar__user:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.profile {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 620px;
}

.profile__id {
  display: flex;
  align-items: center;
  gap: 14px;
}

.profile__avatar {
  width: 56px;
  height: 56px;
  flex: none;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--border-strong);
}

.profile__who {
  min-width: 0;
}

.profile__name {
  margin: 0;
  font-size: 20px;
  line-height: 1.2;
}

.profile__kind {
  margin: 3px 0 0;
  font-size: 12.5px;
  color: var(--text-faint);
}

.profile__actions {
  display: flex;
  gap: 9px;
  flex-wrap: wrap;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

/* Signing out is not destructive, but it is the one thing here you would
   regret doing by accident — so it reads differently from its neighbour. */
.btn--danger-text {
  color: var(--danger);
}

.btn--danger-text:hover {
  border-color: var(--danger);
  color: var(--danger);
}

.profile__hint {
  margin: 0 0 10px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-dim);
}

.profile__copy {
  display: flex;
  gap: 8px;
  align-items: center;
}

.profile__copy .field__input {
  flex: 1;
  min-width: 0;
  font-size: 12.5px;
}

@media (max-width: 860px) {
  .profile {
    max-width: none;
  }

  .topbar__username {
    max-width: 8em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .profile__actions .btn {
    flex: 1 0 100%;
  }
}

/* The chip's fallback: initials where a Plex avatar would be. */
.avatar--initials {
  display: grid;
  place-items: center;
  background: var(--bg-hover);
  border: 1px solid var(--border-strong);
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

@media (max-width: 860px) {
  /* Nothing is left in the chip but the mark, so give it room to be pressed. */
  .topbar__user {
    padding: 4px;
  }
}

/* Update available ---------------------------------------------------------- */

/*
 * Sits above the bottom navigation rather than over it, and only ever for an
 * admin. Fixed, because news about the build is not part of any one page —
 * and it should not scroll away half-read.
 */
.updatebar {
  position: fixed;
  left: 50%;
  transform: translate(-50%, 140%);
  bottom: calc(
    var(--bottomnav-room, 70px) +
      max(var(--bottomnav-gap, 4px), calc(env(safe-area-inset-bottom, 0px) - var(--bottomnav-sink, 12px)))
  );
  width: min(560px, calc(100vw - 32px));
  z-index: 60;
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  opacity: 0;
  transition: transform 0.24s ease, opacity 0.24s ease;
}

.updatebar.is-in {
  transform: translate(-50%, 0);
  opacity: 1;
}

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

.updatebar__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 12px 12px 16px;
}

.updatebar__dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  /* The same "something is waiting" language the notification badge uses. */
  box-shadow: 0 0 0 4px rgba(229, 160, 13, 0.16);
}

.updatebar__text {
  flex: 1 1 auto;
  min-width: 0;
}

.updatebar__title {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
}

.updatebar__sub {
  margin: 2px 0 0;
  font-size: 12px;
  color: var(--text-dim);
  /* One line: a commit subject can be long and this is a bar, not a changelog. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.updatebar__close {
  flex: none;
  background: none;
  border: 0;
  color: var(--text-faint);
  font-size: 20px;
  line-height: 1;
  padding: 4px 6px;
  cursor: pointer;
  border-radius: 6px;
}

.updatebar__close:hover {
  color: var(--text);
  background: var(--bg-hover);
}

.update__head {
  margin: 0 0 10px;
  font-size: 19px;
}

.update__lead {
  margin: 0 0 14px;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.55;
}

.update__cmd {
  margin: 0;
  padding: 12px 14px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.6;
  /* Long enough to wrap on a phone; wrapping beats a sideways scroll in
     something the reader is about to select and copy. `anywhere` rather than
     `break-all` so it breaks between words where it can, and only splits a
     token like $(git rev-parse --short HEAD) when there is no other choice. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  user-select: all;
}

.update__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}

.update__note {
  margin: 16px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  color: var(--text-faint);
  font-size: 12px;
  line-height: 1.6;
}

/* Request in 4K ------------------------------------------------------------- */

/*
 * A peer of Request, not a rival to it.
 *
 * Request is the accent because it is what the sheet exists for; 4K is the
 * same action at a different quality, and two accent buttons side by side make
 * the reader choose before they have read either. So: the accent's hue at low
 * saturation, which reads as "related to that one" rather than "instead of it".
 */
/* The asking row sits tighter under the one above it: two halves of the same
   question, not two separate blocks of controls. */
.detail__actions--ask {
  margin-top: -2px;
}

.btn--fourk {
  background: rgba(229, 160, 13, 0.12);
  border: 1px solid rgba(229, 160, 13, 0.42);
  color: var(--accent-hover);
  font-weight: 600;
}

.btn--fourk:hover:not(:disabled) {
  background: rgba(229, 160, 13, 0.2);
  border-color: var(--accent);
}

.btn--fourk:disabled {
  /* Matches how the main Request button reads when it is spoken for: still
     legible, so the label can explain why rather than just looking broken. */
  background: transparent;
  border-color: var(--border-strong);
  color: var(--text-faint);
}

/* App version --------------------------------------------------------------- */

.appver__row {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}

.appver__lead {
  flex: 1 1 220px;
  min-width: 0;
}

.appver__now {
  display: block;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 14px;
  color: var(--text);
}

.appver__note {
  display: block;
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-dim);
}

/* Age rating ---------------------------------------------------------------- */

/*
 * A classification, drawn like one: boxed, upper case, in the muted colour the
 * rest of the meta line uses. It is a fact about who a title is for, not a
 * verdict on it, so it must not read as loudly as the score or the badge.
 *
 * Hidden until it arrives — it comes from a second call after the grid — and
 * hidden for good on anything TMDB has no rating for.
 */
.card__cert {
  display: inline-block;
  margin-left: 6px;
  padding: 0 4px;
  border: 1px solid var(--border-strong);
  border-radius: 3px;
  font-size: 10px;
  line-height: 15px;
  letter-spacing: 0.02em;
  color: var(--text-dim);
  vertical-align: 1px;
  white-space: nowrap;
}

.card__cert[hidden] {
  display: none;
}
