:root {
  /* The house look, and it is deliberately three colours deep: **black, then blue, then white**.
     The near-black pages carry the space, the blue is the one accent (links, buttons, badges, the
     glow behind the header) and the text is white. Every page wears it, the YouTube page included —
     the only red left there is the logo, which is attribution rather than a theme. `color-scheme`
     tells the browser which way round its own widgets are (scrollbars, the dropdown of a <select>,
     a date picker), so a light page does not keep a dark scrollbar. */
  color-scheme: dark;
  --bg: #05070d;
  --bg-panel: #0c121c;
  --bg-panel-top: #101725;
  --bg-raised: #141c2b;
  --bg-input: #070b13;
  --border: #1f2b3f;
  --text: #f4f7fb;
  /* The quieter text (captions, taglines, the small print under a panel). It is deliberately
     dimmer than --text but has to stay comfortably readable *small*, on the near-black page and
     on the lighter panels alike: this value is 9.7:1 on --bg and 9.0:1 on --bg-panel, so even
     the 0.78rem footer line clears AA by a wide margin. */
  --muted: #a7b5c9;
  /* The blue. On this dark page it sits at 7.7:1 on --bg, and the buttons carry near-black text
     on it (7.3:1) — the same light-accent/dark-ink pairing the palette has always used, which is
     also what keeps a filled button readable without a second shade. */
  --accent: #5aa2ff;
  --accent-strong: #3b82f6;
  /* the accent as channels so a tint can be built per theme (rgba(var(--accent-rgb), .12)) */
  --accent-rgb: 90, 162, 255;
  /* text on top of an accent-coloured surface */
  --accent-text: #04101f;
  /* accent-coloured text on a dark surface (a little lighter than the accent itself) */
  --accent-tint: #bcd7ff;
  --good: #4ade80;
  --good-rgb: 74, 222, 128;
  --warn: #fbbf24;
  --warn-rgb: 251, 191, 36;
  --bad: #fb7185;
  --bad-rgb: 251, 113, 133;
  --tint-good: #baf3c9;
  --tint-warn: #f7e2b4;
  --tint-bad: #ffd0d7;
  /* The two menu icons that carry a colour of their own: the blue of the Movies & TV chip on the
     home page (the same blue as the accent, and it turns red with the rest of the page on
     `youtube.html`) and a red for the support heart. Pinned here rather than taken from
     `--accent`, so the heart keeps its own colour whatever the accent is doing. */
  --menu-tv: #5aa2ff;
  --menu-support: #fb7185;
  --radius: 16px;
  --radius-pill: 999px;
  /* The page frame: one width and one gutter for the header, the content and the footer, so the
     three of them line up whatever the window is doing. The gutter is fluid, which is what keeps a
     320px phone from paying the same 24px as a desktop. */
  --container: 1120px;
  --gutter: clamp(14px, 3.4vw, 28px);
  /* the page seen through the sticky header once it has folded away */
  --bg-blur: rgba(5, 7, 13, 0.78);
  --glow: rgba(var(--accent-rgb), 0.16);
  /* panels and cards (--shadow), the dropdowns and the open menu (--shadow-pop), the lift of
     a home card under the pointer (--shadow-lift) */
  --shadow: 0 14px 30px rgba(0, 0, 0, 0.28);
  --shadow-pop: 0 12px 28px rgba(0, 0, 0, 0.45);
  --shadow-lift: 0 18px 34px rgba(0, 0, 0, 0.32);
}

/* The light palette turns the whole thing round: **white, then blue, then black**. The same
   tokens, so every tint built from them follows along. The accent darkens (blue 700 instead of
   the blue 400 the dark page uses) because the dark-page blue is unreadable on white, and
   `--accent-text` turns white to sit on top of it. The `--tint-*` colours are the readable ink
   for text on a light tint. */
html[data-theme="light"] {
  color-scheme: light;
  --bg: #f7f9fc;
  --bg-panel: #ffffff;
  --bg-panel-top: #ffffff;
  --bg-raised: #f2f6fb;
  --bg-input: #ffffff;
  --border: #dbe3ee;
  --text: #0b1220;
  --muted: #526079;
  --accent: #1d4ed8;
  --accent-strong: #1e40af;
  --accent-rgb: 29, 78, 216;
  --accent-text: #ffffff;
  --accent-tint: #1e40af;
  --good: #15803d;
  --good-rgb: 21, 128, 61;
  --warn: #a16207;
  --warn-rgb: 161, 98, 7;
  --bad: #dc2626;
  --bad-rgb: 220, 38, 38;
  --tint-good: #14532d;
  --tint-warn: #713f12;
  --tint-bad: #7f1d1d;
  --menu-tv: #1d4ed8;
  --menu-support: #dc2626;
  --bg-blur: rgba(247, 249, 252, 0.82);
  --glow: rgba(var(--accent-rgb), 0.1);
  --shadow: 0 14px 30px rgba(8, 20, 45, 0.1);
  --shadow-pop: 0 12px 28px rgba(8, 20, 45, 0.18);
  --shadow-lift: 0 18px 34px rgba(8, 20, 45, 0.16);
}

* { box-sizing: border-box; }

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

/* The `hidden` attribute must win from every class that sets a display, otherwise
   a hidden panel stays on the page (it did: the "using" list picker). */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Segoe UI", Roboto, system-ui, -apple-system, sans-serif;
  font-size: 16px;
  line-height: 1.5;
  /* A long title or e-mail address wraps instead of pushing the page sideways on a narrow window. */
  overflow-wrap: break-word;
  /* iOS inflates text in landscape without this, which would undo the fluid sizes below. */
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  /* ⚠️ No scroll anchoring, and that is a bug fix, not taste. The sticky header folds its tagline away
     (html.is-scrolled), which makes the document about 24px shorter. Chrome's scroll anchoring answers a
     height change above the anchor by moving the scroll position with it - so the class flipped the moment
     the position came back within reach of the 24px threshold, and the header flickered forever (measured
     on youtube.html: scroll position 28 → 10 → 27 → 0 while the height swung 2099 ↔ 2123). The hysteresis
     in static/menu.js is the second half of that fix. */
  overflow-anchor: none;
}

/* Anything the page scrolls to lands below the sticky bar instead of under it. `scroll-margin-top`
   is what a browser uses for a #fragment, for `scrollIntoView` and for focusing an element, so this
   covers both the links (`movies.html#howCalc`, `youtube.html#howTube`, `#signin`) and the panels the
   app scrolls to itself — the results panel after a run and the blocked list from the account menu.
   Those two used to land with their heading behind the bar, because a scripted `scrollIntoView` is
   not a `:target` and had no margin of its own.
   Measured with transitions off (a transition makes `getComputedStyle` return the *animating* value):
   the compact bar is the title row plus one row of nav links, which measures 118-181px depending on
   the width (the quick links wrap to a second line at 320px), so the offset is 132px in the wide range
   and 190px in the narrow one. */
:root { --header-offset: 132px; }

/* The compact bar plus the quick-nav row is about 185px at this width (measured with transitions
   off), so 190 leaves a few pixels of air instead of letting a heading land just behind the border. */
@media (max-width: 767px) {
  :root { --header-offset: 190px; }
}

:target,
main > *,
.sub-details { scroll-margin-top: var(--header-offset); }

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* The header sticks, so the menu button and the account pills stay within reach while the page
   scrolls. `html.is-scrolled` (static/menu.js) lands on <html> after the first 24px of scrolling and
   folds the tagline away, which is what makes the bar cost about a third of the room it takes at the
   top of the page. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 30;
  max-width: var(--container);
  margin: 0 auto;
  padding: clamp(18px, 2.6vw, 30px) var(--gutter) 10px;
  border-bottom: 1px solid transparent;
  transition: padding 0.22s ease-out, background 0.22s ease-out, border-color 0.22s ease-out;
}

