    /* Self-hosted Google Fonts (latin, latin-ext, vietnamese; cyrillic
       dropped — this site is Vietnamese). font-display: swap keeps text
       painting in the fallback face while a woff2 is still in flight. */
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-400-vietnamese.woff2) format('woff2');
      unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-400-latin-ext.woff2) format('woff2');
      unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-400-latin.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 500;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-500-vietnamese.woff2) format('woff2');
      unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 500;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-500-latin-ext.woff2) format('woff2');
      unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 500;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-500-latin.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 600;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-600-vietnamese.woff2) format('woff2');
      unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 600;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-600-latin-ext.woff2) format('woff2');
      unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 600;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-600-latin.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 700;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-700-vietnamese.woff2) format('woff2');
      unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 700;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-700-latin-ext.woff2) format('woff2');
      unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
    }
    @font-face {
      font-family: 'Be Vietnam Pro';
      font-style: normal;
      font-weight: 700;
      font-display: swap;
      src: url(/static/fonts/BeVietnamPro-700-latin.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }
    @font-face {
      font-family: 'IBM Plex Mono';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(/static/fonts/IBMPlexMono-400-vietnamese.woff2) format('woff2');
      unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
    }
    @font-face {
      font-family: 'IBM Plex Mono';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(/static/fonts/IBMPlexMono-400-latin-ext.woff2) format('woff2');
      unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
    }
    @font-face {
      font-family: 'IBM Plex Mono';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(/static/fonts/IBMPlexMono-400-latin.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }
    @font-face {
      font-family: 'IBM Plex Mono';
      font-style: normal;
      font-weight: 500;
      font-display: swap;
      src: url(/static/fonts/IBMPlexMono-500-vietnamese.woff2) format('woff2');
      unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
    }
    @font-face {
      font-family: 'IBM Plex Mono';
      font-style: normal;
      font-weight: 500;
      font-display: swap;
      src: url(/static/fonts/IBMPlexMono-500-latin-ext.woff2) format('woff2');
      unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
    }
    @font-face {
      font-family: 'IBM Plex Mono';
      font-style: normal;
      font-weight: 500;
      font-display: swap;
      src: url(/static/fonts/IBMPlexMono-500-latin.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }
    @font-face {
      font-family: 'Playfair Display';
      font-style: italic;
      font-weight: 600;
      font-display: swap;
      src: url(/static/fonts/PlayfairDisplay-600i-vietnamese.woff2) format('woff2');
      unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
    }
    @font-face {
      font-family: 'Playfair Display';
      font-style: italic;
      font-weight: 600;
      font-display: swap;
      src: url(/static/fonts/PlayfairDisplay-600i-latin-ext.woff2) format('woff2');
      unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
    }
    @font-face {
      font-family: 'Playfair Display';
      font-style: italic;
      font-weight: 600;
      font-display: swap;
      src: url(/static/fonts/PlayfairDisplay-600i-latin.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }
    @font-face {
      font-family: 'Playfair Display';
      font-style: normal;
      font-weight: 600;
      font-display: swap;
      src: url(/static/fonts/PlayfairDisplay-600-vietnamese.woff2) format('woff2');
      unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
    }
    @font-face {
      font-family: 'Playfair Display';
      font-style: normal;
      font-weight: 600;
      font-display: swap;
      src: url(/static/fonts/PlayfairDisplay-600-latin-ext.woff2) format('woff2');
      unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
    }
    @font-face {
      font-family: 'Playfair Display';
      font-style: normal;
      font-weight: 600;
      font-display: swap;
      src: url(/static/fonts/PlayfairDisplay-600-latin.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }
    @font-face {
      font-family: 'Playfair Display';
      font-style: normal;
      font-weight: 700;
      font-display: swap;
      src: url(/static/fonts/PlayfairDisplay-700-vietnamese.woff2) format('woff2');
      unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
    }
    @font-face {
      font-family: 'Playfair Display';
      font-style: normal;
      font-weight: 700;
      font-display: swap;
      src: url(/static/fonts/PlayfairDisplay-700-latin-ext.woff2) format('woff2');
      unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
    }
    @font-face {
      font-family: 'Playfair Display';
      font-style: normal;
      font-weight: 700;
      font-display: swap;
      src: url(/static/fonts/PlayfairDisplay-700-latin.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }

    /* The public site is always black lacquer; the admin console (body.admin)
       switches to porcelain when the OS asks for light. */
    :root {
      color-scheme: dark;
      --head-bg: rgba(22, 17, 19, 0.88);
      --bg: #161113;
      --sheen: rgba(224, 85, 74, 0.05);
      --panel: #1e171a;
      --panel-raised: #261d20;
      --edge: rgba(240, 226, 214, 0.16);
      --edge-soft: rgba(240, 226, 214, 0.08);
      --ink: #f0e8df;
      --ink-muted: rgba(240, 232, 223, 0.55);
      --cinnabar: #e0554a;
      --cinnabar-deep: #b8382e;
      --cinnabar-ink: #fff6f0;
      --gold: #cfa75a;
      --jade: #7cc5a1;
      --amber: #dcb15f;
      --tint-ok: rgba(124, 197, 161, 0.12);
      --tint-warn: rgba(220, 177, 95, 0.12);
      --tint-err: rgba(224, 85, 74, 0.13);
      --shadow: 0 14px 40px rgba(8, 4, 5, 0.45);
      /* Liquid-glass material (public site only): lacquer-tinted panes
         that blur the artwork passing beneath them. */
      --glass: rgba(32, 23, 27, 0.5);
      --glass-heavy: rgba(28, 20, 24, 0.62);
      --glass-edge: rgba(240, 226, 214, 0.2);
      --glass-blur: blur(22px) saturate(1.7);
      --glass-shine: inset 0 1px 0 rgba(255, 245, 235, 0.13);
    }
    @media (prefers-color-scheme: light) { body.admin {
      color-scheme: light;
      --bg: #eceee7;
      --sheen: rgba(255, 255, 255, 0.55);
      --panel: #f9f9f4;
      --panel-raised: #ffffff;
      --edge: rgba(29, 33, 38, 0.18);
      --edge-soft: rgba(29, 33, 38, 0.09);
      --ink: #22262b;
      --ink-muted: rgba(34, 38, 43, 0.56);
      --cinnabar: #ab372c;
      --cinnabar-deep: #962c24;
      --cinnabar-ink: #fdf6ef;
      --gold: #85661f;
      --jade: #1e6f4c;
      --amber: #8a6000;
      --tint-ok: rgba(30, 111, 76, 0.10);
      --tint-warn: rgba(138, 96, 0, 0.10);
      --tint-err: rgba(171, 55, 44, 0.10);
      --shadow: 0 10px 30px rgba(34, 38, 43, 0.10);
      --head-bg: rgba(236, 238, 231, 0.88);
    } }
    * { box-sizing: border-box; }
    html { height: 100%; }
    body {
      font-family: "Be Vietnam Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      margin: 0; min-height: 100%; overflow-x: hidden;
      /* Column flex so the footer's auto top margin pushes it to the bottom
         of the window on short pages, while staying in normal flow (it
         scrolls; never fixed over mobile Safari's bottom bar). */
      display: flex; flex-direction: column;
      background: radial-gradient(70rem 42rem at 50% -12%, var(--sheen), transparent 70%) no-repeat var(--bg);
      color: var(--ink);
      -webkit-font-smoothing: antialiased;
      line-height: 1.55;
    }
    main { width: 100%; max-width: 960px; margin-inline: auto; padding: 1.5rem 1.25rem 4.5rem; }

    /* ---- Header ---- */
    /* Full window width on every page, and stays put while scrolling. */
    .site-head {
      position: sticky; top: 0; z-index: 40;
      display: flex; align-items: center; gap: 1.1rem;
      padding: 1.1rem 1.25rem .9rem;
      background: var(--head-bg);
      -webkit-backdrop-filter: blur(12px);
      backdrop-filter: blur(12px);
      border-bottom: 1px solid var(--edge-soft);
    }
    .head-right { display: flex; align-items: center; gap: .8rem; margin-left: auto; }
    /* The wordmark cedes its space to the menu on smaller windows; the boat
       mark alone still brands the corner. Pages with no menu (sign-in,
       account, admin — the nav renders only when current_user is defined)
       have the room, so they keep the wordmark all the way down. */
    @media (max-width: 1000px) {
      .site-head .brand-name { display: none; }
      body.no-menu .site-head .brand-name { display: inline; }
    }
    .brand {
      font-family: "Playfair Display", Georgia, serif;
      font-weight: 700; font-size: 1.15rem; letter-spacing: .005em;
      text-decoration: none; color: var(--ink); white-space: nowrap;
      display: inline-flex; align-items: baseline; gap: .55rem;
    }
    /* The brand mark: a Vietnamese fishing boat riding a wavy film strip —
       "phim" (film) meets "tàu" (boat). Two-tone via the palette vars so it
       follows the admin console's light mode automatically. */
    .brand-mark { width: 2.05rem; height: 2.05rem; flex-shrink: 0; align-self: center; }
    .brand-mark .mark-strip { fill: var(--cinnabar); }
    .brand-mark .mark-boat { fill: var(--gold); }
    .brand:hover { text-decoration: none; }
    .brand small {
      font-family: "Be Vietnam Pro", sans-serif; font-weight: 600; font-size: .62rem;
      color: var(--gold); letter-spacing: .22em; text-transform: uppercase;
    }
    .head-actions { display: flex; align-items: center; gap: .6rem; }
    .account-link {
      font-size: .82rem; font-weight: 600; color: var(--ink); white-space: nowrap;
      max-width: 12rem; overflow: hidden; text-overflow: ellipsis;
    }
    .account-link:hover { color: var(--cinnabar); text-decoration: none; }
    /* Account dropdown: the greeting is a toggle, reusing the .more-menu
       panel but right-anchored so it opens under the top-right corner. */
    .account-menu { position: relative; }
    .account-toggle {
      display: inline-flex; align-items: center; gap: .3rem;
      background: none; border: 0; cursor: pointer; font: inherit;
    }
    /* The person icon stands in for the greeting on touch only. */
    .account-toggle .account-icon { display: none; }
    .account-caret { transition: transform .2s ease; }
    .account-menu.open .account-caret { transform: rotate(180deg); }
    /* Inset from the toggle's right edge so the panel keeps a comfortable
       margin from the window edge (header padding alone reads too tight). */
    /* Right-anchored, inset a touch so the panel keeps a comfortable margin
       from the window edge. Needs the extra class for specificity: the shared
       `.more-menu { left: 0 }` is defined later in this stylesheet and would
       otherwise win, stretching the panel out past the toggle. */
    .account-menu .account-more { left: auto; right: .5rem; }
    .account-menu.open .account-more { display: flex; }
    .account-more form { margin: 0; }
    /* The logout row is a form <button>, so strip button chrome to match the
       <a> pills beside it, and let it fill the panel width. Each row is an
       icon + label. */
    .account-more .menu-pill {
      border-radius: 9px; padding: .55rem .8rem;
      display: flex; align-items: center; gap: .55rem; width: 100%; text-align: left;
      background: none; border: 0; cursor: pointer; font: inherit;
    }
    .account-more .menu-pill svg { flex-shrink: 0; color: var(--ink-muted); }
    .account-more .menu-pill:hover { background: rgba(255, 255, 255, .07); }
    .account-more .menu-pill:hover svg { color: var(--ink); }
    /* Hairline above logout, fading out at both ends. */
    .account-divider {
      height: 1px; margin: .3rem .3rem;
      background: linear-gradient(to right,
        transparent, rgba(255, 255, 255, .18) 20%, rgba(255, 255, 255, .18) 80%, transparent);
    }
    /* Name fields on the account page: quiet label over value, side by side. */
    .account-fields { margin: .9rem 0 0; display: flex; flex-wrap: wrap; gap: 1.4rem; }
    .account-fields div { display: flex; flex-direction: column; gap: .15rem; }
    .account-fields dt {
      font-size: .68rem; letter-spacing: .05em; text-transform: uppercase;
      color: var(--ink-muted);
    }
    .account-fields dd { margin: 0; font-size: .92rem; color: var(--ink); }
    .nav-link {
      font-size: .82rem; font-weight: 600; color: var(--ink); white-space: nowrap;
      margin-right: .35rem;
    }
    .nav-link:hover { color: var(--cinnabar); text-decoration: none; }

    /* ---- Menu pills (desktop): free-standing, no wrapping capsule; the
       current page's pill gets the liquid-glass fill. ---- */
    .head-menu { display: inline-flex; align-items: center; gap: .3rem; }
    .menu-pill {
      padding: .42rem .95rem; border-radius: 999px;
      font-size: .82rem; font-weight: 600; color: var(--ink); white-space: nowrap;
      border: 1px solid transparent;
      transition: background .2s ease;
    }
    .menu-pill:hover { background: rgba(255, 255, 255, .07); color: var(--ink); text-decoration: none; }
    .menu-pill[aria-current="page"] {
      background:
        linear-gradient(120deg, rgba(255, 255, 255, .09), rgba(255, 255, 255, .02) 55%,
          rgba(255, 255, 255, .06)) rgba(30, 24, 21, .4);
      -webkit-backdrop-filter: blur(14px) saturate(1.5);
      backdrop-filter: blur(14px) saturate(1.5);
      border-color: rgba(255, 255, 255, .12);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10), 0 4px 12px rgba(0, 0, 0, .18);
    }

    /* ---- Expanding search ---- */
    .site-search { position: relative; display: flex; align-items: center; }
    .search-toggle {
      display: inline-flex; background: none; border: 0; cursor: pointer;
      color: var(--ink); padding: .35rem; margin: -.35rem;
    }
    .search-toggle:hover { color: var(--cinnabar); }
    .search-input {
      width: 0; opacity: 0; padding: .45rem 0; margin-left: .45rem;
      border: 1px solid transparent; border-radius: 999px;
      background: rgba(255, 255, 255, .07); color: var(--ink);
      font: inherit; font-size: .85rem;
      transition: width .28s ease, opacity .2s ease, padding .28s ease;
      pointer-events: none;
    }
    .search-input::placeholder { color: var(--ink-muted); }
    .search-input:focus { outline: none; border-color: rgba(255, 255, 255, .22); }
    .site-search.open .search-input {
      opacity: 1; border-color: rgba(255, 255, 255, .13); pointer-events: auto;
    }
    /* Once open, the magnifier tucks inside the box (decorative — clearing and
       closing are the native ✕, Escape, and the touch ✕ button). */
    .site-search.open .search-toggle {
      position: absolute; left: .7rem; top: 50%; transform: translateY(-50%);
      margin: 0; padding: 0; z-index: 2; pointer-events: none; color: var(--ink-muted);
    }
    /* Desktop: the open field is lifted out of the flow and grows leftwards
       over the nav pills. Expanding it in-flow instead would widen the header
       past the window and push the account menu off the right edge. */
    @media (hover: hover) and (pointer: fine) {
      .site-search { --search-w: min(16rem, 40vw); }
      .site-search.open .search-input {
        position: absolute; right: 0; top: 50%; transform: translateY(-50%); z-index: 1;
        width: var(--search-w); margin: 0; padding: .45rem .95rem .45rem 2.3rem;
        /* Opaque — even a few percent of translucency lets the pills
           underneath ghost through the field. */
        background-color: var(--panel-raised);
      }
      /* Follows the field's left edge, which now floats free of the button. */
      .site-search.open .search-toggle { left: auto; right: calc(var(--search-w) - 2.05rem); }
    }
    /* Touch-only ✕ beside the field: the bottom tab bar opens search, this
       dismisses it and puts the page back. (Desktop closes by clearing the
       field, pressing Escape, or clicking away.) */
    .search-cancel {
      display: none; align-items: center; flex-shrink: 0;
      background: none; border: 0; padding: .35rem; margin: 0; cursor: pointer;
      color: var(--ink-muted);
    }
    .search-cancel svg { display: block; }
    .search-cancel:hover { color: var(--cinnabar); }
    /* ---- Search takeover: a live results grid replaces the main column while
       typing; the header script toggles body.search-grid. ---- */
    .search-takeover { margin-bottom: 2rem; }
    body.search-grid main > :not(.search-takeover) { display: none; }
    /* While a query is in flight, the input sweeps with the same shimmer as the
       thumbnail skeletons, so "searching" reads instantly. */
    .search-input.searching {
      background-image: linear-gradient(
        105deg, transparent 35%, rgba(255, 255, 255, 0.12) 50%, transparent 65%);
      background-size: 220% 100%; background-repeat: no-repeat;
      animation: thumb-shimmer 1s linear infinite;
    }
    @media (prefers-reduced-motion: reduce) {
      .search-input.searching { animation: none; }
    }
    /* Touch-only "Danh sách" shortcut, top right — shown as an icon on mobile
       to save the top bar's width. */
    .head-watchlist {
      display: none; align-items: center; color: var(--ink);
      padding: .2rem; margin: -.2rem;
    }
    .head-watchlist svg { width: 22px; height: 22px; }
    .head-watchlist:hover { color: var(--cinnabar); text-decoration: none; }

    /* Navigation has three layouts, chosen by device rather than width alone:
       - wide desktop: every pill sits inline (the default above);
       - narrow desktop (mouse): the later pills collapse into a "Thêm ▾"
         dropdown, Netflix-style — no hamburger, sign-in stays on the right;
       - touch phones/tablets: a fixed bottom tab bar (header menu hidden).
       The pointer/hover media features distinguish a real touch device from a
       merely narrowed desktop window. */
    .menu-more { position: relative; display: none; }
    .more-toggle {
      display: inline-flex; align-items: center; gap: .35rem;
      background: none; cursor: pointer; font: inherit;
    }
    .more-toggle svg { transition: transform .2s ease; }
    .menu-more.open .more-toggle svg { transform: rotate(180deg); }
    .more-menu {
      display: none; position: absolute; top: calc(100% + .55rem); left: 0; z-index: 60;
      flex-direction: column; gap: .1rem; min-width: 10.5rem; padding: .4rem;
      /* These open over poster artwork, so the fill is a raised surface rather
         than glass — but a light one: near-black read as a hole punched in the
         page, and the original .86 let faces through the labels. */
      background:
        linear-gradient(160deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, 0) 40%)
        rgba(43, 34, 32, .96);
      -webkit-backdrop-filter: blur(28px) saturate(1.25);
      backdrop-filter: blur(28px) saturate(1.25);
      border: 1px solid rgba(255, 255, 255, .10); border-radius: 14px;
      box-shadow: 0 18px 44px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 255, 255, .06);
    }
    .menu-more.open .more-menu { display: flex; }
    .more-menu .menu-pill { border-radius: 9px; padding: .55rem .8rem; }
    .more-menu .collapsible-sm, .more-menu .collapsible-xs { display: none; }
    .bottom-nav { display: none; }

    @media (hover: hover) and (pointer: fine) and (max-width: 700px) {
      .menu-pill.collapsible { display: none; }
      .menu-more { display: block; }
      /* Tighten the header's own spacing before giving up any more pills —
         the density is worth roughly one extra pill on the way down. */
      .site-head { gap: .65rem; }
      .head-right { gap: .55rem; }
      .menu-pill { padding: .42rem .7rem; }
      /* Long display names cede width here, but not so tight that an ordinary
         "Chào, <first name>" gets an ellipsis. */
      .account-link { max-width: 10rem; }
    }
    /* Narrower still, the remaining pills drop into "Thêm" one at a time,
       right to left. Collapsing them all at once would leave a dead gap
       mid-header; keeping them would push the account menu off the right edge
       (the header can't scroll — it would take the page with it). */
    @media (hover: hover) and (pointer: fine) and (max-width: 600px) {
      .head-menu > .collapsible-sm { display: none; }
      .more-menu .collapsible-sm { display: block; }
    }
    @media (hover: hover) and (pointer: fine) and (max-width: 440px) {
      .head-menu > .collapsible-xs { display: none; }
      .more-menu .collapsible-xs { display: block; }
    }

    @media (hover: none) and (pointer: coarse) {
      /* Header menu collapses to the bottom tab bar, which is pure navigation.
         Identity lives top-right in both states — "Đăng nhập" for a guest, the
         account dropdown (as a person icon) once signed in — so it doesn't
         jump between the top bar and the bottom bar on sign-in. */
      .head-menu { display: none; }
      .account-toggle .account-name, .account-toggle .account-caret { display: none; }
      .account-toggle .account-icon { display: block; width: 23px; height: 23px; }
      .account-toggle { padding: .2rem; margin: -.2rem; }
      /* The menu pills are gone here, so the wordmark has room again even
         though the narrow-desktop rule above hides it. */
      .site-head .brand-name { display: inline; }
      /* Danh sách rides the top bar; search moves to the bottom tab bar. */
      .head-watchlist { display: inline-flex; }
      /* Sign-in is the guest's primary action, so give it a legible outlined
         pill rather than a bare icon; the bookmark beside it stays icon-only. */
      .login-link {
        padding: .3rem .8rem; border-radius: 999px;
        border: 1px solid rgba(255, 255, 255, .22);
        font-size: .8rem; line-height: 1.35;
        /* .account-link clips overflow (for the signed-in name's ellipsis);
           the pill must not, or Vietnamese descenders (g, p) get cut off. */
        overflow: visible;
      }
      .login-link:hover { border-color: var(--cinnabar); }
      /* Search opens from the bottom tab bar and takes over the header row —
         the brand and the top-right actions step aside for a full-width field,
         with a ✕ to back out. Results land in the main column's grid, the same
         as desktop, so there is no scrim or layer over the page. */
      .site-search { display: none; }
      body.search-open .site-head .brand,
      body.search-open .head-watchlist,
      body.search-open .head-actions { display: none; }
      body.search-open .head-right { flex: 1 1 auto; margin-left: 0; gap: .55rem; }
      body.search-open .site-search { display: flex; flex: 1 1 auto; gap: .6rem; }
      /* Beats the desktop `.site-search.open .search-input` rule (three
         classes) — it fixes a narrow width the full-bleed bar must override. */
      body.search-open .site-search .search-input {
        flex: 1 1 auto; width: auto; opacity: 1; margin-left: 0;
        /* Here the field stays in flow — it is the header row, not an overlay
           floated across it as on desktop. */
        position: static; transform: none;
        padding: .55rem 1rem .55rem 2.45rem; pointer-events: auto;
        border-color: rgba(255, 255, 255, .18);
        /* iOS auto-zooms (and stays zoomed) when a focused input's font is
           under 16px — keep it at 16px so the layout never shifts. */
        font-size: 16px;
      }
      body.search-open .site-search .search-toggle { left: .75rem; right: auto; }
      body.search-open .search-cancel { display: inline-flex; }
      .bottom-nav {
        display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
        justify-content: space-around; align-items: stretch;
        background: var(--head-bg);
        -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
        border-top: 1px solid var(--edge-soft);
        padding: .35rem .2rem calc(.35rem + env(safe-area-inset-bottom));
      }
      .bottom-nav a, .bottom-nav .bottom-search {
        flex: 1 1 0; display: flex; flex-direction: column; align-items: center; gap: .16rem;
        padding: .3rem .1rem; color: var(--ink-muted);
        font-size: .62rem; font-weight: 600; letter-spacing: .01em; text-align: center;
        background: none; border: 0; font-family: inherit; cursor: pointer;
      }
      .bottom-nav a:hover { text-decoration: none; }
      .bottom-nav a svg, .bottom-nav .bottom-search svg { width: 23px; height: 23px; }
      .bottom-nav a[aria-current="page"] { color: var(--cinnabar); }
      /* Keep page content clear of the fixed bar. */
      body { padding-bottom: calc(3.4rem + env(safe-area-inset-bottom)); }
    }

    /* ---- Panels ---- */
    .card {
      background: var(--panel);
      border: 1px solid var(--edge-soft);
      border-radius: 6px;
      padding: 1.35rem 1.5rem;
      margin-bottom: 1.25rem;
      box-shadow: var(--shadow);
    }
    .hero { border-top: 1px solid var(--gold); }
    /* Legal pages (privacy, terms): a single readable column. */
    .legal { max-width: 46rem; margin-inline: auto; margin-top: 1.5rem; line-height: 1.7; }
    .legal h2 {
      font-family: "Playfair Display", Georgia, serif;
      font-size: 1.15rem; font-weight: 600; margin: 1.6rem 0 .4rem;
    }
    .legal ul { margin: .4rem 0 .4rem 1.2rem; }
    .legal li { margin-bottom: .3rem; }
    .site-foot {
      width: 100%; max-width: 960px; margin: auto auto 0; padding: 1.5rem 1.25rem 2rem;
      text-align: center;
      /* Hairline rule that tapers off at both ends instead of hitting the
         edges as a hard stop. */
      background: linear-gradient(
          to right, transparent, var(--edge-soft) 22%, var(--edge-soft) 78%, transparent
        ) top / 100% 1px no-repeat;
    }
    .foot-links { display: flex; gap: 1.4rem; justify-content: center; flex-wrap: wrap; }
    .foot-links a { font-size: .82rem; font-weight: 600; color: var(--ink-muted); }
    .foot-links a:hover { color: var(--cinnabar); text-decoration: none; }

    h1 {
      font-family: "Playfair Display", Georgia, serif;
      font-size: 1.7rem; font-weight: 600; letter-spacing: .002em;
      margin: 0 0 .3rem; line-height: 1.2;
    }
    h2 { font-size: 1rem; font-weight: 650; margin: 1.3rem 0 .35rem; }
    .eyebrow {
      display: flex; align-items: center; gap: .6rem;
      font-size: .66rem; font-weight: 700; letter-spacing: .22em;
      text-transform: uppercase; color: var(--gold); margin: 0 0 1rem;
    }
    /* A shelf heading that links to its full catalog (e.g. "Phim lẻ ›"). */
    .shelf-more {
      display: inline-flex; align-items: center; gap: .3rem;
      color: inherit; letter-spacing: inherit;
    }
    .shelf-more:hover { color: var(--cinnabar); text-decoration: none; }
    .shelf-more svg { margin-top: -1px; }
    .muted { color: var(--ink-muted); font-size: .85rem; }
    a { color: var(--cinnabar); text-decoration: none; }
    a:hover { text-decoration: underline; text-underline-offset: 3px; }
    code, time, .mono {
      font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
      font-size: .92em;
    }
    time { white-space: nowrap; }

    /* ---- Tables ---- */
    .table-wrap { overflow-x: auto; }
    table { border-collapse: collapse; width: 100%; font-size: .88rem; }
    th {
      text-align: left; font-size: .64rem; font-weight: 700; letter-spacing: .14em;
      text-transform: uppercase; color: var(--ink-muted);
      padding: .35rem .6rem .45rem; border-bottom: 1px solid var(--edge);
    }
    td { text-align: left; padding: .55rem .6rem; border-bottom: 1px solid var(--edge-soft); }
    tr:last-child td { border-bottom: 0; }
    /* Generic admin data browser: arbitrary columns can hold long text
       (titles, URLs) that would otherwise make every row absurdly tall. */
    .data-table td {
      max-width: 22rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }

    /* ---- Controls ---- */
    input[type=text], input[type=email], input[type=password], input[type=number], textarea {
      width: 100%; padding: .55rem .75rem; font: inherit; color: var(--ink);
      background: var(--panel-raised); border: 1px solid var(--edge); border-radius: 4px;
    }
    input::placeholder, textarea::placeholder { color: var(--ink-muted); }
    input:focus-visible, textarea:focus-visible, button:focus-visible, a:focus-visible {
      outline: 2px solid var(--cinnabar); outline-offset: 2px;
    }
    textarea { min-height: 5.5rem; resize: vertical; line-height: 1.6; }

    button, .btn {
      display: inline-block; padding: .5rem 1.1rem; font: inherit; font-weight: 600;
      font-size: .86rem; border-radius: 4px; cursor: pointer; text-decoration: none;
      color: var(--ink); background: transparent; border: 1px solid var(--edge);
      transition: border-color .15s ease, background-color .15s ease, color .15s ease;
    }
    button:hover, .btn:hover { border-color: var(--ink-muted); text-decoration: none; }
    button.primary {
      background: var(--cinnabar-deep); border-color: var(--cinnabar-deep);
      color: var(--cinnabar-ink);
    }
    button.primary:hover { background: var(--cinnabar); border-color: var(--cinnabar); }
    button.danger { color: var(--cinnabar); border-color: var(--tint-err); }
    button.danger:hover { border-color: var(--cinnabar); }
    button.small, .btn.small { padding: .24rem .7rem; font-size: .76rem; }
    /* Liquid-glass button, for header/page actions outside the auth card
       (e.g. the "Tài khoản" back-link) — same fill as the active nav pill. */
    .btn.glass {
      border-radius: 999px; color: var(--ink);
      background:
        linear-gradient(120deg, rgba(255, 255, 255, .09), rgba(255, 255, 255, .02) 55%,
          rgba(255, 255, 255, .06)) rgba(30, 24, 21, .4);
      -webkit-backdrop-filter: blur(14px) saturate(1.5);
      backdrop-filter: blur(14px) saturate(1.5);
      border-color: rgba(255, 255, 255, .12);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10), 0 4px 12px rgba(0, 0, 0, .18);
    }
    .btn.glass:hover { background-color: rgba(30, 24, 21, .6); border-color: rgba(255, 255, 255, .2); }
    /* ---- Sign-in card (public): liquid glass, matching the header. ---- */
    .auth-card {
      max-width: 420px; margin: 10vh auto 0;
      background:
        linear-gradient(160deg, rgba(255, 255, 255, .05), rgba(255, 255, 255, 0) 40%)
        rgba(26, 20, 18, .6);
      -webkit-backdrop-filter: blur(20px) saturate(1.4);
      backdrop-filter: blur(20px) saturate(1.4);
      border: 1px solid rgba(255, 255, 255, .10); border-radius: 16px;
      box-shadow: 0 18px 44px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .06);
    }
    .auth-card .auth-divider { text-align: center; margin: 1rem 0; }
    .auth-card input[type=email], .auth-card input[type=password] {
      border-radius: 999px; padding: .62rem 1.1rem;
      background: rgba(255, 255, 255, .07);
      border: 1px solid rgba(255, 255, 255, .13);
      /* 16px so iOS Safari doesn't zoom the page on focus. */
      font-size: 16px;
    }
    .auth-card input:focus-visible {
      outline: none; border-color: rgba(255, 255, 255, .3);
    }
    .auth-btn { width: 100%; border-radius: 999px; padding: .62rem 1.1rem; }
    .auth-card button.primary.auth-btn {
      background:
        linear-gradient(120deg, rgba(255, 255, 255, .16), rgba(255, 255, 255, .02) 55%,
          rgba(255, 255, 255, .08)) var(--cinnabar-deep);
      border: 1px solid rgba(255, 255, 255, .22);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 6px 18px rgba(0, 0, 0, .25);
    }
    .auth-card button.primary.auth-btn:hover { background-color: var(--cinnabar); }
    .auth-btn-secondary {
      margin-top: .5rem;
      background: rgba(255, 255, 255, .06);
      border: 1px solid rgba(255, 255, 255, .13);
      -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .08);
    }
    .auth-btn-secondary:hover { background: rgba(255, 255, 255, .1); border-color: rgba(255, 255, 255, .2); }
    .auth-card .google-btn { border-radius: 999px; }
    .link-btn {
      display: block; width: 100%; margin-top: .7rem; padding: .2rem;
      background: none; border: 0; color: var(--ink-muted);
      font-size: .8rem; font-weight: 600; cursor: pointer; text-align: center;
    }
    .link-btn:hover { color: var(--cinnabar); border: 0; }
    /* Google's official "Sign in with Google" button (dark theme variant,
       from Google's brand button generator) — kept as its own fixed dark
       surface distinct from the site's own dark palette, per brand
       guidelines, but this variant happens to blend in well here. */
    .google-btn {
      -webkit-appearance: none;
      display: flex; width: 100%; box-sizing: border-box;
      align-items: center; justify-content: center; gap: .65rem;
      background-color: #131314; border: 1px solid #8e918f; border-radius: 4px;
      color: #e3e3e3; cursor: pointer;
      font-family: Roboto, "Be Vietnam Pro", arial, sans-serif;
      font-size: .88rem; font-weight: 500; letter-spacing: .01em;
      height: 40px; padding: 0 1rem; position: relative; overflow: hidden;
      transition: background-color .218s, border-color .218s, box-shadow .218s;
    }
    .google-btn svg, .google-btn span { position: relative; flex-shrink: 0; }
    .google-btn::after {
      content: ""; position: absolute; inset: 0; opacity: 0;
      background: #fff; transition: opacity .218s;
    }
    .google-btn:hover {
      box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .3), 0 1px 3px 1px rgba(0, 0, 0, .15);
    }
    .google-btn:hover::after { opacity: .08; }
    .google-btn:active::after { opacity: .12; }
    form.inline { display: inline; }
    form.settings-row { display: flex; gap: .5rem; margin-top: .85rem; }

    /* ---- Status ---- */
    .pill {
      display: inline-block; padding: .1rem .55rem; border-radius: 3px;
      font-size: .7rem; font-weight: 650; letter-spacing: .06em; text-transform: uppercase;
    }
    .pill.posted, .pill.enabled { color: var(--jade); background: var(--tint-ok); }
    .pill.dry_run { color: var(--amber); background: var(--tint-warn); }
    .pill.failed { color: var(--cinnabar); background: var(--tint-err); }
    .pill.disabled { color: var(--ink-muted); background: var(--edge-soft); }

    .flash { padding: .65rem 1rem; border-radius: 4px; margin-bottom: 1rem; font-size: .9rem; }
    .flash.ok { background: var(--tint-ok); color: var(--jade); }
    .flash.err { background: var(--tint-err); color: var(--cinnabar); }

    /* ---- Hero ---- */
    .stats { display: flex; gap: 2.4rem; flex-wrap: wrap; margin: 1.3rem 0 .5rem; padding: 0; }
    .stats > div { margin: 0; }
    .stats dt {
      font-size: .64rem; font-weight: 700; letter-spacing: .18em;
      text-transform: uppercase; color: var(--ink-muted);
    }
    .stats dd { margin: .2rem 0 0; font-size: .98rem; font-weight: 500; }

    .page-title { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin: .4rem 0 1.25rem; }
    /* Browse dropdown on the catalog pages: fully custom liquid glass (a
       native <select> pops a system menu that ignores the theme); sits on
       the left, right after the page title. */
    .page-title .glass-select { margin-right: auto; }
    .glass-select { position: relative; }
    .glass-select-toggle {
      display: inline-flex; align-items: center; gap: .5rem; cursor: pointer;
      font: inherit; font-size: .85rem; font-weight: 600; color: var(--ink);
      padding: .45rem 1.05rem; border-radius: 999px;
      border: 1px solid rgba(255, 255, 255, .13);
      background:
        linear-gradient(120deg, rgba(255, 255, 255, .09), rgba(255, 255, 255, .02) 55%,
          rgba(255, 255, 255, .06)) rgba(30, 24, 21, .4);
      -webkit-backdrop-filter: blur(14px) saturate(1.5);
      backdrop-filter: blur(14px) saturate(1.5);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .08);
    }
    .glass-select-toggle svg { transition: transform .2s ease; color: var(--ink-muted); }
    .glass-select.open .glass-select-toggle svg { transform: rotate(180deg); }
    .glass-select-menu {
      display: none; position: absolute; top: calc(100% + .55rem); left: 0; z-index: 60;
      flex-direction: column; gap: .1rem; min-width: 11rem; padding: .4rem;
      background:
        linear-gradient(160deg, rgba(255, 255, 255, .05), rgba(255, 255, 255, 0) 40%)
        rgba(26, 20, 18, .86);
      -webkit-backdrop-filter: blur(20px) saturate(1.4);
      backdrop-filter: blur(20px) saturate(1.4);
      border: 1px solid rgba(255, 255, 255, .10); border-radius: 14px;
      box-shadow: 0 18px 44px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 255, 255, .06);
    }
    .glass-select.open .glass-select-menu { display: flex; }
    .glass-select-menu a {
      padding: .5rem .85rem; border-radius: 9px;
      font-size: .85rem; font-weight: 600; color: var(--ink); white-space: nowrap;
    }
    .glass-select-menu a:hover { background: rgba(255, 255, 255, .07); text-decoration: none; }
    .glass-select-menu a.active {
      background: rgba(255, 255, 255, .14);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10);
    }
    .glass-select-menu hr {
      border: 0; height: 1px; margin: .3rem .5rem;
      /* Tapered ends, echoing the footer hairline. */
      background: linear-gradient(to right, transparent,
        rgba(255, 255, 255, .16) 25%, rgba(255, 255, 255, .16) 75%, transparent);
    }

    /* ---- Home ----
       The page spans the full window; the side paddings line the grid up
       with the centered hero slide. The slide is sized so the first grid
       row stays visible below the slider on load. */
    main.home {
      /* Slides shrink with the window height, but only down to a floor —
         past that the slider keeps its size and the page scrolls. The last
         term is a hard cap (480px tall at 16:9) so large desktop windows
         keep the hero modest and the grid visible below it. */
      --slide-w: min(72vw, max(540px, (100vh - 12rem) * 16 / 9), 480px * 16 / 9);
      --slide-w: min(72vw, max(540px, (100dvh - 12rem) * 16 / 9), 480px * 16 / 9);
      max-width: none;
      padding-inline: clamp(1rem, 3vw, 3rem);
      padding-bottom: 2rem;
    }
    /* The hero is 16:9 everywhere: the artwork is 16:9 and object-fit: cover
       would crop its sides (faces, titles) in any other box. Only the WIDTH
       changes by breakpoint. */
    @media (max-width: 700px) {
      main.home { --slide-w: 86vw; }
    }
    /* Short landscape (a rotated phone). Bounded by viewport WIDTH, never
       height: a height-based value (100dvh …) blows the hero up wherever dvh
       misreports the current viewport — device emulation and iOS in landscape
       both hand back the portrait/large height, so an unbounded height calc
       produced a hero far taller than the window. 52vw keeps the 16:9 art
       inside the shortest common landscape phone and cannot overshoot. */
    @media (max-height: 500px) and (orientation: landscape) {
      main.home { --slide-w: 52vw; }
    }

    /* ---- Home hero slider ----
       Full-window looping carousel: the current slide sits centered with
       its neighbours peeking in from both edges; clicking a peeking slide
       brings it to the centre. Native scroll snap, so it also swipes on
       touch. Controls sit on artwork, hence fixed lacquer tones. */
    .hero-slider {
      /* Escape main's gutters with plain negative margins (the calc(50% -
         50vw) full-bleed trick misplaces the box in Safari). */
      position: relative;
      margin: 0 calc(-1 * clamp(1rem, 3vw, 3rem)) 1.4rem;
    }
    .hero-slider .slides {
      display: flex; gap: .9rem;
      padding-inline: calc((100vw - var(--slide-w)) / 2);
      overflow-x: auto; scroll-snap-type: x mandatory;
      scrollbar-width: none;
    }
    .hero-slider .slides::-webkit-scrollbar { display: none; }
    .hero-slider .slide {
      position: relative; flex: 0 0 var(--slide-w);
      scroll-snap-align: center;
      border-radius: 6px; overflow: hidden;
      background: var(--panel-raised); box-shadow: var(--shadow);
      opacity: .55; transition: opacity .6s ease;
    }
    .hero-slider .slide.current { opacity: 1; }
    /* Height comes from the image's own 16:9 aspect-ratio (in flow), exactly
       like the shelf cards below — NOT from aspect-ratio on the .slide flex
       item, which iOS Safari leaves stale after a rotate, cropping the art. */
    .hero-slider .slide .thumb { display: block; width: 100%; aspect-ratio: 16 / 9; height: auto; }
    .hero-slider .ep-chip { right: .8rem; bottom: .8rem; }
    /* Title overlaid on hero artwork with no baked-in title (see
       .thumb-title): larger, in the display serif, clear of the ep-chip. */
    .hero-slider .hero-title {
      position: absolute; left: 1.1rem; right: 6.5rem; bottom: .85rem;
      color: var(--ink);
      font-family: "Playfair Display", Georgia, serif;
      font-size: clamp(1.05rem, 2.4vw, 1.8rem); font-weight: 700; line-height: 1.18;
      text-shadow: 0 1px 2px rgba(0, 0, 0, .8), 0 2px 14px rgba(0, 0, 0, .65);
      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
      overflow: hidden; pointer-events: none;
    }

    /* ---- Public library ---- */
    .series-grid {
      display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
      gap: 1.1rem;
    }
    a.series-card {
      display: block; overflow: hidden; color: var(--ink);
      background: var(--panel); border: 1px solid var(--edge-soft); border-radius: 6px;
      box-shadow: var(--shadow);
      transition: transform .15s ease, border-color .15s ease;
    }
    a.series-card:hover { text-decoration: none; transform: translateY(-3px); border-color: var(--edge); }
    .series-card .meta { display: block; padding: .65rem .8rem .8rem; }
    .series-card .title {
      margin: 0; font-weight: 600; font-size: .92rem; line-height: 1.35;
      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
    }
    /* Smaller cards on handheld sizes (after the base rule so they win). */
    @media (max-width: 900px) {
      .series-grid { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: .7rem; }
      .series-card .title { font-size: .82rem; }
      .series-card .meta { padding: .5rem .6rem .6rem; }
    }
    @media (max-width: 700px) {
      .series-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    }
    .thumb {
      display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover;
      background: var(--panel-raised);
    }
    /* Loading shimmer (Netflix/Facebook-style): a soft highlight sweeps the
       placeholder until the image has painted over it. JS in the footer adds
       .loaded when each thumb finishes; skeleton pager cards shimmer too. */
    img.thumb:not(.loaded), .skeleton .thumb {
      background-image: linear-gradient(
        105deg, transparent 40%, rgba(255, 255, 255, 0.06) 50%, transparent 60%);
      background-size: 220% 100%;
      background-repeat: no-repeat;
      animation: thumb-shimmer 1.5s ease-in-out infinite;
    }
    @keyframes thumb-shimmer {
      from { background-position: 130% 0; }
      to { background-position: -130% 0; }
    }
    @media (prefers-reduced-motion: reduce) {
      img.thumb:not(.loaded), .skeleton .thumb { animation: none; }
    }
    .series-card.skeleton { pointer-events: none; }
    .thumb-wrap { position: relative; display: block; overflow: hidden; }
    .thumb-wrap .thumb { transition: transform .35s ease; }
    a.series-card:hover .thumb-wrap .thumb,
    a.feature-card:hover .thumb-wrap .thumb { transform: scale(1.04); }
    /* Episode count as a small cinnabar seal, echoing the brand mark. */
    .ep-chip {
      position: absolute; right: .55rem; bottom: .55rem;
      padding: .12rem .5rem; border-radius: 2px;
      background: var(--cinnabar-deep); color: var(--cinnabar-ink);
      font-family: "IBM Plex Mono", ui-monospace, monospace;
      font-size: .68rem; font-weight: 500; letter-spacing: .05em;
      white-space: nowrap;
    }
    /* Title overlaid on thumbnails whose artwork has no baked-in title
       (Playlist.thumb_has_text is False — see app/thumb_text.py). The
       gradient is a legibility scrim over the artwork, not decoration. */
    .thumb-title {
      position: absolute; left: 0; right: 0; bottom: 0;
      padding: 1.9rem .65rem .5rem;
      background: linear-gradient(to top, rgba(12, 9, 10, .92), rgba(12, 9, 10, 0));
      color: var(--ink); font-weight: 600; font-size: .82rem; line-height: 1.3;
      text-shadow: 0 1px 2px rgba(0, 0, 0, .55);
      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
      overflow: hidden; pointer-events: none;
    }
    @media (max-width: 900px) {
      .thumb-title { font-size: .74rem; padding: 1.5rem .5rem .4rem; }
    }
    /* Empty states are the only text on the page, so the muted grey reads as
       washed out — lift them closer to the body ink and up a size. */
    .empty-note {
      margin: 0; font-size: .95rem;
      color: var(--ink);
      color: color-mix(in srgb, var(--ink) 80%, transparent);
    }

    /* Card hover: play and episode-list actions surface over the artwork
       on pointer devices; touch devices just tap through to the player. */
    .card-hover {
      position: absolute; left: 0; right: 0; bottom: 0;
      display: none; align-items: center; justify-content: space-between; gap: .6rem;
      padding: .5rem .6rem;
      background: rgba(22, 17, 19, .88);
      transform: translateY(102%); transition: transform .25s ease;
    }
    @media (hover: hover) {
      .card-hover { display: flex; }
      a.series-card:hover .card-hover,
      a.series-card:focus-visible .card-hover { transform: translateY(0); }
    }
    /* Continue Watching's mobile play badge: always visible (no hover to
       reveal it on touch), centred over the artwork, purely a visual
       affordance — the whole card is already one tap target to resume. */
    .mobile-play { display: none; }
    @media (hover: none) and (pointer: coarse) {
      [data-shelf="continue-watching"] .mobile-play {
        display: flex; align-items: center; justify-content: center;
        position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
        width: 3rem; height: 3rem; border-radius: 50%;
        background: rgba(22, 17, 19, .55); color: var(--ink);
        border: 1.5px solid var(--ink);
        pointer-events: none;
      }
    }
    .hover-actions { display: inline-flex; gap: .45rem; }
    .hover-btn {
      display: inline-flex; align-items: center; justify-content: center;
      width: 2rem; height: 2rem; padding: 0; border-radius: 50%;
      background: transparent; border: 1px solid rgba(240, 226, 214, .45);
      color: var(--ink); cursor: pointer;
      transition: border-color .15s ease, background-color .15s ease;
    }
    .hover-btn:hover { border-color: var(--ink); background: rgba(240, 226, 214, .14); }
    .hover-btn.play { background: var(--ink); border-color: var(--ink); color: var(--bg); }
    /* Any element carrying a data-tip gets the same floating label on
       hover/focus, centred above it by default. Elements that aren't
       already positioned (everything but shelf-remove, absolute for its
       own corner placement) need position:relative to anchor it. */
    [data-tip]::after {
      content: attr(data-tip);
      position: absolute; bottom: calc(100% + .55rem); left: 50%;
      transform: translateX(-50%);
      padding: .28rem .65rem; border-radius: 3px;
      background: var(--ink); color: var(--bg);
      font-size: .72rem; font-weight: 600; letter-spacing: .01em; white-space: nowrap;
      opacity: 0; pointer-events: none; transition: opacity .15s ease;
      z-index: 10;
    }
    [data-tip]::before {
      content: "";
      position: absolute; bottom: calc(100% + .12rem); left: 50%;
      transform: translateX(-50%);
      border: .32rem solid transparent; border-top-color: var(--ink);
      opacity: 0; pointer-events: none; transition: opacity .15s ease;
      z-index: 10;
    }
    [data-tip]:hover::after, [data-tip]:focus-visible::after,
    [data-tip]:hover::before, [data-tip]:focus-visible::before { opacity: 1; }
    .hover-btn[data-tip], .ep-nav[data-tip],
    #theater-toggle[data-tip], #watchlist-toggle[data-tip] { position: relative; }
    /* The corner ✕ sits right at the card's top-right edge, with no room
       above or centred beneath it for the tooltip the other buttons use —
       series-card's own overflow:hidden silently clips it there. Open this
       one downward and right-aligned to the button instead, so it grows
       into the card rather than off either edge. */
    .shelf-remove[data-tip]::after {
      bottom: auto; top: calc(100% + .55rem); left: auto; right: 0;
      transform: none;
    }
    .shelf-remove[data-tip]::before {
      bottom: auto; top: calc(100% + .12rem); left: auto; right: .6rem;
      transform: none;
      border-top-color: transparent; border-bottom-color: var(--ink);
    }
    /* Add-to-list button on catalog cards: a plus that becomes a cinnabar
       check once the series is in the viewer's list. */
    .hover-btn.add .icon-added { display: none; }
    .hover-btn.add[data-in-list="true"] { color: var(--cinnabar); }
    .hover-btn.add[data-in-list="true"] .icon-add { display: none; }
    .hover-btn.add[data-in-list="true"] .icon-added { display: block; }
    .hover-meta {
      font-family: "IBM Plex Mono", ui-monospace, monospace;
      font-size: .7rem; letter-spacing: .06em; color: var(--ink); white-space: nowrap;
    }

    /* Horizontally-scrolling "shelf" rows: Continue Watching / My List. */
    .shelf { margin: 0 0 1.8rem; }
    .shelf-track {
      display: flex; gap: 1.1rem; overflow-x: auto; overflow-y: hidden;
      scroll-snap-type: x proximity;
      scrollbar-width: none; -ms-overflow-style: none;
    }
    .shelf-track::-webkit-scrollbar { display: none; }
    .shelf-track .series-card {
      flex: 0 0 210px; min-width: 0; scroll-snap-align: start;
    }
    @media (max-width: 900px) {
      .shelf-track { gap: .7rem; }
      .shelf-track .series-card { flex-basis: 170px; }
    }
    @media (max-width: 700px) {
      .shelf-track .series-card { flex-basis: 46vw; }
    }
    /* "Popular This Week" ranked shelf: a large outlined rank number
       bleeding behind each thumbnail, streaming-service "Top 10" style.
       The number is absolutely positioned (not laid out via negative
       margin) so its own box never affects the row's flex sizing — a
       negative-margin overlap here mis-measured on Safari and left large
       gaps between cards. */
    /* scroll-padding-left matches padding-left so scroll-snap's "start"
       point lands at scrollLeft 0 — without it, proximity snapping
       auto-scrolled past the padding on load, hiding rank #1 entirely. */
    .shelf-track.ranked { padding-left: 3rem; scroll-padding-left: 3rem; gap: 3.6rem; }
    .rank-item { position: relative; flex: 0 0 210px; min-width: 0; scroll-snap-align: start; }
    .rank-number {
      position: absolute; left: -3rem; bottom: 0; z-index: 0;
      font-family: Georgia, "Playfair Display", serif;
      font-weight: 800; font-size: 6.5rem; line-height: 1;
      /* -webkit-text-fill-color (not just color) is needed for a reliably
         transparent fill on Safari — color alone left the stroke looking
         very faint there. */
      color: transparent; -webkit-text-fill-color: transparent;
      -webkit-text-stroke: 2.5px rgba(240, 232, 223, .8);
      pointer-events: none; user-select: none; white-space: nowrap;
    }
    .rank-item .series-card { position: relative; z-index: 1; width: 100%; }
    @media (max-width: 900px) {
      .rank-item { flex-basis: 170px; }
      .shelf-track.ranked { padding-left: 2.2rem; scroll-padding-left: 2.2rem; gap: 2.6rem; }
      .rank-number { font-size: 4.8rem; left: -2.2rem; }
    }
    @media (max-width: 700px) {
      .rank-item { flex-basis: 46vw; }
      .shelf-track.ranked { padding-left: 1.7rem; scroll-padding-left: 1.7rem; gap: 2rem; }
      .rank-number { font-size: 3.8rem; left: -1.7rem; }
    }
    /* Arrow-button paging over the shelf: hidden until hover, with a
       gradient scrim (not a solid block) so the next card still peeks
       through, matching streaming-service carousels. */
    .shelf-viewport { position: relative; }
    .shelf-nav {
      position: absolute; top: 0; bottom: .3rem; z-index: 2; width: 3.6rem;
      display: flex; align-items: center; border: none; padding: 0;
      color: var(--ink); cursor: pointer; opacity: 0;
      transition: opacity .18s ease;
    }
    .shelf-viewport:hover .shelf-nav, .shelf-viewport:focus-within .shelf-nav { opacity: 1; }
    .shelf-nav:disabled { opacity: 0 !important; pointer-events: none; }
    .shelf-nav svg { filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .6)); }
    .shelf-nav.prev {
      left: 0; justify-content: flex-start; padding-left: .3rem;
      background: linear-gradient(to right, rgba(22, 17, 19, .88), rgba(22, 17, 19, 0));
    }
    .shelf-nav.next {
      right: 0; justify-content: flex-end; padding-right: .3rem;
      background: linear-gradient(to left, rgba(22, 17, 19, .88), rgba(22, 17, 19, 0));
    }
    @media (hover: none) {
      .shelf-nav { display: none; }
    }
    .shelf-remove {
      position: absolute; top: .4rem; right: .4rem; z-index: 1;
      display: inline-flex; align-items: center; justify-content: center;
      width: 1.7rem; height: 1.7rem; padding: 0; border-radius: 50%;
      background: rgba(22, 17, 19, .78); border: 1px solid rgba(240, 226, 214, .45);
      color: var(--ink); cursor: pointer; opacity: 0; transition: opacity .15s ease, border-color .15s ease;
    }
    a.series-card:hover .shelf-remove, a.series-card:focus-within .shelf-remove { opacity: 1; }
    .shelf-remove:hover { border-color: var(--ink); background: rgba(22, 17, 19, .92); }
    /* Touch has no hover to reveal it, so the ✕ is always on — kept small
       and out of the way now that Continue Watching also has the play
       badge to visually anchor the card. */
    @media not (hover: hover) {
      .shelf-remove { opacity: 1; width: 1.5rem; height: 1.5rem; top: .3rem; right: .3rem; }
    }
    /* Danh sách's own page: the ✓ inside the hover overlay already handles
       removal on desktop, so its corner ✕ is mobile-only there — on a
       pointer device it would just be a second control doing the same
       thing right next to the first. */
    @media (hover: hover) {
      #watchlist-grid .shelf-remove { display: none; }
    }

    /* Watchlist swipe-to-delete: the card slides left off a delete panel that
       sits underneath it. Touch only — on desktop the card's ✓ button removes
       the series, so the panel never renders. */
    /* Clipped: the card slides away inside its own cell rather than travelling
       across the neighbouring one, and the rounded corners here clip both the
       card and the panel, so no wedge of page can show between them. */
    .swipe-item { position: relative; overflow: hidden; border-radius: 6px; }
    /* Full height, or the panel behind it shows past the bottom of a card
       whose title wraps to fewer lines than its neighbour's. */
    .swipe-item > .series-card {
      position: relative; z-index: 1; height: 100%; transition: transform .22s ease;
    }
    .swipe-item.dragging > .series-card { transition: none; }
    .swipe-delete {
      /* The whole cell — full width so sliding the card uncovers more red
         rather than sliding it off a fixed-width tab, and full height so the
         red runs the depth of the block, title strip included. */
      position: absolute; inset: 0; z-index: 0;
      display: none; padding: 0; border: 0; border-radius: 0;
      background: var(--cinnabar); color: #fff; cursor: pointer;
    }
    /* Sits where the card's edge exposes it, not in the middle of a strip the
       card is still covering. */
    .swipe-delete svg { position: absolute; top: 50%; right: 1.6rem; transform: translateY(-50%); }
    /* Only while the gesture is live: parked exactly under the card it would
       otherwise edge out past the card's rounded corners. */
    @media (hover: none) and (pointer: coarse) {
      .swipe-item.dragging > .swipe-delete,
      .swipe-item.open > .swipe-delete { display: flex; }
    }

    /* Episodes-and-info panel opened from a card. */
    body.modal-open { overflow: hidden; }
    .series-modal { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 1.25rem; }
    .series-modal[hidden] { display: none; }
    .modal-backdrop { position: absolute; inset: 0; background: rgba(8, 4, 5, .72); }
    .modal-panel {
      position: relative; width: min(680px, 100%); max-height: 86vh;
      display: flex; flex-direction: column; overflow: hidden;
      background: var(--panel); border: 1px solid var(--edge);
      border-top-color: var(--gold); border-radius: 6px; box-shadow: var(--shadow);
    }
    /* A plain, non-scrolling header: only .modal-body below it scrolls, so
       nothing ever moves behind the header and it never needs to mask
       anything — it's just part of the same flat surface as the panel. */
    .modal-header {
      flex-shrink: 0;
      padding: 1.5rem 1.6rem 1rem;
      display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem;
      border-bottom: 1px solid var(--edge-soft);
    }
    .modal-body {
      flex: 1 1 auto; min-height: 0; overflow-y: auto;
      padding: 1rem 1.6rem 1.5rem;
    }
    .modal-close {
      flex-shrink: 0;
      display: inline-flex; align-items: center; justify-content: center;
      width: 2.2rem; height: 2.2rem; padding: 0; border-radius: 50%;
      background: var(--panel-raised); border: 1px solid var(--edge);
      color: var(--ink); cursor: pointer;
      transition: border-color .15s ease;
    }
    .modal-close:hover { border-color: var(--ink-muted); }
    /* Compact "sign in to continue" prompt (My List for guests, etc.) — a
       small card, not the full episodes-panel .modal-panel above. */
    .auth-prompt-card {
      position: relative; z-index: 1; max-width: 300px; width: 100%;
      background: var(--panel); border: 1px solid var(--edge); border-radius: 10px;
      padding: 1.6rem 1.4rem 1.4rem; text-align: center; box-shadow: var(--shadow);
    }
    .auth-prompt-card .modal-close { position: absolute; top: .6rem; right: .6rem; width: 1.9rem; height: 1.9rem; }
    .auth-prompt-card h2 {
      font-family: "Playfair Display", Georgia, serif;
      font-size: 1.1rem; font-weight: 600; margin: 0 0 .35rem;
    }
    .auth-prompt-card p { margin: 0 0 1.15rem; }
    .auth-prompt-card .btn.primary { display: block; }
    .modal-title {
      font-family: "Playfair Display", Georgia, serif;
      font-size: 1.35rem; font-weight: 600; line-height: 1.3;
      margin: 0 0 .3rem;
    }
    .modal-meta { margin: 0; }
    @media (prefers-reduced-motion: no-preference) {
      .series-modal:not([hidden]) .modal-panel { animation: rise .3s ease both; }
    }

    .watch-actions { display: flex; align-items: center; gap: .15rem; flex-shrink: 0; }
    /* Previous/next episode: icon-only, hidden at either end of the series. */
    .ep-nav {
      display: inline-flex; align-items: center; justify-content: center;
      padding: .3rem; border-radius: 4px;
      color: var(--ink-muted); cursor: pointer;
    }
    .ep-nav:hover { color: var(--ink); text-decoration: none; }
    .ep-nav[hidden] { display: none; }
    /* Theater toggle shows what pressing it does: expand when off,
       collapse when on (the body class tracks the current mode). */
    #theater-toggle {
      display: inline-flex; align-items: center; justify-content: center;
      padding: .3rem; flex-shrink: 0;
      background: transparent; border: 0; border-radius: 4px;
      color: var(--ink-muted); cursor: pointer;
      transition: color .15s ease;
    }
    #theater-toggle:hover { color: var(--ink); border: 0; }
    #theater-toggle .icon-exit { display: none; }
    /* Hidden below the sidebar breakpoint: with no sidebar there is nothing to
       toggle between — the episodes sit under the video in either mode — so
       the page is simply always theater and the button has nothing to do.
       Hidden on handhelds at any width for the same reason: a tablet stacks
       the list under the player however wide it is. */
    @media (max-width: 1023.98px), (hover: none) and (pointer: coarse) {
      #theater-toggle { display: none; }
    }
    body.theater-mode #theater-toggle .icon-enter { display: none; }
    body.theater-mode #theater-toggle .icon-exit { display: block; }
    /* My List toggle: cinnabar once added, same footprint as the theater
       toggle so the action row stays visually even. */
    #watchlist-toggle {
      display: inline-flex; align-items: center; justify-content: center;
      padding: .3rem; flex-shrink: 0;
      background: transparent; border: 0; border-radius: 4px;
      color: var(--ink-muted); cursor: pointer;
      transition: color .15s ease;
    }
    #watchlist-toggle:hover { color: var(--ink); }
    #watchlist-toggle .icon-added { display: none; }
    #watchlist-toggle[data-in-list="true"] { color: var(--cinnabar); }
    #watchlist-toggle[data-in-list="true"] .icon-add { display: none; }
    #watchlist-toggle[data-in-list="true"] .icon-added { display: block; }
    /* ---- Watch page ----
       Theater (default): player spans the wide column, episodes below.
       Theater off (desktop only): episodes become a right-hand sidebar.
       The player is fluid: width tracks the column, height via aspect-ratio. */
    main.watch { max-width: 1280px; }
    .watch-layout { display: grid; grid-template-columns: 1fr; gap: 1.25rem; align-items: start; }
    .player-box {
      position: relative; aspect-ratio: 16 / 9; background: #000;
      border-radius: 6px; overflow: hidden; box-shadow: var(--shadow);
      /* Height follows the viewport, not just the width: never taller than
         the window (YouTube letterboxes inside the iframe when capped). */
      max-height: calc(100vh - 10rem);
      max-height: calc(100dvh - 10rem);
      /* Anchored to the left of its column, not centred. Once the height cap
         bites, the box is narrower than the column it sits in, and centring
         made the video slide rightwards as the window widened — the frame
         should grow from a fixed left edge, not wander. */
      margin-inline: 0;
    }
    @media (min-width: 901px) {
      /* ...but never squashed flat on short windows: below this the page
         scrolls instead. min-height wins over max-height by spec. */
      .player-box { min-height: 21rem; }
    }
    .watch-info {
      display: flex; justify-content: space-between; align-items: flex-start;
      gap: 1rem; margin: .9rem 0 .15rem;
    }
    #ep-heading { font-size: 1.15rem; margin: 0; }
    /* Theater: the page container itself spans the full window, so the video
       needs no viewport math (Safari misplaces calc(50% - 50vw) full-bleeds);
       the text and episode list keep their own gutters.
       No lower width bound. Once the sidebar is gone the page is pinned to
       theater, so the video should meet both edges at every width below that —
       a 901px floor put the gutters back as the window kept shrinking. Touch
       devices never carry the .theater class (the pinned-player block bleeds
       the video itself), so this is the pointer-device path only. */
    body.theater-mode main.watch { max-width: none; padding-inline: 0; }
    .watch-layout.theater .player-box {
      width: 100%; margin: 0;
      border-radius: 0;
      aspect-ratio: auto; max-height: none;
      height: min(56.25vw, calc(100vh - 10rem));
      height: min(56.25vw, calc(100dvh - 10rem));
    }
    .watch-layout.theater .watch-info { padding-inline: clamp(1rem, 3vw, 3rem); }
    .watch-layout.theater .episodes-panel { margin-inline: clamp(1rem, 3vw, 3rem); }
    .player-box iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

    .episode-list { list-style: none; margin: 0; padding: 0; }
    /* The panel is a non-scrolling glass surface and the episode list scrolls
       inside it, the same split .modal-panel/.modal-body uses. When the panel
       scrolled itself, Firefox painted its scrollbar gutter square over the
       two right-hand corners, so a rounded panel lost them (Chrome and Safari
       overlay the scrollbar instead, and never showed it). Keeping the
       scrollbar on an inner element sits it inside the padding, clear of the
       corners, and pins the heading while the list scrolls. */
    .episodes-panel {
      padding: .9rem 1.1rem; margin: 0;
      display: flex; flex-direction: column; overflow: hidden;
    }
    .episodes-panel .episode-list { min-height: 0; overflow-y: auto; }
    /* Arrow paging over the episode list — the shelf carousel's pattern turned
       vertical: no scrollbar at all, a chevron at each end that fades in on
       hover and only while there is more list that way. */
    .list-viewport {
      position: relative; display: flex; flex-direction: column;
      flex: 1 1 auto; min-height: 0;
    }
    .list-nav {
      /* Width comes from the thumbnails, not the column: auto-fill only lays
         out whole tracks, so the column is usually a little wider than the
         grid it holds, and stretching to it put the chevron off-centre over
         the artwork (and the scrim over empty space). The watch page measures
         a row and sets --list-content-w. */
      position: absolute; left: 0; right: auto; width: var(--list-content-w, 100%);
      z-index: 2; height: 3rem;
      display: flex; justify-content: center; border: none; padding: 0;
      color: var(--ink); cursor: pointer; opacity: 0;
      transition: opacity .18s ease;
      /* Square: the global button radius left the scrim's corners rounded, so
         four little notches of uncovered artwork showed at the ends. */
      border-radius: 0;
    }
    .list-viewport:hover .list-nav, .list-viewport:focus-within .list-nav { opacity: 1; }
    .list-nav:disabled { opacity: 0 !important; pointer-events: none; }
    .list-nav svg { filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .6)); }
    .list-nav.up {
      top: 0; align-items: flex-start; padding-top: .25rem;
      background: linear-gradient(to bottom, rgba(22, 17, 19, .88), rgba(22, 17, 19, 0));
    }
    .list-nav.down {
      bottom: 0; align-items: flex-end; padding-bottom: .25rem;
      background: linear-gradient(to top, rgba(22, 17, 19, .88), rgba(22, 17, 19, 0));
    }
    /* Touch scrolls by swiping; the arrows would only cover episodes. */
    @media (hover: none) {
      .list-nav { display: none; }
    }
    .episode-grid {
      /* 260px tracks: at 300 the windows between the phone grid and the row
         layout could only fit one or two cards across, so each one blew up to
         450-580px — a single episode filling the width. 260 keeps two or three
         on every line in that range. */
      display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
      gap: .45rem .75rem;
      /* Cards that stop short of their track sit centred in it, so the spare
         width reads as even spacing rather than a ragged right edge. */
      justify-items: center;
    }
    /* Never wider than the rendition behind it (mqdefault, 320px). Where a
       track is roomier the card stops growing and the slack becomes spacing —
       a stretched thumbnail is worse than a wider gutter. */
    .episodes-panel .episode-list .ep { max-width: 320px; width: 100%; }
    /* The sidebar layout is for pointer devices only. A portrait iPad is
       1024px wide, and without the pointer test it picked up the sidebar's
       200px tracks and its title-hiding while the stacked handheld layout was
       also in force — four narrow cards down the left with the width to their
       right unused. Handhelds get the plain card grid instead. */
    @media (min-width: 1024px) and (hover: hover) and (pointer: fine) {
      /* A higher threshold than the other watch-page breakpoints (901px):
         the fixed 340px sidebar plus gap needs real width left over for the
         video column, or it reads as cramped rather than a proper sidebar —
         901px only left ~540px for the video, uncomfortably close to the
         sidebar itself. Below this width, episodes stay below the video
         (the plain single-column layout) instead.
         Stretch (the grid default, overriding .watch-layout's own
         align-items: start above) so the sidebar's height always matches
         the video column's actual height instead of being capped at a
         fixed 82vh independent of it — otherwise a video column shorter
         than that leaves the sidebar visibly not reaching down to align
         with its bottom edge. The 82vh max-height stays as a ceiling for
         the opposite case, an unusually tall video column. */
      /* The video column is sized from the video, not given all the leftover
         width. Its height is capped to the viewport, so on a wide-but-short
         window a 1fr column is far wider than the 16:9 frame inside it and
         the slack opens as a gap between the video and the episode list. Take
         whichever is smaller: the width that height allows, or everything
         except the sidebar. The list then sits right beside the frame at any
         window shape, and the leftover falls outside the pair instead of
         between them. */
      /* The sidebar takes whatever the video leaves, up to three thumbnails
         across: 3 x 200px + 2 x .45rem gaps = 614.4px, so the cap is 616 —
         auto-fill counts repetitions by a track's MAX size, and at 614 the
         third column missed by a rounding hair. Past three the leftover stays
         put on the right at any window width; a fourth column would turn the
         sidebar into a second catalogue rather than a place to pick the next
         episode. The video column's cap subtracts only the sidebar's MINIMUM,
         so the video is served first and the list grows into what survives. */
      .watch-layout:not(.theater) {
        grid-template-columns: min(calc((100vh - 10rem) * 16 / 9), calc(100% - 196px)) minmax(176px, 616px);
        grid-template-columns: min(calc((100dvh - 10rem) * 16 / 9), calc(100% - 196px)) minmax(176px, 616px);
        align-items: stretch;
      }
      /* align-self: start, because the 82vh ceiling is often a hair shorter
         than the row: stretch then cannot be satisfied and the box settles
         against the bottom, leaving the first thumbnail sitting below the top
         of the video. The top edges are the ones that must line up. */
      .watch-layout:not(.theater) .episodes-panel { max-height: 82vh; align-self: start; }
      /* One row per line in the sidebar. No titles here: across the catalogue
         94% of playlists print the identical label on every episode — the
         series name, which is already in the heading under the video, with the
         number already on the pill. Dropping it lets the column shrink to the
         picture, and that width goes to the video (about +28% picture area on
         a 1512x860 window). Leaving the titles out but keeping the column wide
         would be the worst of both: the artwork inflates to fill it and starts
         competing with the video for attention.
         The theater grid under the video keeps its titles — cards are wide
         enough there that the text costs nothing. */
      /* Tracks are capped at 200px rather than 1fr, so a wider sidebar adds
         columns instead of inflating one thumbnail — the artwork stays a
         constant size and any remainder sits at the right edge. */
      .watch-layout:not(.theater) .episode-grid {
        grid-template-columns: repeat(auto-fill, minmax(176px, 200px));
        gap: .45rem;
      }
      .watch-layout:not(.theater) .episodes-panel .episode-list .ep-title { display: none; }
      .watch-layout:not(.theater) .episodes-panel .episode-list .ep-thumb {
        width: 100%; flex: 1 1 auto;
      }
      /* The sidebar layout spans the window, the way the header does, so the
         video's left edge stays level with the wordmark at any width and the
         episode list pins to the right edge — YouTube's arrangement. The
         page's centred 1280px column instead slid the whole layout rightwards
         once the window outgrew it, while the header stayed put. */
      /* Matching bottom padding, so the list is framed evenly: the page's
         default 4.5rem tail left three times as much air under the sidebar as
         above it, which reads as the column having slipped upwards. */
      body:not(.theater-mode) main.watch { max-width: none; padding-bottom: 1.5rem; }
    }
    /* Short landscape windows (a rotated phone, or a browser window resized
       short-and-wide): the theater rules above cap player-box height well
       below what 16:9 needs at the full window width, so the box ends up
       far wider than the actual video — YouTube then pillarboxes the real
       16:9 frame inside it with black bars on both sides. Switch to a
       side-by-side layout instead, like YouTube's own mobile player does in
       landscape: the video sized to the available height at its own aspect
       ratio, flush left, with the episode list filling the leftover width
       on the right rather than sitting empty as pillarbox space. */
    @media (max-height: 500px) and (min-width: 480px) and (orientation: landscape) {
      /* The column width is computed directly from the same height math as
         the box below, rather than left to intrinsic ("max-content") grid
         sizing — that would size the column off the episode heading's
         unwrapped text width instead of the video, shoving the episode
         list off-screen to the right for anything but a very short title. */
      /* The player column is sized from the viewport height, but capped at
         (100vw − sidebar) with min() so it can NEVER be wider than the space
         left for the episode list — otherwise the list spills off the right.
         Height uses svh (smallest viewport): unlike dvh, it never over-reports
         on iOS landscape (dvh there hands back the taller portrait height,
         which is what pushed the sidebar off-screen). */
      .watch-layout, .watch-layout.theater {
        grid-template-columns: min(calc((100vh - 6rem) * 16 / 9), calc(100vw - 268px)) minmax(224px, 1fr);
        grid-template-columns: min(calc((100svh - 6rem) * 16 / 9), calc(100vw - 268px)) minmax(224px, 1fr);
      }
      .player-box, .watch-layout.theater .player-box {
        width: 100%; aspect-ratio: 16 / 9; height: auto;
        margin: 0; max-height: none;
      }
      .watch-layout.theater .watch-info { padding-inline: 0; }
      .episodes-panel, .watch-layout.theater .episodes-panel {
        margin: 0; padding: .6rem; max-height: calc(100vh - 6rem);
        max-height: calc(100svh - 6rem);
      }
      .episode-grid { grid-template-columns: 1fr; gap: .4rem; }
      /* A movie has no episode list to fill the right-hand column, which
         would sit as dead space — span the full width instead and let
         YouTube pillarbox the frame inside, like its own landscape player. */
      .watch-layout.movie { grid-template-columns: 1fr; }
      .watch-layout.movie .player-box {
        aspect-ratio: auto;
        height: calc(100vh - 6rem);
        height: calc(100svh - 6rem);
      }
    }
    .episode-list .ep a {
      display: flex; gap: .65rem; align-items: center;
      padding: .45rem .55rem; border-radius: 4px; color: var(--ink);
      border-left: 2px solid transparent;
    }
    .episode-list .ep a:hover { background: var(--panel-raised); text-decoration: none; }
    .episode-list .ep.current a {
      background: var(--tint-err); border-left-color: var(--cinnabar);
    }
    .episode-list .thumb { width: 106px; flex-shrink: 0; border-radius: 3px; }
    .episode-list .ep-no {
      font-family: "IBM Plex Mono", ui-monospace, monospace;
      font-size: .74rem; color: var(--ink-muted); min-width: 1.6rem; text-align: right;
    }
    .episode-list .ep-title {
      font-size: .84rem; line-height: 1.35;
      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
    }
    /* "Đang phát" badge — a single element the watch page moves into whichever
       episode is playing. Only the thumbnail grid shows it (the row layouts
       mark the current episode with their own tint), so it stays hidden until
       that media query turns it on. */
    .now-playing { display: none; }
    /* The episode list is a set of thumbnail cards — bottom gradient, "Tập N"
       pill, and a badge on the one playing and the one up next. Every episode
       of a series carries the same artwork, so the repeated title beside each
       row was pure noise; the pills carry all the information a row of text
       did, in less space. Applies wherever the panel appears (phone grid,
       desktop sidebar, the wide grid under a theater-mode video) — only the
       column count changes per breakpoint.
       Scoped to .episodes-panel so the homepage series modal, which reuses
       .episode-list, keeps its own row layout. Placed after the general
       .episode-list rules above (same specificity) so it wins the tie. */
    .episodes-panel .episode-list .ep a {
      display: block; padding: 0; gap: 0; border-left: 0; border-radius: 6px;
    }
    .episodes-panel .episode-list .ep-thumb {
      position: relative; display: block; border-radius: 6px; overflow: hidden;
    }
    /* The episode playing now: the whole thumbnail dims and a badge sits over
       it. A coloured border was easy to lose against artwork this busy, and
       every card in a series shares the same artwork — so the cue has to be a
       change to the picture itself, not an edge. */
    .episodes-panel .episode-list .ep.current a { background: none; }
    .episodes-panel .episode-list .ep.current .ep-thumb::before {
      content: ""; position: absolute; inset: 0; z-index: 1;
      background: rgba(6, 3, 4, .58);
    }
    /* Same pill, same corner as "Tiếp theo", so the two states read as one
       system — gold rather than cinnabar so playing and next never blur
       together on adjacent cards. */
    .episodes-panel .episode-list .ep.current .now-playing {
      display: inline-flex; align-items: center; gap: .28rem;
      position: absolute; bottom: .3rem; left: .3rem; z-index: 2;
      padding: .1rem .5rem .1rem .42rem; border-radius: 999px;
      background: var(--gold); color: #191113;
      font-size: .7rem; font-weight: 600; letter-spacing: .01em;
    }
    /* Everything in em, so the meter scales with whatever size the badge is
       at (the sidebar runs it smaller than the cards do). */
    .now-playing .eq {
      display: inline-flex; align-items: flex-end; gap: .13em; height: .85em;
    }
    .now-playing .eq i {
      display: block; width: .17em; height: 100%; border-radius: .08em;
      background: currentColor; transform-origin: bottom;
      animation: now-playing-eq .9s ease-in-out infinite alternate;
    }
    /* Negative delays start each bar mid-cycle, so they never march in step. */
    .now-playing .eq i:nth-child(2) { animation-delay: -.3s; }
    .now-playing .eq i:nth-child(3) { animation-delay: -.55s; }
    /* Paused holds the bars where they stand rather than resetting them, so
       the meter reads as stopped mid-level — the shape still says "playing
       this one", the stillness says "not right now". */
    .now-playing.paused .eq i { animation-play-state: paused; }
    @keyframes now-playing-eq { from { transform: scaleY(.3); } to { transform: scaleY(1); } }
    @media (prefers-reduced-motion: reduce) {
      /* Frozen at three different heights, so it still reads as a meter. */
      .now-playing .eq i { animation: none; transform: scaleY(.75); }
      .now-playing .eq i:nth-child(2) { transform: scaleY(1); }
      .now-playing .eq i:nth-child(3) { transform: scaleY(.5); }
    }
    /* The dimming is the state, so it covers the whole row — title included,
       not just the picture. The badge keeps its full contrast on top. */
    .episodes-panel .episode-list .ep.current .ep-title { opacity: .55; }
    .episodes-panel .episode-list .thumb { display: block; width: 100%; border-radius: 6px; }
    /* A soft dark gradient hugging the bottom edge, so the pills stay legible
       over bright artwork. */
    .episodes-panel .episode-list .ep-thumb::after {
      content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 45%;
      background: linear-gradient(to top, rgba(6, 3, 4, .82), transparent);
      pointer-events: none; z-index: 1;
    }
    /* The episode number becomes a "Tập N" pill in the bottom-right, sitting
       above the gradient (the word comes from data-ep-word: Tập / Phần).
       Right, because the artwork's own episode badge is nearly always in the
       top-left and the two read as competing labels stacked down one edge. */
    .episodes-panel .episode-list .ep-no {
      position: absolute; top: auto; bottom: .3rem; left: auto; right: .3rem; z-index: 2;
      min-width: 0; padding: .1rem .5rem; border-radius: 999px;
      background: rgba(8, 4, 5, .72); color: var(--ink); text-align: right;
      font-family: inherit; font-size: .72rem; font-weight: 600;
    }
    .episodes-panel .episode-list .ep-no::before { content: attr(data-ep-word) " "; }
    /* The episode you'll land on next is called out on the opposite corner,
       so the pair reads "Tiếp theo … Tập 2" along the same gradient. */
    .episodes-panel .episode-list .ep.next .ep-thumb::before {
      content: "Tiếp theo";
      position: absolute; bottom: .3rem; left: .3rem; z-index: 2;
      padding: .1rem .5rem; border-radius: 999px;
      background: var(--cinnabar); color: #fff;
      font-size: .7rem; font-weight: 600; letter-spacing: .01em;
    }
    .episodes-panel .episode-list .ep-title { display: none; }
    /* The panel is the cards and nothing else — no glass box around them, on
       any screen. Placed after the glass styling further down would be the
       other way to win the tie; this sits before it, so it re-states the
       reset there instead. */
    /* Anywhere there's room, the list is rows: a small picture with the title
       beside it, the same shape whether it's the desktop sidebar or the grid
       under a theater-mode video. Full-bleed artwork at this size wastes the
       width — the episodes all share one image, so a bigger copy of it says
       nothing the title doesn't. Below this a card is only a couple of hundred
       pixels across (the two-up phone grid, the short landscape column) and
       the picture has to carry the row alone. */
    @media (min-width: 1024px) and (min-height: 501px) and (hover: hover) and (pointer: fine) {
      /* 1024px is where the grid can hold three of these columns; below it the
         auto-fill maths lands on one or two very wide ones, and a 148px
         picture with a title stranded in 500px of space reads as broken. So
         narrower windows keep the thumbnails-only cards, which fill whatever
         width they are given. It is also the width the sidebar appears at, so
         the sidebar and the theater grid change shape together.
         300px tracks, not 330, for the same reason: tighter tracks keep every
         column close to the width a row actually needs. */
      .episodes-panel .episode-grid {
        grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
        gap: .45rem .9rem;
      }
      /* No padding on the row: it would set the thumbnail down from the top of
         the list, and the first one has to line up with the top of the video
         beside it. The gap between rows does the separating instead. */
      .episodes-panel .episode-list .ep a {
        display: flex; align-items: center; gap: .6rem; padding: 0;
      }
      /* Rows, not cards: the picture is a fixed 148px and the title takes the
         rest, so the row wants its whole track — the card cap does not apply. */
      .episodes-panel .episode-list .ep { max-width: none; }
      .episodes-panel .episode-list .ep-thumb {
        width: 148px; flex-shrink: 0; border-radius: 4px;
      }
      .episodes-panel .episode-list .ep-title {
        display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
        overflow: hidden; font-size: .8rem; line-height: 1.35;
      }
      /* Smaller pills: at full size "Tiếp theo" and "Tập 12" together overrun
         a 148px thumbnail. */
      .episodes-panel .episode-list .ep-no,
      .episodes-panel .episode-list .ep.next .ep-thumb::before,
      .episodes-panel .episode-list .ep.current .now-playing {
        font-size: .62rem; padding: .06rem .4rem; bottom: .25rem;
      }
      .episodes-panel .episode-list .ep-no { right: .25rem; }
      .episodes-panel .episode-list .ep.next .ep-thumb::before,
      .episodes-panel .episode-list .ep.current .now-playing { left: .25rem; }
    }
    @media (max-width: 600px) {
      /* Portrait phones: two thumbnail cards per row. */
      .episodes-panel .episode-grid { grid-template-columns: 1fr 1fr; gap: .5rem; }
      .modal-header { padding: 1.1rem .9rem .8rem; }
      .modal-body { padding: .8rem .9rem 1.1rem; }
    }
    /* Portrait phone, watch page: the video and its title row pin together as
       one bar under the header, and the episode grid scrolls underneath. What
       you're watching and the next/save buttons stay reachable however far
       down the list you go.
       Gated on hover/pointer, the same test the bottom tab bar uses: wherever
       that bar is on screen the device is a handheld, tablets included, and
       gets the handheld treatment — a narrowed desktop window does not. No
       width cap, so a portrait iPad stacks the list under a pinned player
       rather than running the desktop sidebar. */
    @media (hover: none) and (pointer: coarse) and (orientation: portrait) {
      /* The video meets the header's bottom border: main's top padding would
         otherwise leave a strip of page showing above a bar that is flush once
         it pins. The side padding is put back explicitly, because a movie page
         forces theater mode and theater strips it — everything below bleeds
         out by 1rem and pads back 1rem on the assumption it is there, so
         without it the title and actions sit flush against the screen edge and
         the video overhangs by 1rem each side. */
      body.watch-page main.watch { padding-top: 0; padding-inline: 1rem; }
      /* Block flow: a sticky grid item is trapped in its own grid area and
         would never move. As a block child the bar stays pinned for the whole
         length of the episode list. */
      body.watch-page .watch-layout { display: block; }
      body.watch-page .watch-layout > div:first-child {
        /* --head-h is the header's measured height (base.html's script); the
           fallback is its built height: 1.1rem + 2.05rem mark + .9rem + border.
           Pulled up a pixel so the two always overlap rather than risk a
           hairline seam; z-index 30 keeps it under the header's 40, so the
           overlap is hidden behind the header rather than showing through. */
        position: sticky; top: calc(var(--head-h, 4.1rem) - 1px); z-index: 30;
        /* Bleeds to both edges so the grid can't show through beside the bar,
           then pads the title row back to the page gutter. */
        margin-inline: -1rem; padding: 0 1rem;
      }
      /* Scoped through .watch-layout to out-specify the theater player rule: a
         movie page is always in theater mode, and without this its video sat
         inside the gutters while a series ran edge to edge. */
      body.watch-page .watch-layout .player-box {
        width: auto; margin-inline: -1rem;
        aspect-ratio: 16 / 9; height: auto; max-height: none;
      }
      /* The title strip is glass, like the header and the tab bar — episodes
         pass under it frosted rather than disappearing behind a slab. Only
         this strip is blurred: the video above it is opaque anyway, and
         blurring that whole area every frame would cost for nothing. */
      body.watch-page .watch-info {
        margin: 0 -1rem; padding: .6rem 1rem .65rem;
        background: var(--head-bg);
        -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
        border-bottom: 1px solid var(--edge-soft);
      }
    }

    /* ---- Liquid glass (public site only) ----
       The admin console (body.admin) keeps its flat porcelain/lacquer
       panels. Glass goes only where content moves beneath the surface:
       the floating header, chips and hover sheets on artwork, and the
       episodes modal. Panels on the plain background stay opaque. */
    /* The wordmark as glass: a top-lit gradient inside the letterforms,
       matching how the panes catch light. Guarded so browsers without
       background-clip:text keep the plain ink color. */
    @supports ((-webkit-background-clip: text) or (background-clip: text)) {
      body:not(.admin) .brand {
        background: linear-gradient(to bottom, rgba(255, 250, 244, 1) 20%, rgba(255, 250, 244, 0.55));
        -webkit-background-clip: text;
        background-clip: text;
        color: transparent;
      }
    }
    /* Glass edges: every border is a gradient ring, lit at the top and
       fading toward the bottom — a pane catching light from above. Done
       with two background layers (fill clipped to padding-box, ring
       clipped to border-box) under a transparent border. */
    body:not(.admin) .card {
      border-radius: 14px;
      border-color: transparent;
      background: linear-gradient(var(--panel), var(--panel)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.24), rgba(255, 245, 235, 0.06)) border-box;
    }
    body:not(.admin) .hero-slider .slide {
      border-radius: 14px;
      border: 1px solid transparent;
      background: linear-gradient(var(--panel-raised), var(--panel-raised)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.3), rgba(255, 245, 235, 0.07)) border-box;
    }
    body:not(.admin) a.series-card {
      border-radius: 12px;
      border-color: transparent;
      background: linear-gradient(var(--panel), var(--panel)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.26), rgba(255, 245, 235, 0.06)) border-box;
    }
    body:not(.admin) a.series-card:hover {
      border-color: transparent;
      background: linear-gradient(var(--panel), var(--panel)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.42), rgba(255, 245, 235, 0.12)) border-box;
    }
    body:not(.admin) .player-box { border-radius: 12px; }
    /* Square corners on a handheld held upright: the rounded box reads as a
       card on a desktop page, but there the video runs edge to edge and the
       radius only bites a notch out of the picture at each corner. Landscape
       keeps its corners — the video is inset from the window there, so it
       still reads as a card.
       Must sit after the 12px rule above, not before: same selector, so the
       later one wins and a media query adds no specificity of its own. */
    @media (hover: none) and (pointer: coarse) and (orientation: portrait) {
      body:not(.admin) .player-box { border-radius: 0; }
    }
    body:not(.admin) .watch-layout:not(.theater) .player-box {
      box-shadow: var(--shadow), 0 0 0 1px rgba(255, 245, 235, 0.12);
    }
    body:not(.admin) .episode-list .thumb {
      border-radius: 7px;
      box-shadow: 0 0 0 1px rgba(255, 245, 235, 0.14);
    }
    body:not(.admin) .ep-chip {
      padding: .18rem .62rem; border-radius: 999px;
      border: 1px solid transparent;
      background: linear-gradient(rgba(14, 10, 12, 0.55), rgba(14, 10, 12, 0.55)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.24), rgba(255, 245, 235, 0.11)) border-box;
      -webkit-backdrop-filter: blur(14px) saturate(1.6);
      backdrop-filter: blur(14px) saturate(1.6);
      color: #fff6f0;
    }
    /* Light blur only: the sheet is a thin strip, so a heavy blur turns
       the artwork behind it to mud — at 10px shapes still ghost through. */
    body:not(.admin) .card-hover {
      background: rgba(22, 17, 19, 0.45);
      -webkit-backdrop-filter: blur(10px) saturate(1.5);
      backdrop-filter: blur(10px) saturate(1.5);
      border-top: 1px solid rgba(255, 245, 235, 0.1);
    }
    /* Dark well with a quiet ring, like the modal close button; hover
       only lifts the fill a shade. */
    body:not(.admin) .hover-btn:not(.play) {
      border-color: transparent;
      background: linear-gradient(rgba(14, 10, 12, 0.6), rgba(14, 10, 12, 0.6)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.24), rgba(255, 245, 235, 0.11)) border-box;
      -webkit-backdrop-filter: blur(10px);
      backdrop-filter: blur(10px);
    }
    body:not(.admin) .hover-btn:not(.play):hover,
    body:not(.admin) .hover-btn:not(.play):focus-visible {
      border-color: transparent;
      background: linear-gradient(rgba(56, 46, 50, 0.65), rgba(56, 46, 50, 0.65)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.3), rgba(255, 245, 235, 0.14)) border-box;
    }
    body:not(.admin) .hover-btn.play:hover,
    body:not(.admin) .hover-btn.play:focus-visible {
      background: #fffdf9; border-color: #fffdf9;
    }
    body:not(.admin) .shelf-remove {
      border-color: transparent;
      background: linear-gradient(rgba(14, 10, 12, 0.6), rgba(14, 10, 12, 0.6)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.24), rgba(255, 245, 235, 0.11)) border-box;
      -webkit-backdrop-filter: blur(10px);
      backdrop-filter: blur(10px);
    }
    body:not(.admin) .shelf-remove:hover,
    body:not(.admin) .shelf-remove:focus-visible {
      border-color: transparent;
      background: linear-gradient(rgba(56, 46, 50, 0.65), rgba(56, 46, 50, 0.65)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.3), rgba(255, 245, 235, 0.14)) border-box;
    }
    body:not(.admin) .modal-panel {
      border: 1px solid transparent; border-radius: 18px;
      background: linear-gradient(var(--glass-heavy), var(--glass-heavy)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.3), rgba(255, 245, 235, 0.07)) border-box;
      -webkit-backdrop-filter: blur(12px) saturate(1.5);
      backdrop-filter: blur(12px) saturate(1.5);
      box-shadow: var(--shadow);
    }
    body:not(.admin) .modal-header {
      /* No fill or blur of its own: .modal-panel's overflow: hidden clips it
         to the same rounded corners, and since only .modal-body scrolls,
         nothing ever moves behind the header for it to mask. */
      border-bottom-color: var(--glass-edge);
    }
    /* Dark well with a quiet ring, barely brighter at the top; hover only
       lifts the fill a shade. */
    body:not(.admin) .modal-close {
      border-color: transparent;
      background: linear-gradient(rgba(14, 10, 12, 0.6), rgba(14, 10, 12, 0.6)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.24), rgba(255, 245, 235, 0.11)) border-box;
      -webkit-backdrop-filter: blur(10px);
      backdrop-filter: blur(10px);
    }
    body:not(.admin) .modal-close:hover,
    body:not(.admin) .modal-close:focus-visible {
      border-color: transparent;
      background: linear-gradient(rgba(56, 46, 50, 0.65), rgba(56, 46, 50, 0.65)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.3), rgba(255, 245, 235, 0.14)) border-box;
    }
    /* Scrollbars inside glass panels: a slim translucent thumb on no
       track, inset from the rounded corners. Firefox and new Chrome use
       the standard properties; Safari uses the -webkit- pseudos. */
    body:not(.admin) .modal-body {
      scrollbar-width: thin;
      scrollbar-color: rgba(255, 245, 235, 0.22) transparent;
    }
    body:not(.admin) .modal-body::-webkit-scrollbar { width: 8px; }
    body:not(.admin) .modal-body::-webkit-scrollbar-track {
      background: transparent; margin-block: 16px;
    }
    body:not(.admin) .modal-body::-webkit-scrollbar-thumb {
      background: rgba(255, 245, 235, 0.22); border-radius: 999px;
      border: 2px solid transparent; background-clip: padding-box;
    }
    body:not(.admin) .modal-body::-webkit-scrollbar-thumb:hover {
      background: rgba(255, 245, 235, 0.35);
      border: 2px solid transparent; background-clip: padding-box;
    }
    /* No scrollbar on the episode list: the .list-nav chevrons page it, the
       same way the shelves are paged horizontally. A bar of any width reserves
       a gutter beside the thumbnails and reads as furniture next to artwork. */
    body:not(.admin) .episodes-panel .episode-list { scrollbar-width: none; }
    body:not(.admin) .episodes-panel .episode-list::-webkit-scrollbar { display: none; }
    body:not(.admin) .episodes-panel {
      border: 1px solid transparent; border-radius: 16px;
      background: linear-gradient(var(--glass), var(--glass)) padding-box,
                  linear-gradient(to bottom, rgba(255, 245, 235, 0.28), rgba(255, 245, 235, 0.06)) border-box;
      -webkit-backdrop-filter: var(--glass-blur);
      backdrop-filter: var(--glass-blur);
      box-shadow: var(--shadow);
    }
    /* Glass needs the blur to read as material: without backdrop-filter
       (or when the viewer asks for less transparency) fall back to
       near-opaque lacquer so text stays legible. */
    @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
      body:not(.admin) .ep-chip { background: rgba(26, 19, 22, 0.78); border-color: var(--glass-edge); }
      body:not(.admin) .card-hover { background: rgba(22, 17, 19, 0.92); }
      body:not(.admin) .modal-panel,
      body:not(.admin) .episodes-panel { background: #241b1f; border-color: var(--glass-edge); }
    }
    @media (prefers-reduced-transparency: reduce) {
      body:not(.admin) .ep-chip { background: rgba(26, 19, 22, 0.78); border-color: var(--glass-edge); }
      body:not(.admin) .card-hover { background: rgba(22, 17, 19, 0.92); }
      body:not(.admin) .modal-panel,
      body:not(.admin) .episodes-panel { background: #241b1f; border-color: var(--glass-edge); }
    }
    /* The episode list drops its glass box entirely — no background, border,
       blur or shadow — so the thumbnails sit flush against the page and read
       as one set of cards rather than a panel of them. Placed after the box
       styling above so it wins the source-order tie. */
    body:not(.admin) .episodes-panel {
      background: none; border: 0; border-radius: 0; box-shadow: none;
      -webkit-backdrop-filter: none; backdrop-filter: none;
      /* No padding either: the .card default would drop the first thumbnail
         below the top of the video it sits beside. */
      padding: 0;
    }

    @media (prefers-reduced-motion: no-preference) {
      .card { animation: rise .4s ease both; }
      @keyframes rise { from { opacity: 0; transform: translateY(6px); } }
      /* Cards surface in a quiet diagonal wave, column by column. */
      .hero-slider, .series-card { animation: rise .45s ease both; }
      /* Watchlist cards sit inside a swipe wrapper — the wrapper takes the
         entrance animation so the card's own transform stays free for the
         swipe (a running animation would otherwise override it). */
      .swipe-item { animation: rise .45s ease both; }
      .swipe-item > .series-card { animation: none; }
      .series-grid > * { animation-delay: .06s; }
      .series-grid > :nth-child(4n+2) { animation-delay: .11s; }
      .series-grid > :nth-child(4n+3) { animation-delay: .16s; }
      .series-grid > :nth-child(4n) { animation-delay: .21s; }
    }
    /* Apple Wallet's card-delete: the card squishes flat into a thin
       horizontal line and fades, centred in place.
       `animation: none` is the load-bearing line, for the same reason the
       swipe wrapper above needs it: `rise` is set to fill *both*, so it keeps
       applying transform and opacity long after it has finished, and an
       animation outranks a transition in the cascade. Leave it on and this
       transition computes correctly but never paints — which is exactly how
       it failed on the homepage shelves, whose cards take `rise` directly
       (watchlist cards already sit behind a wrapper that absorbs it).
       `a.series-card.card-dissolving` (element + 2 classes) ties the hover
       rule's own specificity and sits after it in the file, so it wins the
       source-order tie even while the pointer is still over the card — the
       common case, since delete is usually clicked from the hover overlay. */
    /* Step one, applied a frame earlier while the card still looks untouched:
       drop `rise` on its own. Cancelling it in the same frame that sets the
       target values leaves Chrome and Firefox without a settled start value
       and they jump straight to the end — Safari interpolates from the
       cancelled value, which is why the delete animated there and nowhere
       else. Watchlist cards never hit this: their wrapper already absorbs
       `rise`, so there was nothing to cancel and they animated everywhere. */
    a.series-card.card-dissolve-ready { animation: none; }
    a.series-card.card-dissolving {
      animation: none;
      transform-origin: center;
      transition: transform .32s cubic-bezier(.4, 0, .6, 1), opacity .32s ease .1s;
      transform: scaleY(.04) scaleX(.92);
      opacity: 0;
      pointer-events: none;
    }
    @media (prefers-reduced-motion: reduce) {
      /* Fade only, no squish, and no delay — the JS still waits on this
         class, so the timing has to be real even without the motion. */
      a.series-card.card-dissolving {
        animation: none; transform: none; transition: opacity .16s ease; opacity: 0;
      }
    }
    @media (max-width: 600px) {
      .site-head, main { padding-inline: 1rem; }
      .stats { gap: 1.3rem; }
    }