html.is-scrolled .site-header {
  padding-top: 10px;
  padding-bottom: 10px;
  background: var(--bg-blur);
  border-bottom-color: var(--border);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

/* Links in the copy (the panels, the footer) had no colour of their own and came out in the
   browser's dark blue on a near-black background — see the accent instead. */
a {
  color: var(--accent);
  text-underline-offset: 3px;
}

a:hover { color: var(--accent-tint); }

/* The first thing the keyboard reaches: a way past the sticky header and the nav. It is only
   visible while focused, so it costs the layout nothing. */
.skip-link {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: 100;
  padding: 10px 14px;
  border-radius: 8px;
  background: var(--accent);
  color: var(--accent-text);
  font-weight: 600;
  text-decoration: none;
  transform: translateY(-160%);
  transition: transform 0.15s ease-out;
}

.skip-link:focus { transform: none; }

/* A soft band of accent light behind the header — decoration only. It fills the header box
   exactly: a negative inset made the page wider than the viewport (a pseudo-element counts
   towards the scrollable area), and a taller one would paint over the first panel, because
   a positioned pseudo-element paints above a non-positioned sibling's background. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(58% 130% at 18% 0%, var(--glow), transparent 72%);
  pointer-events: none;
  z-index: -1;
}

/* One control height for the whole bar: the menu button, the account pills and the title line.
   The row is aligned to the *top* on purpose (so the tagline can hang below the title without
   pushing the controls down), which means every item centres on its own height - and without a
   shared one the title (24.8px in the compact bar) sat 8.6px above the 42px menu button.
   `--mark-size` is the site mark beside the brand on the home page: square, and never taller than a
   control, so the line it sits in stays the same height as the buttons. */
:root { --control-h: 42px; --mark-size: 1.6em; }

/* Every page keeps this header on one row: the menu button, the title beside it, and the account
   area on the right where there is one. `nowrap` is what puts the button beside the title instead
   of above it; the brand shrinks to make room rather than let the row break. */
.header-inner {
  display: flex;
  gap: 18px;
  align-items: flex-start;
  flex-wrap: nowrap;
}

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

/* ------------------------------------------------ the menu button and its panel */

.menu-wrap { position: relative; }

.menu-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-h);
  height: var(--control-h);
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-panel);
  color: var(--text);
  cursor: pointer;
}

.menu-btn:hover { background: var(--bg-raised); border-color: var(--accent); }
.menu-btn[aria-expanded="true"] { border-color: var(--accent); color: var(--accent); }
.menu-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.site-menu {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 272px;
  /* A short landscape phone (568x320) cut the theme item off: the panel is 298px tall and there
     is no room below the 60px stick header. Cap it to the viewport and let it scroll instead. */
  max-height: calc(100dvh - 76px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 8px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: 8px;
  color: var(--text);
  text-decoration: none;
  font-size: 0.94rem;
}

.menu-item:hover { background: rgba(var(--accent-rgb), 0.12); text-decoration: none; }
.menu-item.current { color: var(--muted); cursor: default; }
.menu-item.current:hover { background: transparent; }
.menu-item.current .menu-text { font-weight: 600; }

/* One fixed column width on every row, so all the labels start at the same place. */
.menu-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
}

/* Two of the menu icons keep a colour of their own: the TV is drawn in the accent blue (<html> at
   home, red on the YouTube page), the support heart in red. The <svg> draws with
   stroke="currentColor", so setting the colour here colours the line itself — there is no box
   around the icon, just the icon in its own colour. */
.menu-icon--tv { color: var(--menu-tv); }
.menu-icon--heart { color: var(--menu-support); }

/* ------------------------------------------------------- the theme switch (in the menu) */

/* The switch is a <button>, but it has to look exactly like the links around it. */
.site-menu button.menu-item {
  width: 100%;
  margin-top: 6px;
  padding: 9px 10px;
  border: 0;
  border-top: 1px solid var(--border);
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* The button says where it takes you, not where you are: the sun while the page is dark, the
   moon while it is light. Both icons and both labels sit in the markup and CSS picks one, so
   the label is right on the very first paint (no flash of "Light theme" on a light page). */
.theme-item .icon-moon { display: none; }
.theme-item .theme-to-dark { display: none; }

html[data-theme="light"] .theme-item .icon-sun { display: none; }
html[data-theme="light"] .theme-item .icon-moon { display: block; }
html[data-theme="light"] .theme-item .theme-to-light { display: none; }
html[data-theme="light"] .theme-item .theme-to-dark { display: inline; }

/* The badge in front of the page name (the TV, the info dot, the YouTube play icon). It is a
   flex item of .h1-sub, which centres it on the middle of the title, so it needs no baseline
   tweak of its own. */
.page-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}

/* the drawn badges are a square 24x24 box; the artwork inside keeps about 12% room around it,
   so the box comes out a little larger than the part you actually see */
.page-badge svg { width: 1.3em; height: 1.3em; }

.account-area {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 0.88rem;
  color: var(--muted);
  margin-left: auto;
  justify-content: flex-end;
  text-align: right;
  /* the pills keep their own (smaller) height, but sit on the bar's centre line like the title */
  min-height: var(--control-h);
}

/* The two ways in (and, once somebody is signed in, the account link) wear a filled pill
   of their own rather than the transparent ghost style, so the top-right corner is visible
   at a glance instead of blending into the header. Both an <a> and a <button> can be one. */
a.account-btn,
button.account-btn,
summary.account-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 17px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--bg-panel);
  color: var(--text);
  font: inherit;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.15s;
}

a.account-btn:hover,
button.account-btn:hover,
summary.account-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
  text-decoration: none;
}

/* "Create an account" is the one call to action in the header, so it is the accent-filled
   pill — the same shape the primary buttons use further down the page. */
a.account-btn--primary,
button.account-btn--primary {
  border-color: transparent;
  background: var(--accent);
  color: var(--accent-text);
  box-shadow: 0 6px 16px rgba(var(--accent-rgb), 0.26);
}

a.account-btn--primary:hover,
button.account-btn--primary:hover {
  background: var(--accent-strong);
  color: var(--accent-text);
  transform: translateY(-1px);
}

/* The signed-in form: an avatar tile plus the e-mail, as one link to the reference lists. */
.account-btn--who { max-width: 250px; }

.account-avatar {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(var(--accent-rgb), 0.24);
  color: var(--accent-tint);
  font-size: 0.78rem;
  font-weight: 700;
}

.account-email {
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
}

a.account-btn--who:hover .account-email { color: inherit; }

/* The signed-in corner is a menu: the avatar and the e-mail open a small list of the three
   places your data lives, plus the way out. <details> needs no JavaScript to open, and a
   page whose markup was written before this file ran keeps working as plain links. */
.account-menu { position: relative; }

.account-menu > summary {
  list-style: none;
  cursor: pointer;
}

.account-menu > summary::-webkit-details-marker { display: none; }

.account-menu[open] > summary {
  border-color: var(--accent);
  color: var(--accent);
}

.account-menu-list {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 40;
  min-width: 216px;
  display: flex;
  flex-direction: column;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-panel);
  box-shadow: 0 18px 42px rgba(0, 0, 0, 0.38);
}

a.account-menu-link,
button.account-menu-link {
  display: block;
  width: 100%;
  padding: 9px 12px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text);
  font: inherit;
  font-weight: 500;
  text-align: left;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

a.account-menu-link:hover,
button.account-menu-link:hover {
  background: rgba(var(--accent-rgb), 0.16);
  color: var(--accent-tint);
  text-decoration: none;
}

button.account-menu-link.danger { color: var(--bad); }

button.account-menu-link.danger:hover { background: rgba(var(--bad-rgb), 0.16); }

/* The title line: the brand name plus the page name. It is a flex row so the page name and
   its badge centre on the middle of the brand instead of sitting on its baseline.
   `.brand-name` is the same line on the home page, where the brand is not a heading any more:
   the page's <h1> is the promise in the hero, so the brand is navigation and not structure. */
.site-header h1,
.site-header .brand-name {
  margin: 0;
  font-size: clamp(1.5rem, 1.15rem + 1.7vw, 2.1rem);
  letter-spacing: -0.4px;
  line-height: 1.15;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 18px;
  transition: font-size 0.22s ease-out;
  /* The line sits on the same centre line as the menu button and the account pills: half of the
     difference between a control's height and one line of the title, as *padding* rather than
     `min-height`, so it also holds when the page title wraps to a second line (a taller box would
     leave the first line where it was - above the buttons). `em` is the h1's own font size, so this
     follows the fluid clamp above. */
  padding: calc((var(--control-h) - 1.15em) / 2) 0;
}

/* The home page has no page name: its line is the brand plus the mark, and the mark is exactly as
   tall as a control (see `--mark-size`), so there the line already *is* the right height and it
   only has to be centred - the shift above would push it 1.7px past the buttons. */
body.page-home .site-header h1,
body.page-home .site-header .brand-name {
  min-height: var(--control-h);
  padding: 0;
}

html.is-scrolled .site-header h1,
html.is-scrolled .site-header .brand-name { font-size: 1.35rem; }

/* The tagline folds away once the header sticks: max-height is what makes that smooth (fading it
   alone would leave the empty room behind), and 8em is more than the two lines it ever needs. */
.tagline {
  color: var(--muted);
  max-width: 72ch;
  margin: 8px 0 0;
  font-size: clamp(0.94rem, 0.9rem + 0.25vw, 1.05rem);
  max-height: 8em;
  transition: max-height 0.22s ease-out, opacity 0.16s ease-out, margin 0.22s ease-out;
}

html.is-scrolled .tagline {
  max-height: 0;
  margin-top: 0;
  opacity: 0;
  overflow: hidden;
}

/* A window that is wide but short — a phone in landscape, or a browser window someone has dragged
   to half height — cannot afford the tall header either, so it starts compact instead of waiting for
   the first scroll. The tagline is the one thing that goes: it is the only block up here that is not
   a control. */
@media (max-height: 520px) {
  .site-header { padding-top: 10px; padding-bottom: 10px; }
  .site-header h1,
  .site-header .brand-name { font-size: 1.35rem; }
  .tagline { display: none; }
}

/* The site mark beside the brand on the home page: the same file the browser tab shows, so the two
   can never drift apart. In `em`, so it scales with the title instead of being a second size to
   keep in sync. */
.brand-mark {
  flex: 0 0 auto;
  display: block;
  /* square, and never taller than a control in the bar it shares with the menu button */
  height: min(var(--mark-size), var(--control-h));
  width: auto;
}

/* The "why an account" panel on the home page: the benefits as a short list, then the two
   ways in — the same pills the header uses, so the corner and the panel match. */
.account-promo-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin: 16px 0 10px;
}

.account-promo-note { margin: 0; }

/* ------------------------------------------------------ the home page cards */

.home-cards {
  display: grid;
  /* Three pages, so the cards wrap instead of a hard two-column grid: three across when the content
     column is wide enough, two below that, one on a phone (see the container query at the end). */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 18px;
}

.home-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 24px;
  background: linear-gradient(180deg, var(--bg-panel-top), var(--bg-panel));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  text-decoration: none;
  transition: border-color 0.15s, transform 0.15s, box-shadow 0.2s;
}

.home-card:hover {
  border-color: var(--accent);
  transform: translateY(-3px);
  box-shadow: var(--shadow-lift), 0 0 0 4px rgba(var(--accent-rgb), 0.1);
  text-decoration: none;
}

.home-card h3 {
  margin: 0;
  font-size: 1.25rem;
}

.home-card p {
  margin: 0;
  color: var(--muted);
  font-size: 0.92rem;
}

.home-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: 16px;
  background: rgba(var(--accent-rgb), 0.14);
  color: var(--accent);
  line-height: 1;
}

/* The channels card wears the official YouTube icon. YouTube's Branding Guidelines do not allow
   altering, covering or shadowing the mark and ask for a plain backdrop behind it, so the chip
   stays uncoloured — a translucent red one read as a red glow around the logo. */
.home-card--yt .home-icon { background: none; }

.home-go {
  margin-top: auto;
  padding-top: 10px;
  color: var(--accent);
  font-weight: 600;
  font-size: 0.92rem;
}

.home-card:hover .home-go { text-decoration: underline; }

/* The two cards stack as soon as the content column is narrow, whatever the window is doing. */
@container main (max-width: 620px) {
  .home-cards { grid-template-columns: 1fr; }
}

/* a word in the copy that explains itself when you hover over it (native tooltip
   from the title attribute) — the dotted underline is the hint to hover */
.term-hint {
  cursor: help;
  border-bottom: 1px dotted var(--accent);
}

/* the brand name links back to the home page on every page except the home page */
.brand-link {
  color: inherit;
  text-decoration: none;
}

.brand-link:hover { text-decoration: underline; text-underline-offset: 4px; }

/* `main` is the container the card grids and the side-by-side panels are measured against, so a
   panel answers to its own width instead of to the window — which is exactly what a half-screen
   window, a resized window or a phone changes. */
main {
  container-type: inline-size;
  container-name: main;
  max-width: var(--container);
  margin: 0 auto;
  padding: 8px var(--gutter) 48px;
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 2vw, 22px);
}

.panel {
  background: linear-gradient(180deg, var(--bg-panel-top), var(--bg-panel));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: clamp(16px, 2.4vw, 24px);
  box-shadow: var(--shadow);
  /* A container of its own: the field grids inside it switch to one column when *the panel* is
     narrow, which happens in a side-by-side layout long before the window itself is small. */
  container-type: inline-size;
  container-name: panel;
}

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 14px;
}

.panel-head h2 {
  margin: 0;
  font-size: 1.15rem;
  display: flex;
  align-items: center;
  gap: 10px;
}

.step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-text);
  font-size: 0.85rem;
  font-weight: 700;
}

.counter {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--accent);
}

.counter-max { color: var(--muted); }

/* counter plus the +/− collapse button of a long list */
.panel-head-right {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* The page name next to the brand: nearly as big as the brand itself and centred on the
   middle of it. Everything here is in `em`, so this one number sets the size of the text
   and of the badge in front of it together. */
.h1-sub {
  font-size: 0.85em;
  font-weight: 500;
  color: var(--muted);
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

/* On a narrow screen the bar stays one row: the menu button, then the brand, then the account
   pills on the right. The tagline is dropped (the page's own lede repeats the promise) and the
   title steps down, so the bar costs about 110px instead of a third of the screen. `order` keeps
   the three groups in that sequence when the markup order would differ. */
@media (max-width: 700px) {
  .site-header { padding-top: 12px; }
  .header-inner { flex-wrap: nowrap; gap: 10px 14px; }
  .menu-wrap { order: 1; }
  .brand { order: 2; flex: 1 1 auto; min-width: 0; }
  .account-area { order: 3; flex: 0 0 auto; margin-left: 0; justify-content: flex-end; font-size: 0.85rem; }
  .site-header h1, .site-header .brand-name { font-size: 1.45rem; }
  .site-header h1 { gap: 4px 10px; }
  .tagline { display: none; }
  a.account-btn, button.account-btn, summary.account-btn { min-height: 42px; padding: 9px 14px; font-size: 0.85rem; }
  /* Touch targets: the per-row controls and small actions were 26-36px. These live at the end of
     the file because `.icon-btn` is defined later; a media query does not raise specificity. */
}

@media (max-width: 520px) {
  /* One row is the point of the bar: the brand sits between the menu button and Sign in, so on a
     phone the secondary "Create an account" pill steps out. It stays one click away from Sign in
     and the home page keeps its own "Create a free account" button. */
  .account-area .account-btn--primary { display: none; }
}

@media (max-width: 360px) {
  /* At 320px the name and the mark stopped fitting on one line and the logo wrapped under the
     text. A slightly smaller title (and a mark in em) keeps the brand on the row. */
  .site-header h1, .site-header .brand-name { font-size: 1.2rem; }
  .brand-mark { height: min(1.5em, 38px); }
}

@media (max-width: 400px) {
  a.account-btn, button.account-btn, summary.account-btn { padding: 8px 11px; font-size: 0.8rem; }
}

.muted { color: var(--muted); }
.small { font-size: 0.85rem; }

/* inline code in the explanatory copy (paths, localStorage keys, CLI commands) */
code {
  font-family: "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace;
  font-size: 0.86em;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 1px 5px;
}

/* ------------------------------------------------------------------ search */

.search { position: relative; }

.search input {
  width: 100%;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text);
  font-size: 1rem;
}

.search input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.15);
}

.search-results {
  list-style: none;
  margin: 6px 0 0;
  padding: 6px;
  position: absolute;
  z-index: 20;
  left: 0;
  right: 0;
  max-height: 320px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-pop);
}

/* static/menu.js flips the list above the box when there is more room there (a phone keyboard,
   or a search box low on the page) and caps the height to the room that is left. */
.search-results--up {
  top: auto;
  bottom: calc(100% + 6px);
  margin: 0 0 6px;
}

.search-results li {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 8px;
  cursor: pointer;
}

/* One highlighted row, and it is always the row the cursor is on: the arrow keys move that cursor and so
   does the pointer (mousemove in static/app.js). A `:hover` background of its own is deliberately NOT
   used here - with it, the row under a resting pointer and the row the arrows had moved to were both lit
   at once (2026-10-16). */
.search-results li.active { background: rgba(var(--accent-rgb), 0.16); }

.search-results .meta {
  color: var(--muted);
  font-size: 0.85rem;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-empty {
  padding: 10px;
  color: var(--muted);
  font-size: 0.9rem;
}

/* The line under the channel search that explains what you can paste. It is instruction, not fine
   print: an accent-tinted callout with the path examples in monospace, so it is the thing the eye
   lands on before the field instead of the quietest line on the page. */
.search-note {
  margin: 10px 0 0;
  padding: 10px 12px;
  border: 1px solid rgba(var(--accent-rgb), 0.35);
  border-left: 3px solid var(--accent);
  border-radius: 10px;
  background: linear-gradient(115deg, rgba(var(--accent-rgb), 0.16), rgba(var(--accent-rgb), 0.04));
  color: var(--accent-tint);
  font-size: 0.92rem;
  line-height: 1.5;
}

.search-note code {
  color: var(--text);
  background: rgba(var(--accent-rgb), 0.16);
  border-color: rgba(var(--accent-rgb), 0.35);
  white-space: nowrap;
}

/* The shared-key budget is spent (or Google's real quota for that key is gone, which is counted
   across every visitor). Orange, at the top of the page, with the one action that helps. */
.quota-warning {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 8px 12px;
  margin: 0 0 16px;
  padding: 12px 14px;
  border: 1px solid rgba(var(--warn-rgb), 0.55);
  border-left: 3px solid var(--warn);
  border-radius: 12px;
  background: linear-gradient(115deg, rgba(var(--warn-rgb), 0.16), rgba(var(--warn-rgb), 0.05));
  color: var(--tint-warn);
  font-size: 0.9rem;
  line-height: 1.5;
}

.quota-warning p {
  flex: 1 1 320px;
  margin: 0;
  max-width: 66ch;
}

.quota-warning strong { color: var(--warn); }

.quota-warning-icon {
  font-size: 1.1rem;
  line-height: 1.4;
}

.quota-warning button {
  flex: 0 0 auto;
  margin-left: auto;
}

@media (max-width: 600px) {
  .quota-warning button { width: 100%; margin-left: 0; }
}

/* Rows under the search box that cannot be picked: the "Popular right now" head that
   appears when the box is focused but still empty, and the note that says what is going on
   (the complete catalog is still on its way, the title is not in the catalog at all, ...). */
.search-results li.search-head {
  cursor: default;
  color: var(--muted);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 4px 10px 2px;
}

.search-results li.search-note {
  cursor: default;
  color: var(--muted);
  font-size: 0.9rem;
  padding: 10px;
}

/* A title from the complete set whose details are still being fetched. */
.search-results li.busy { opacity: 0.55; }
.search-results li.busy::after { content: ' …'; }

/* ------------------------------------------------------------- rated titles */

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

.rated-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
}

.row-head {
  display: flex;
  align-items: center;
  gap: 10px;
}

.row-head .rated-main { flex: 1 1 auto; }

.row-controls {
  display: flex;
  align-items: center;
  gap: 10px;
}

.row-controls input[type="range"] { flex: 1 1 auto; min-width: 0; }

.score-input {
  width: 58px;
  flex: 0 0 auto;
  padding: 6px 8px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text);
  font-size: 0.95rem;
  font-weight: 600;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.score-input:focus { outline: none; border-color: var(--accent); }
.score-input.small { width: 52px; padding: 4px 6px; font-size: 0.9rem; }

.smiley {
  font-size: 1.5rem;
  line-height: 1;
  flex: 0 0 auto;
  filter: saturate(1.1);
}

/* The star eyes of the 10, and in the emoji font of most desktops those stars sit in the same red family
   as the heart eyes of the 9 below it, so the two best scores blur together. `wtwSmileyStars` (built by
   static/smiley.js) keeps the pixels whose red runs far ahead of their green and paints those in the
   site's violet - the face, the outline and the highlights are left exactly as the font drew them.
   Must stay after the rule above: same specificity, so the source order decides which filter wins. */
.smiley--stars { filter: saturate(1.1) url(#wtwSmileyStars); }

/* The same script measures the emoji and draws what the artwork cannot say: it paints over the tongue of
   a grinning face (in the colour of the mouth around it) and wipes the whole star of the 10 and redraws it
   bigger about its inner tip, so its points run past the face on the upper left and right and the score
   still reads as two eyes. It hangs on the smiley's own origin, with `left` and `top` in em set inline,
   so the overlay follows the smiley at any size and never takes a click meant for the row behind it. */
.smiley-overlay { position: absolute; left: 0; top: 0; pointer-events: none; }

/* ---------------------------------------------------------------- list picker */

.list-bar {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.list-picker {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 0.85rem;
  color: var(--muted);
}

.list-picker select {
  padding: 9px 11px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text);
  font-size: 0.95rem;
  min-width: 190px;
}

.suggest-list {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.9rem;
  color: var(--muted);
}

.suggest-list select {
  padding: 11px 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-panel);
  color: var(--text);
  font-size: 0.95rem;
}

.rated-main { min-width: 0; }

.rated-title { font-weight: 600; }

/* ------------------------------------------------------------ love/hate lists */

.add-bar {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 4px;
}

.grow { flex: 1 1 320px; }

.segmented {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  flex: 0 0 auto;
}

.segmented .seg {
  background: transparent;
  border: 0;
  color: var(--muted);
  padding: 11px 16px;
  font-size: 0.92rem;
  cursor: pointer;
  font-weight: 600;
  white-space: nowrap;
}

.segmented .seg + .seg { border-left: 1px solid var(--border); }
.segmented .seg:hover { color: var(--text); }
.segmented .seg.active { background: rgba(var(--accent-rgb), 0.2); color: var(--text); }
.segmented.small .seg { padding: 7px 11px; font-size: 0.9rem; }

.list-columns {
  display: grid;
  gap: clamp(16px, 2vw, 20px);
  grid-template-columns: minmax(0, 1fr);
}

/* Two columns from 760px of content. auto-fit, not a fixed two: a page whose two panels became one
   (movies.html keeps only the reference list in here) then gets the full width instead of half. */
@container main (min-width: 760px) {
  .list-columns { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); align-items: start; }
}

.mini-buttons { display: inline-flex; gap: 6px; }

.sub-details {
  margin-top: 18px;
  border-top: 1px solid var(--border);
  padding-top: 12px;
  /* also a container: the field grids inside it (the channel filters) answer to its width, not to
     the window, and this panel is not a .panel or a .how */
  container-type: inline-size;
  container-name: panel;
}

.sub-details summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.9rem;
}

.result-actions {
  display: flex;
  gap: 8px;
  margin-top: 12px;
  flex-wrap: wrap;
}

/* ------------------------------------------------------------------- filters */

.filter-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  position: relative;
}

.filter-icon { display: inline-flex; }

.filter-badge {
  background: var(--accent);
  color: var(--accent-text);
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 700;
  min-width: 19px;
  padding: 0 6px;
  text-align: center;
}

.filter-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 4px;
}

.filter-label { font-size: 0.85rem; color: var(--muted); }

.genre-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  max-height: 230px;
  overflow-y: auto;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-input);
}

.genre-chip {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 999px;
  padding: 5px 11px;
  font-size: 0.8rem;
  cursor: pointer;
}

.genre-chip:hover { border-color: var(--accent); }

.genre-chip.excluded {
  background: rgba(var(--bad-rgb), 0.18);
  border-color: var(--bad);
  color: var(--tint-bad);
  text-decoration: line-through;
}

.genre-chip .count { color: var(--muted); font-size: 0.72rem; margin-left: 5px; }

.genre-chips-empty { color: var(--muted); font-size: 0.82rem; padding: 4px; }

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

.filter-summary { margin: 0; }

/* --------------------------------------------------- posters (enrichment only) */

/* `.result-card` itself is two columns; a poster is not part of the markup today (posters were
   removed with TMDB). This rule used to switch every card to three columns and was overridden by
   the later `.result-card` rule, so it silently did nothing — a card with a poster added now
   breaks the grid. Keep the size for a future enrichment, but do not change the grid here.
 */
.result-poster {
  width: 66px;
  height: 99px;
  border-radius: 8px;
  object-fit: cover;
  background: var(--bg-input);
  display: block;
}

.attribution {
  display: block;
  color: var(--muted);
  font-size: 0.78rem;
}

.attribution a { color: var(--accent); }

/* The YouTube channel avatar in a suggestion card reuses .result-poster above. */

.rated-meta {
  display: block;
  color: var(--muted);
  font-size: 0.82rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rated-row input[type="range"] {
  width: 100%;
  accent-color: var(--accent);
}

.rating-value {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  text-align: center;
  color: var(--accent);
}

.icon-btn {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--muted);
  border-radius: 8px;
  width: 30px;
  height: 30px;
  cursor: pointer;
  font-size: 0.9rem;
  line-height: 1;
}

.icon-btn:hover { color: var(--bad); border-color: var(--bad); }

.icon-btn:disabled {
  opacity: 0.35;
  cursor: default;
  color: var(--muted);
  border-color: var(--border);
}

/* The list is a ranking: #1 weighs most, so every row shows its place and the
   two arrows that move it. */
.rank-badge {
  flex: 0 0 auto;
  min-width: 26px;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--accent);
  font-size: 0.9rem;
}

.rank-buttons { display: inline-flex; gap: 4px; }

.rank-buttons .icon-btn { width: 26px; height: 26px; font-size: 0.68rem; }

.rank-buttons .icon-btn:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }

/* ------------------------------------------------------------------- ai box */

.ai-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  margin-top: 14px;
}



.ai-grid label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 0.85rem;
  color: var(--muted);
}

.ai-grid label.wide { grid-column: 1 / -1; }

.ai-grid input,
.ai-grid select {
  padding: 9px 11px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text);
  font-size: 0.95rem;
}

.ai-grid input:focus,
.ai-grid select:focus {
  outline: none;
  border-color: var(--accent);
}

.remember {
  flex-direction: row !important;
  align-items: center;
  gap: 8px !important;
}

/* The AI panel's own lines: the "sign in first" note, the model-list status and the
   "your key, your bill" links. Small and muted, red only when something failed. */
.ai-note {
  margin: 10px 0 0;
  font-size: 0.85rem;
  color: var(--muted);
}

.ai-note.error { color: var(--bad); }

.ai-note a,
#aiHelp a { color: var(--accent); }

#aiLocked {
  margin: 10px 0 0;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  font-size: 0.88rem;
}

#aiLocked button { margin: 0 6px; }

.ai-grid button:disabled,
.ai-grid input:disabled,
.ai-grid select:disabled { opacity: 0.55; }

/* The "use AI re-ranking" switch is outside the grid; dim its text with the rest when locked. */
.inline-switch input:disabled + span { opacity: 0.55; }

.switch {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.9rem;
  color: var(--muted);
  cursor: pointer;
}

/* ------------------------------------------------------------------ actions */

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

button.primary,
button.ghost {
  padding: 13px 24px;
  border-radius: var(--radius-pill);
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.15s;
}

button.primary {
  background: var(--accent);
  color: var(--accent-text);
  box-shadow: 0 6px 18px rgba(var(--accent-rgb), 0.22);
}

button.primary:hover { background: var(--accent-strong); transform: translateY(-1px); }
button.primary:disabled { opacity: 0.55; cursor: progress; box-shadow: none; transform: none; }

/* A primary button for something irreversible ("Delete this list"): the same pill in the warning
   colour. `--bad` is light in the dark palette and dark in the light one, exactly like `--accent`,
   so `--accent-text` (near-black / white) stays the readable ink on top of it in both. */
button.primary.danger { background: var(--bad); }
button.primary.danger:hover { background: var(--bad); filter: brightness(0.92); }

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

button.ghost:hover { border-color: var(--accent); color: var(--accent); }
button.ghost.small { padding: 9px 15px; font-size: 0.85rem; }

/* --------------------------------------------------------------- run history */

/* The line that says when the previous run was and what this one changed. */
.run-note {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 12px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 10px;
  background: rgba(var(--accent-rgb), 0.08);
}

.run-note p { margin: 0; flex: 1 1 320px; }
.run-note button { flex: 0 0 auto; }

/* ------------------------------------------------------------------ results */

/* `min(100%, 320px)` keeps a single card from overflowing a 320px window: with a plain 320px
   minimum the grid cannot shrink below the viewport and the page scrolls sideways. */
.results {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
  gap: 12px;
}

/* Step 2 before the first run. The panel is on the page from the start — without it the visible
   steps read "1 … 3" and nobody knows where the ten picks will land. */
.results-empty {
  margin: 0;
  padding: 16px 0 2px;
  color: var(--muted);
}

.result-card {
  display: grid;
  grid-template-columns: 74px minmax(0, 1fr);
  gap: 14px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 14px;
  transition: border-color 0.15s;
}

.result-card:hover { border-color: rgba(var(--accent-rgb), 0.5); }

.result-percent {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  padding: 6px;
  /* A soft tint plus the title's initials behind the score, so the wide column reads as artwork
     instead of an empty box. The hue comes from the title (see titleHue/titleInitials in app.js).
     Opaque on purpose: a white score on a translucent pastel tile was unreadable in the light theme. */
  background: linear-gradient(
    155deg,
    hsl(var(--art-h, 215) 48% 30%),
    hsl(calc(var(--art-h, 215) + 35) 45% 16%)
  );
}

.result-art {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 12px;
  font-size: 1.45rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  color: rgba(255, 255, 255, 0.24);
  pointer-events: none;
}

.result-percent strong,
.result-percent span { position: relative; }

.result-percent strong {
  font-size: 1.45rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  color: #fff;
}

.result-percent span { font-size: 0.7rem; color: rgba(255, 255, 255, 0.76); }

.result-title { font-weight: 600; margin: 0 0 2px; }

.result-meta { color: var(--muted); font-size: 0.82rem; }

.result-genres {
  margin: 6px 0 0;
  font-size: 0.82rem;
  color: var(--muted);
}

/* The genre line of a suggestion carries the whole list and stops exactly where the
   card runs out of room, so the free space on the line is used before the "…" appears
   (a genre is not cut at a fixed number any more). */
.result-genres-fit {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.result-reason {
  margin: 8px 0 0;
  font-size: 0.82rem;
  color: var(--muted);
  border-left: 2px solid var(--border);
  padding-left: 8px;
}

.result-tag {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 0.7rem;
  background: rgba(var(--good-rgb), 0.18);
  color: var(--good);
}

.result-tag.ai { background: rgba(var(--warn-rgb), 0.2); color: var(--warn); }

/* "not suggested the previous time" — the whole point of running the same search again */
.result-tag.fresh { background: rgba(var(--accent-rgb), 0.22); color: var(--accent-tint); }

.result-fresh {
  margin: 6px 0 0;
  font-size: 0.82rem;
  color: var(--accent-tint);
}

.confidence {
  margin-top: 8px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.75rem;
  color: var(--muted);
}

.confidence-bar {
  flex: 1;
  height: 4px;
  border-radius: 999px;
  background: var(--border);
  overflow: hidden;
}

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

.banner {
  border-radius: 10px;
  padding: 10px 12px;
  margin-bottom: 14px;
  font-size: 0.88rem;
  border: 1px solid var(--border);
  background: rgba(var(--warn-rgb), 0.12);
  color: var(--tint-warn);
}

.banner.error {
  background: rgba(var(--bad-rgb), 0.12);
  border-color: rgba(var(--bad-rgb), 0.5);
  color: var(--tint-bad);
}

/* --------------------------------------------------------------------- misc */

.how {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: clamp(14px, 2vw, 18px);
  background: var(--bg-panel);
  container-type: inline-size;
  container-name: panel;
}

.how summary {
  cursor: pointer;
  font-weight: 600;
}

.how ol {
  margin: 12px 0 0;
  padding-left: 20px;
  color: var(--muted);
  font-size: 0.88rem;
}

.how li { margin-bottom: 6px; }

.site-footer {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--gutter) 36px;
}

/* ------------------------------------------------------------- profile panel */

.profile-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 14px 0 0;
}

/* the one action that belongs on the far right of the row, away from the list-wide ones */
.profile-tools .push-right { margin-left: auto; }

/* Between a phone and a wide window the list actions used to jump to a second row — usually only
   "+ Add non existing", which then sat alone on the right. Give every button an equal share of the
   row instead and let its own label wrap: the buttons get taller, but they stay on one line. Below
   561px a five-way split leaves too little room per label; above 900px the row already fits. */
@media (min-width: 561px) and (max-width: 900px) {
  .profile-tools.list-actions { flex-wrap: nowrap; align-items: stretch; }
  .profile-tools.list-actions > button {
    flex: 1 1 0;
    min-width: 0;
    min-height: 52px;
    padding: 6px 9px;
    line-height: 1.15;
    white-space: normal;
    text-align: center;
  }
  .profile-tools.list-actions .push-right { margin-left: 0; }
  .profile-tools.list-actions .filter-btn { gap: 6px; }
}

/* On a phone the buttons wrap by themselves; the right-aligned lone "+ Add non existing" then
   reads as a gap, so it simply follows the others in the flow instead. */
@media (max-width: 560px) {
  .profile-tools.list-actions .push-right { margin-left: 0; }
}

.profile-list {
  max-height: 460px;
  overflow-y: auto;
}

button.ghost.danger:hover {
  border-color: var(--bad);
  color: var(--bad);
}

.rating-value.negative { color: var(--bad); }
.rating-value.neutral { color: var(--muted); }
.rating-value.positive { color: var(--good); }

.rated-source {
  color: var(--muted);
  font-size: 0.72rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0 6px;
  margin-left: 6px;
  vertical-align: middle;
}

/* -------------------------------------------------------------------- modals */

.modal {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-panel);
  color: var(--text);
  padding: 0;
  width: min(460px, calc(100vw - 32px));
}

.modal::backdrop { background: rgba(0, 0, 0, 0.62); }

.modal-body {
  /* One number for the dialog's gutter: the bodies use it as padding and the action bar below
     cancels it out again, so both stay in step. */
  --modal-pad: clamp(16px, 4vw, 22px);
  padding: var(--modal-pad) var(--modal-pad) 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* A dialog on a phone with the keyboard open is short: the body scrolls instead of pushing the
     buttons out of reach. dvh follows the height that is really visible. */
  max-height: 88dvh;
  overflow-y: auto;
  container-type: inline-size;
  container-name: panel;
}

.modal-body h3 { margin: 0; font-size: 1.1rem; }

/* The title row of a dialog that carries a close cross in the corner ("Filter the suggestions",
   the confirmation dialog). The cross is a real button, so the keyboard reaches it in the same
   order the eye does — the Escape key does the same thing, but nothing on screen says so. */
.modal-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.modal-head h3 { padding-right: 4px; }

.modal-close {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-input);
  color: var(--muted);
  font: inherit;
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.modal-close:hover { border-color: var(--accent); color: var(--accent); }

.modal-body label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 0.85rem;
  color: var(--muted);
}

.modal-body input,
.modal-body select {
  padding: 9px 11px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text);
  font-size: 0.95rem;
}

.modal-body input[type="range"] { padding: 0; accent-color: var(--accent); }

.modal-body input[type="number"] {
  padding: 9px 11px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text);
  font-size: 0.95rem;
}

/* The buttons stay put while the body scrolls. In a window that is shorter than the dialog — a
   phone in landscape, or a browser window dragged to half the screen — the whole content scrolls,
   and "Done" used to scroll out of sight with it (measured: 275px below the fold at 390x420).
   `position: sticky` keeps the bar on the bottom edge; the two negative margins are what make it
   span the full dialog width, and the same numbers as the body's padding keep the spacing around
   the buttons identical to the dialogs that do fit. */
.modal-actions {
  position: sticky;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 6px calc(-1 * var(--modal-pad)) 0;
  padding: var(--modal-pad) var(--modal-pad) var(--modal-pad);
  background: var(--bg-panel);
}

.manual-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.auth-error,
.auth-info {
  margin: 0;
  font-size: 0.85rem;
  border-radius: 8px;
  padding: 8px 10px;
}

.auth-error { background: rgba(var(--bad-rgb), 0.14); color: var(--tint-bad); }
.auth-info { background: rgba(var(--good-rgb), 0.14); color: var(--tint-good); }

.disclaimer {
  margin: 16px 0 0;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid rgba(var(--warn-rgb), 0.45);
  background: rgba(var(--warn-rgb), 0.1);
  color: var(--tint-warn);
  font-size: 0.85rem;
}

.plain-list {
  margin: 12px 0 0;
  padding-left: 20px;
  color: var(--muted);
  font-size: 0.88rem;
}

.plain-list li { margin-bottom: 6px; }

.plain-list a { color: var(--accent); }

.inline-switch {
  margin-top: 14px;
  display: flex;
  gap: 8px;
  align-items: center;
  font-size: 0.88rem;
  color: var(--text);
}

/* ------------------------------------------------------------- the support page */

/* One way to help. The rows are written out in support.html; static/support.js adds .is-live and
   unhides the button as soon as the matching URL is filled in in static/config.js. A row that has
   no URL yet stays visible in a dashed box reading "coming soon", so nothing ever links nowhere. */
.support-list {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.support-row {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-raised);
}

/* A row that is still a placeholder is marked by an open, dashed box rather than by fading the
   whole row: at 70% opacity the note text drops to about 3.2:1, under AA. */
.support-row:not(.is-live) {
  background: transparent;
  border-style: dashed;
}

.support-row:not(.is-live) .support-name { color: var(--muted); }

.support-main {
  flex: 1 1 280px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.support-name { font-weight: 600; }

.support-note {
  color: var(--muted);
  font-size: 0.86rem;
  max-width: 62ch;
}

.support-soon {
  flex: 0 0 auto;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px dashed var(--border);
  border-radius: var(--radius-pill);
  padding: 4px 11px;
}

/* One column for the field grids as soon as the box they sit in is narrow: a panel in a
   side-by-side layout or a dialog on a phone. */
@container panel (max-width: 460px) {
  .ai-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Three segments side by side ("Both / Animated only / Not animated") are wider than a
   phone-width dialog: the control has `overflow: hidden`, so its third option was clipped and
   could not be picked at all below about 420px of window. Below 380px of *box* they stack, so
   every option stays reachable. A container query, because the box is the dialog, not the page. */
@container panel (max-width: 380px) {
  .segmented { flex-direction: column; width: 100%; }
  .segmented .seg + .seg { border-left: 0; border-top: 1px solid var(--border); }
}

@media (max-width: 640px) {
  .add-bar { flex-direction: column; align-items: stretch; }
  /* `flex: 1 1 320px` on `.grow` is a *row* basis. In the column layout this media query asks for,
     that number becomes the height, and the block reserved 320px of empty space, which made the
     movies page 640px longer than it needs to be at this width (measured 1981px against 1373px at
     641px, and 2271px at 390px). The same rule applied to the old "score the complete catalog"
     switch, until that switch was removed. */
  .add-bar > .grow { flex: 0 0 auto; }
  .segmented { justify-content: stretch; }
  .segmented .seg { flex: 1; }
  .actions { flex-direction: column; align-items: stretch; }
  .actions button { width: 100%; }
  .modal { width: calc(100vw - 24px); }
  .genre-chips { max-height: 180px; }
}

/* ------------------------------------------------------- the YouTube channels */

/* The reference rows and the suggestion cards of youtube.html. These come last in
   the file on purpose: `.channel-card` has to win from `.result-card` above. */

.channel-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--bg-input);
  object-fit: cover;
}

.rated-tags {
  display: block;
  color: var(--muted);
  font-size: 0.78rem;
  margin-top: 2px;
  overflow-wrap: anywhere;
}

.channel-card { grid-template-columns: 78px minmax(0, 1fr); }

.channel-card .result-poster {
  width: 78px;
  height: 78px;
  border-radius: 50%;
}

.channel-visual {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.channel-visual .result-percent {
  width: 100%;
  flex: 1 0 auto;
}

.mini-buttons a {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

.mini-buttons a:hover { border-color: var(--accent); color: var(--accent); }

/* The "open on YouTube" links wear the ghost style of the buttons next to them
   (the ghost rules above only match <button>). */
a.ghost {
  display: inline-flex;
  align-items: center;
  padding: 13px 24px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text);
  font-size: 1rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

a.ghost.small { padding: 9px 14px; font-size: 0.85rem; }

a.ghost:hover { border-color: var(--accent); color: var(--accent); }

/* ------------------------------------------------------------- genre opinions */

.genre-rates {
  margin-top: 16px;
  border-top: 1px solid var(--border);
  padding-top: 12px;
}

.genre-rates summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.9rem;
}

.genre-rates-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 290px), 1fr));
  gap: 8px 18px;
  margin: 12px 0;
}

.genre-rate-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 96px 52px 30px;
  align-items: center;
  gap: 8px;
}

/* A genre with no opinion yet is dimmed, but not so far that its small grey count (11.5px)
   drops under AA: at 0.62 that text measured about 4.3:1, at 0.75 it is 5.9:1 and still reads
   as "not rated". */
.genre-rate-row.unrated { opacity: 0.75; }

.genre-rate-name {
  font-size: 0.86rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.genre-rate-name .count {
  color: var(--muted);
  font-size: 0.72rem;
  margin-left: 5px;
}

.genre-rate-row input[type="range"] { width: 100%; }

/* ----------------------------------------------------------- the quota gauge */

.quota { margin: 12px 0 14px; }

.quota p { margin: 8px 0 0; }

.quota-meter {
  height: 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  overflow: hidden;
}

.quota-meter i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width 0.3s ease-out;
}

.quota-meter i.tight { background: var(--warn); }
.quota-meter i.spent { background: var(--bad); }

.quota-help { margin: 4px 0 14px; }

.quota-help summary {
  cursor: pointer;
  font-size: 0.88rem;
  color: var(--muted);
}

.quota-help summary:hover { color: var(--text); }

.quota-help .plain-list { margin-top: 10px; }

#keyPanel h4 {
  margin: 18px 0 6px;
  font-size: 0.95rem;
}

/* ===========================================================================
   The filter panel of the channels page (a subscriber range + a minimum on the
   number of uploaded videos — two independent blocks in one <details>)
   =========================================================================== */

/* Each filter is a card of its own. A card that really narrows the pool lights up (accent
   border, tinted background, bold label) and the boxes that hold a limit get the accent
   colour too — that is how you see which filter is in use, without a sentence about it. */
.filter-block {
  margin-top: 16px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg-panel);
  transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
}

/* the tint is layered over the (opaque) panel colour, so an active card is lighter than an
   inactive one instead of showing the page background through it */
.filter-block.is-on {
  border-color: var(--accent);
  background: linear-gradient(rgba(var(--accent-rgb), 0.14), rgba(var(--accent-rgb), 0.14)),
    var(--bg-panel);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.1);
}

.filter-block .switch-label { transition: color 0.15s; }
.filter-block.is-on .switch-label { color: var(--text); font-weight: 600; }

/* :placeholder-shown is true while a box is empty, so :not(...) means "it holds a value" */
.filter-block.is-on input[type="number"]:not(:placeholder-shown) {
  border-color: var(--accent);
  background: linear-gradient(rgba(var(--accent-rgb), 0.18), rgba(var(--accent-rgb), 0.18)),
    var(--bg-input);
  color: var(--accent-tint);
  font-weight: 700;
}

.filter-panel .inline-switch { margin-top: 0; }
.filter-panel .ai-grid { margin-top: 12px; margin-bottom: 0; }
.filter-note { margin: 10px 0 0; }
.filter-panel #tubeFilterNote { margin-top: 16px; }

.filter-panel.has-filters > summary { color: var(--accent); font-weight: 600; }

.filter-panel.has-filters #tubeFilterSummary {
  background: rgba(var(--accent-rgb), 0.2);
  color: var(--accent-tint);
  border-radius: 999px;
  padding: 1px 9px;
  font-weight: 600;
}

/* The logo files in static/img are the official YouTube PNGs (transparent background).
   They carry a lot of transparent padding around the artwork:

     icon red.png     1255x1075, the artwork is 827x579  (x 1.85 empty)
     logo white.png   1705x573,  the artwork is 1294x289 (x 1.98 empty)

   So the rules below set the *image* height and get an artwork that is about half as
   tall — that is where the numbers come from (18px of wordmark = a 36px image). The empty
   space is transparent, so where it would reach outside its box the wrapper clips it
   instead of shifting the text around. */

/* The footer attribution next to "Channel data from the YouTube Data API v3" — the logo the
   Branding Guidelines want next to the data. There is deliberately no wordmark in the header
   any more: the channels page wears the site's own header like every other page. */
.yt-attrib {
  display: inline-flex;
  align-items: center;
  margin-right: 10px;
  vertical-align: middle;
}

/* artwork ~18px tall */
.yt-attrib img { display: block; height: 36px; width: auto; }

/* `logo white.png` is white artwork for a dark page, `logo black.png` the same wordmark for a
   light one. Both are in the page and CSS shows the one that belongs to the theme, so the
   footer stays readable in light mode (white on white is not). */
.yt-attrib .logo-on-light { display: none; }

html[data-theme="light"] .logo-on-dark { display: none; }
html[data-theme="light"] .logo-on-light { display: block; }

/* The red play icon in the page title of the channels page. icon red.png is 1255x1075 with the
   artwork (827x579) in the middle, so the image has to be about twice the height of the artwork
   it shows. The wrapper is the artwork's own box and clips the transparent edge, so the space
   around the icon matches the drawn badges. */
.page-badge--red {
  width: 1.3em;   /* 827/579 of the visible height */
  height: 0.91em;
  overflow: hidden;
}
.page-badge--red img { display: block; height: 1.69em; width: auto; }

/* The home card of the channels page: the file is 1255x1075 with the artwork (827x579) centred, so
   the image is about 63/34 times as tall as the part you see. Nothing is clipped — the transparent
   margin is simply allowed to hang outside the chip, which leaves the artwork itself untouched. */
.home-card--yt .home-icon img { display: block; height: 63px; width: auto; }

/* The channels page wears the same theme as every other page: it only adds its own
   component rules (`.channel-card`, `.channel-visual`, the filter panel, the key/quota
   panel), all of which are written against the tokens at the top of this file. There is no
   separate YouTube palette any more — the YouTube logo files in static/img are still used
   for the attribution YouTube's terms require (the footer, and the red play badge in the
   page title), which is branding rather than a theme. */



/* ===========================================================================
   The header's own nav, the home hero and the footer
   =========================================================================== */

/* Three destinations inline on every screen size: films, music and YouTube are the pages a
   visitor moves between, so they are always one click away; the rest (home, support, privacy)
   stays in the hamburger menu. Both live in the markup, so nothing here depends on JavaScript. */
.top-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 4px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

/* The row of links has to stay one row: two lines of nav in a sticky bar cost 35px out of every
   screenful and would make the measured `--header-offset` too small. */
@media (max-width: 899px) {
  .top-nav a { padding: 5px 8px; font-size: 0.82rem; }
}

.top-nav a {
  padding: 6px 11px;
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-size: 0.9rem;
  text-decoration: none;
  white-space: nowrap;
}

.top-nav a:hover { background: var(--bg-raised); color: var(--text); }

.top-nav a.current {
  background: rgba(var(--accent-rgb), 0.14);
  color: var(--accent-tint);
  font-weight: 600;
}

/* The theme switch beside the account pills — the same control as the one inside the menu, which a
   wide window never opens (`static/theme.js` binds both). */
.theme-top { display: none; flex: 0 0 auto; }
.theme-top .icon-moon { display: none; }

html[data-theme="light"] .theme-top .icon-sun { display: none; }
html[data-theme="light"] .theme-top .icon-moon { display: inline; }

@media (min-width: 701px) {
  .theme-top { display: inline-flex; }
}

/* Running text keeps a readable measure. 45-90 characters per line is the typographic norm and the
   panels ran 144-184 at 1440px, where one line crossed the whole window. Controls, grids and the
   result cards are untouched: their own boxes are narrower than this cap. */
main p,
main li,
main dd { max-width: 68ch; }

/* The sentence that says what a step is for, right under its heading. Not fine print, so not
   `.small` (13.6px): it is the first thing read in the panel and it sets the pace. */
.lede { color: var(--muted); font-size: 1rem; }

/* The empty state of a panel: what is missing, and the one click that fills it in. */
.empty-state { margin: 6px 0 0; }
.empty-state > p { margin: 0; }
.empty-state > p + p { margin-top: 10px; }
.empty-hint { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }

/* ---------------------------------------------------------------- the home hero */

/* What the site is for, and one way in, before any explanation. The header keeps the brand only,
   so this is the page's one <h1>. */
.hero { padding: clamp(2px, 1.4vw, 12px) 0 0; }

.hero-kicker {
  margin: 0 0 10px;
  color: var(--accent-tint);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.hero-title {
  margin: 0;
  max-width: 34ch;
  font-size: clamp(1.7rem, 1.2rem + 2.5vw, 2.7rem);
  line-height: 1.1;
  letter-spacing: -0.6px;
  text-wrap: balance;
}

.hero-lede {
  margin: 16px 0 0;
  max-width: 58ch;
  color: var(--muted);
  font-size: clamp(1rem, 0.95rem + 0.35vw, 1.15rem);
}

/* The three ways in on one row — films, music, channels — at every width, each exactly a third of
   the content width. They sit under the hero grid (not inside the copy column) because three
   buttons need the whole width; inside the left half they would each be 150px wide. */
.hero-actions {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin: 26px 0 0;
  /* It is a <p>, and main p is capped at 68ch for reading comfort — a button row is not prose
     and needs the full content width, or three buttons end up 190px wide with clipped labels. */
  max-width: none;
}

/* The way into the app is the one thing on this page that has to look like the way in.
   `white-space: normal` undoes the pill's own nowrap: on a narrow window the label wraps inside
   its third instead of being clipped, and the three keep their one row. */
.hero-actions .account-btn,
.hero-actions .account-btn--primary {
  justify-content: center;
  padding: 13px 14px;
  font-size: 0.95rem;
  line-height: 1.25;
  text-align: center;
  white-space: normal;
}

@media (max-width: 720px) {
  .hero-actions { gap: 8px; }
  .hero-actions .account-btn,
  .hero-actions .account-btn--primary { padding: 10px 7px; font-size: 0.84rem; }
}

/* The second way in is an outline, not a text link: the accent border is there at rest (the old
   one only appeared on hover), and the hover adds the glow plus a touch of accent fill. */
a.account-btn--outline {
  border-color: var(--accent);
  color: var(--accent-tint);
  background: rgba(var(--accent-rgb), 0.06);
}

a.account-btn--outline:hover {
  border-color: var(--accent-strong);
  background: rgba(var(--accent-rgb), 0.16);
  color: var(--text);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.16);
  transform: translateY(-1px);
}

/* A two-column hero from 800px: the promise on the left, a real suggestion card on the right.
   Below that the preview simply follows the copy, so a phone still gets the product shot. */
.hero-grid {
  display: grid;
  /* One explicit constraining column before the 800px breakpoint: an implicit `auto` column takes
     its width from the widest child's max-content, and the two-button grid below then pushed the
     whole page 193px sideways on a 320px phone. */
  grid-template-columns: minmax(0, 1fr);
  gap: 26px;
}

@media (min-width: 800px) {
  .hero-grid { grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); align-items: center; gap: 40px; }
}

.hero-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 14px 30px;
  margin: 26px 0 0;
  padding: 16px 0 0;
  border-top: 1px solid var(--border);
  list-style: none;
}

.hero-stats li { display: flex; flex-direction: column; gap: 1px; }
.hero-stats strong { font-size: 1.3rem; line-height: 1.15; }
.hero-stats span { color: var(--muted); font-size: 0.8rem; }

/* The product itself, before the explanation: this is the layout the results panel uses, so the
   preview cannot drift from what the app really shows. Decorative only (aria-hidden). */
.hero-preview {
  display: grid;
  gap: 12px;
  align-content: center;
}

.hero-preview .result-card {
  padding: 12px;
  grid-template-columns: 62px minmax(0, 1fr);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
}

.hero-preview .result-percent { min-height: 92px; }
.hero-preview .result-percent strong { font-size: 1.15rem; }
.hero-preview .result-actions, .hero-preview .confidence { display: none; }
.hero-preview .result-reason { margin-top: 6px; }

/* Three short steps instead of four paragraphs: each number is the same accent dot the app's own
   panels use. */
.steps {
  display: grid;
  gap: 18px;
  margin: 4px 0 0;
  padding: 0;
  list-style: none;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
}

.steps li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 3px 12px;
}

.step-dot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: rgba(var(--accent-rgb), 0.14);
  color: var(--accent);
  font-size: 0.85rem;
  font-weight: 700;
  grid-row: span 2;
}

.steps h3 { margin: 0; font-size: 1rem; }
.steps p { margin: 0; color: var(--muted); font-size: 0.9rem; }

/* ---------------------------------------------------------------- the footer */

/* Three columns: where to go, what this is, and what happens to your data. It used to be one
   attribution line with a support link on every page. */
.site-footer {
  margin-top: clamp(26px, 4vw, 48px);
  padding-top: 22px;
  border-top: 1px solid var(--border);
}

.footer-cols {
  display: grid;
  gap: 22px 28px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}

.footer-title {
  margin: 0 0 8px;
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.footer-links {
  display: grid;
  gap: 7px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.88rem;
}

/* The footer used to show the browser-default blue underline on every destination, which made the
   last screen look unstyled next to the cards above it. Muted until hovered, like the header nav. */
.footer-links a {
  color: var(--muted);
  text-decoration: none;
}

.footer-links a:hover {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.footer-note {
  margin: 10px 0 0;
  color: var(--muted);
  font-size: 0.78rem;
  line-height: 1.55;
}

/* The one generated line above the columns — the catalog date on the movies page, the quota note on
   the channels page — needs its own room instead of sitting against the first column title. */
.site-footer > .muted.small { display: block; margin-bottom: 14px; }

/* ===========================================================================
   Motion and the very small screens
   =========================================================================== */

/* Everything above animates with a transition (the folding header, the hovering cards, the quota
   bar, the AI panel). Whoever asked their system for less motion gets the end state without the
   movement — this is a static file, so one rule at the end covers all of them. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* A row that mixes a text column with fixed-width controls (a genre opinion) survives a 320px
   window by giving the controls less room instead of squeezing the text away. */
@media (max-width: 520px) {
  .genre-rate-row { grid-template-columns: minmax(0, 1fr) 88px 44px 28px; gap: 6px; }
  .genre-rate-name { font-size: 0.82rem; }
}

/* Touch targets on a phone: the per-row remove cross was 30x30, the two ranking arrows 26x26 and
   the dialog cross 32x32. Kept at the end of the file so these win from the component rules above
   (a media query does not add specificity). */
@media (max-width: 700px) {
  .icon-btn { width: 44px; height: 44px; flex: 0 0 auto; }
  .rank-buttons .icon-btn { width: 38px; height: 38px; }
  .modal-close { width: 40px; height: 40px; }
  .ghost.small, a.ghost.small { min-height: 44px; }
}
/* ------------------------------------------- pages that are not open yet (static/coming-soon.js) */

/* A link to a page that is not open yet: readable, not clickable, with a small "Soon" badge. It is
   the same row in the hamburger menu and in the top bar, so both are covered here. */
.menu-item.is-coming-soon,
.top-nav a.is-coming-soon {
  color: var(--muted);
  cursor: default;
  opacity: 0.78;
}

.menu-item.is-coming-soon:hover,
.top-nav a.is-coming-soon:hover { background: transparent; }

.soon-badge {
  margin-left: 8px;
  padding: 1px 7px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: rgba(var(--accent-rgb), 0.12);
  color: var(--accent-tint);
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  line-height: 1.6;
}

/* The panel music.html shows while musicComingSoon is true in static/config.js. */
.coming-soon { max-width: 780px; }
.coming-soon h2 { margin: 4px 0 12px; font-size: 1.6rem; }
.coming-soon .hero-kicker { margin-bottom: 0; }
.coming-soon-list { margin: 18px 0; padding-left: 20px; line-height: 1.75; }
.coming-soon-list li { margin-bottom: 4px; }
/* Two buttons, not three: the coming-soon panel has its own pair and a three-column grid would
   leave an empty third. */
.coming-soon .hero-actions {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: 22px;
}

@media (max-width: 700px) {
  .coming-soon .hero-actions { grid-template-columns: minmax(0, 1fr); }
}
.home-card.is-coming-soon { opacity: 0.8; }
.home-card.is-coming-soon:hover {
  border-color: var(--border);
  transform: none;
  box-shadow: none;
  cursor: default;
}
.home-card.is-coming-soon:hover .home-go { text-decoration: none; }

/* music.html wraps its panels in one element so the coming-soon switch can hide the whole app.
   Without this rule that wrapper would also swallow `main`'s spacing, and every panel would sit
   flush against the next one. `:not([hidden])` is required: a bare `display: flex` on the id would
   beat the browser's own [hidden] rule and the app would stay visible while it should be hidden. */
#musicApp:not([hidden]) {
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 2vw, 22px);
}
