Webtoons Dark Mode

Scoped dark theme for Webtoons (desktop + mobile) — comic panels render untouched. Elevated viewer card, accent-green active nav, WCAG-tuned contrast, optional reader dim. OS preference on first install; persistent toggle (Alt+Shift+T).

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Webtoons Dark Mode
// @namespace    https://github.com/hervad/webtoons-dark-mode
// @version      1.2.4
// @description  Scoped dark theme for Webtoons (desktop + mobile) — comic panels render untouched. Elevated viewer card, accent-green active nav, WCAG-tuned contrast, optional reader dim. OS preference on first install; persistent toggle (Alt+Shift+T).
// @author       hervad
// @match        https://www.webtoons.com/*
// @match        https://m.webtoons.com/*
// @icon         https://www.webtoons.com/favicon.ico
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @noframes
// @license      MIT
// @homepageURL  https://github.com/hervad/webtoons-dark-mode
// @supportURL   https://github.com/hervad/webtoons-dark-mode/issues
// ==/UserScript==

(function () {
    'use strict';

    const KEY_THEME = 'wt_dark_enabled';
    const KEY_DIM = 'wt_reader_dim';
    const VERSION = '1.2.4';

    // Retry cohort for SPA-navigation work (vignette class sync, viewer cards,
    // banner cleanup, panel glow). Webtoons renders the new page asynchronously
    // after pushState; one delay never fits all subapps, so we sample three
    // times spanning fast → slow bundles.
    const SPA_RETRY_DELAYS = [200, 800, 2000];

    // Log the startup banner as the FIRST runtime statement so that if anything
    // below throws, the console still proves the script loaded and which
    // version Tampermonkey is serving.
    console.info(`[webtoons-dark-mode] v${VERSION} starting`);

    /* ---------- palette (one place to retheme everything) ---------- */
    const palette = `
        :root {
            --wt-bg:              #15171a;
            --wt-bg-elev:         #22262b;
            --wt-bg-elev2:        #2c313a;
            --wt-bg-hover:        #30353c;
            --wt-bg-input:        #2a2e35;
            --wt-border:          #4a5360;
            --wt-border-strong:   #5a6472;
            --wt-text:            #e6e6e6;
            --wt-text-dim:        #b5b9c0;
            --wt-text-mute:       #878e99;
            --wt-text-on-accent:  #0a0a0a;
            --wt-link:            #7cb6ff;
            --wt-accent:          #00d564;
            --wt-accent-soft:     #4ade80;
            --wt-accent-like:     #f06868;
            --wt-shadow:          0 1px 2px rgba(0,0,0,.6);
        }
    `;

    /* ---------- theme: targeted selectors, no global filter ---------- */
    const theme = `
        html, body {
            background-color: var(--wt-bg) !important;
            color: var(--wt-text) !important;
            scrollbar-color: var(--wt-bg-elev2) var(--wt-bg);
        }

        /* Text */
        h1, h2, h3, h4, h5, h6, p, dt, dd, label, em, strong, small,
        .tit, .sub_tit, .subj, .author, .genre, .grade_num, .info, .summary {
            color: var(--wt-text) !important;
        }
        .desc, .date, .count, .from, .ico_view, .ico_grade, .num,
        .grade_area, .info_area .author, .nick, .meta, .help_txt, .comment_count {
            color: var(--wt-text-dim) !important;
        }

        /* Links */
        a, a:visited { color: var(--wt-text) !important; }
        a:hover      { color: var(--wt-link) !important; }

        /* Exclude button-styled anchors from the global a:hover blue. CTA
           buttons (e.g. age-verification "Continue", "First episode") use
           <a> or <button> with a green background; inheriting --wt-link
           turns their text bright blue on hover which looks broken. Keep
           the button's own text color on hover. */
        a.btn:hover, a[class*="btn_" i]:hover,
        a[class*="button" i]:hover,
        a[role="button"]:hover, button a:hover,
        a[class~="cta"]:hover, a[class*="_cta_" i]:hover, a[class*="_cta" i]:hover,
        a[class*="primary" i]:hover,
        a.lk_continue:hover, a._btn_enter:hover {
            color: inherit !important;
        }

        /* Keyboard focus ring — site has none of its own on most controls,
           which fails WCAG 2.4.7. A 2 px outline in --wt-border-strong with
           a 2 px offset reads clearly on every elevation level without
           clipping. :focus-visible so it only appears for keyboard nav. */
        a:focus-visible, button:focus-visible,
        input:focus-visible, textarea:focus-visible, select:focus-visible,
        [role="button"]:focus-visible, [tabindex]:focus-visible {
            outline: 2px solid var(--wt-border-strong) !important;
            outline-offset: 2px !important;
        }
        .NPI a, .lk_link, .more, .btn_link { color: var(--wt-link) !important; }

        /* Header / global nav — border-color and box-shadow only on the outer
           header shell, NOT on .gnb/.lnb nav lists (causes nav item artifacts). */
        #header, .header, .gnb_wrap, #gnbWrap, .header_bn {
            background-color: var(--wt-bg-elev) !important;
            border-color: var(--wt-border) !important;
            box-shadow: var(--wt-shadow) !important;
            z-index: 10000 !important;
        }
        .gnb, .lnb {
            background-color: var(--wt-bg-elev) !important;
        }
        .gnb a, .lnb a {
            color: var(--wt-text) !important;
            font-family: system-ui, -apple-system, 'Segoe UI', sans-serif !important;
            font-size: 23px !important;
            font-weight: 600 !important;
            letter-spacing: .05em !important;
            padding: 15px 20px !important;
            border-radius: 6px !important;
            transition: color .15s, background-color .15s !important;
        }
        /* Active page link — accent green + inset underline (box-shadow avoids
           clipping, works even when the parent has overflow:hidden). */
        /* Active GNB link: site sets aria-current="true" on the <a> itself.
           ID prefixes (#header / #gnbWrap) boost specificity above the site's
           own color rule which wins at 0,2,1 or lower. */
        #header .gnb a[aria-current="true"], #gnbWrap .gnb a[aria-current="true"],
        #header .lnb a[aria-current="true"], #gnbWrap .lnb a[aria-current="true"] {
            color: var(--wt-accent) !important;
            box-shadow: inset 0 -2px 0 var(--wt-accent) !important;
            border-radius: 6px 6px 0 0 !important;
        }
        /* GNB links wrap their text in <h1> — our blanket h1 rule sets
           color:--wt-text !important which beats the inherited accent color.
           Scope the override directly under #header so it wins by ID specificity. */
        #header a[aria-current="true"] h1,
        #header a[aria-current="true"] h2,
        #header a[aria-current="true"] h3,
        #gnbWrap a[aria-current="true"] h1,
        #gnbWrap a[aria-current="true"] h2 {
            color: var(--wt-accent) !important;
            font-size: inherit !important;
            font-weight: inherit !important;
        }
        /* Hover — rounded dark pill + accent text. */
        .gnb a:hover, .lnb a:hover, .gnb .link:hover, .header .link_menu:hover {
            color: var(--wt-accent) !important;
            background-color: var(--wt-bg-elev2) !important;
            border-radius: 6px !important;
        }

        /* Search button (top-right of header). Base CSS sets a light-grey
           circle (#f3f3f3) which is nearly invisible on our dark header. */
        .header_right .btn_search {
            background: var(--wt-bg-elev2) !important;
            border: 1px solid var(--wt-border) !important;
        }
        .header_right .btn_search:hover { background: var(--wt-bg-hover) !important; }
        .header_right .btn_search:before { filter: brightness(0) invert(1) opacity(.85) !important; }
        /* Log In button hover -- base CSS is #e0e0e0 (light grey, almost white).
           That made our white-glyph icon disappear into the hover background. */
        .header_right .link_login:hover, .header_right .btn_login:hover {
            background: var(--wt-bg-hover) !important;
        }

        /* Search dropdown — three nested layers: .search_area (outer panel) →
           .input_box (the rounded grey pill) → .input_search (the transparent
           <input>). All three need overriding; .input_box has its own bg. */
        .search_cont, .search_area, ._searchArea, .big_search.search_area {
            background: var(--wt-bg-elev) !important;
            border: 1px solid var(--wt-border) !important;
            box-shadow: 0 8px 24px rgba(0,0,0,.5) !important;
        }
        .search_area .input_box {
            background: var(--wt-bg-input) !important;
            border: 1px solid var(--wt-border) !important;
        }
        .input_search, ._txtKeyword,
        .search_area .input_search {
            background: transparent !important;
            color: var(--wt-text) !important;
        }
        input::placeholder { color: var(--wt-text-mute) !important; }
        .ly_autocomplete, ._searchLayer {
            background: var(--wt-bg-elev) !important;
            border: 1px solid var(--wt-border) !important;
            border-radius: 4px;
        }
        .ly_autocomplete li, ._searchLayer li { background: transparent !important; }
        .ly_autocomplete li:hover, ._searchLayer li:hover,
        .ly_autocomplete li.on, ._searchLayer li.on {
            background: var(--wt-bg-elev2) !important;
        }
        .ly_autocomplete a, ._searchLayer a, .ly_autocomplete .title { color: var(--wt-text) !important; }
        .search_area .ly_autocomplete .autocomplete_foot a { color: var(--wt-text-dim) !important; }

        /* Search autocomplete RESULT LIST (e.g. typing "roman" → Selfish Romance,
           Sweet Romance, ... Each <li class="link"> has white-on-hover from base
           CSS, plus #000 title + #8c8c8c info — all needs overriding. */
        .search_area .list_autocomplete li.on,
        .search_area .list_autocomplete .link:hover,
        .search_area .list_autocomplete li.on .link {
            background: var(--wt-bg-elev2) !important;
        }
        .search_area .list_autocomplete .subj { color: var(--wt-text) !important; }
        .search_area .list_autocomplete .info { color: var(--wt-text-dim) !important; }
        /* Bold-highlighted matching substring (e.g. "roman" inside "Sweet Romance").
           Base uses brand green #03aa5a — keep brand identity but use our accent var. */
        .search_area .list_autocomplete strong { color: var(--wt-accent) !important; }
        .search_area .list_autocomplete+.title { border-top-color: var(--wt-border) !important; }
        .search_area .list_autocomplete .info .bar { background: var(--wt-border) !important; }
        .search_area .list_autocomplete .pic:before { border-color: var(--wt-border) !important; }

        /* Creators section in search autocomplete — base hover is #f3f3f3 (white). */
        .search_area .list_creator .link:hover { background: var(--wt-bg-elev2) !important; }
        .search_area .list_creator .info { color: var(--wt-text-dim) !important; }
        .search_area .list_creator .info .bar { background: var(--wt-border) !important; }

        /* Cards / lists */
        .card_lst li, .card_item, .detail_lst li, .lst_area li,
        .daily_lst li, .ranking_lst li, .genre_lst li, .challenge_lst li,
        ._popularList li, ._dailyList li {
            background-color: var(--wt-bg-elev) !important;
            border-color: var(--wt-border) !important;
            border-radius: 6px;
        }
        .card_lst li:hover, .card_item:hover, .detail_lst li:hover {
            background-color: var(--wt-bg-elev2) !important;
        }

        /* /canvas "Weekly HOT" and "Popular By Category" grids. Each card is
           <a class="discover_item"> inside <li>; base CSS paints the link with
           a white background that broke through under the dark theme. */
        .discover_lst li, .discover_lst .discover_item,
        a.discover_item {
            background: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
            border-radius: 8px !important;
            overflow: hidden !important;
            border-color: var(--wt-border) !important;
        }
        a.discover_item:hover {
            background: var(--wt-bg-elev2) !important;
        }
        a.discover_item:hover .subj,
        a.discover_item:hover .title,
        .discover_lst li:hover .subj,
        .discover_lst li:hover .title { color: var(--wt-accent) !important; }

        /* Hover darken: when the cursor lands on a card thumbnail, dim the
           artwork so the title/genre overlay (which sits on top of the image
           as the card's only label) becomes readable. Scoped strictly to card
           containers to avoid touching viewer panel images, which must stay
           pristine. transition lives on the img so the dim eases in/out. */
        .card_lst li img, .card_item img, .detail_lst li img,
        .discover_lst li img, a.discover_item img,
        .daily_lst li img, ._dailyList li img, ._popularList li img,
        .webtoon_list li img, .webtoon_list_wrap li img,
        .main_section .card_item img,
        .discover_spot li img, .spot_lst li img {
            transition: filter .2s ease !important;
        }
        .card_lst li:hover img, .card_item:hover img, .detail_lst li:hover img,
        .discover_lst li:hover img, a.discover_item:hover img,
        .daily_lst li:hover img, ._dailyList li:hover img, ._popularList li:hover img,
        .webtoon_list li:hover img, .webtoon_list_wrap li:hover img,
        .main_section .card_item:hover img,
        .discover_spot li:hover img, .spot_lst li:hover img {
            filter: brightness(.7) !important;
        }
        /* Hover darken extended to: viewer bottom episode strip + viewer sidebar
           "Trending & Popular" / "Top Originals" rankings. */
        .episode_lst li .thmb img, .episode_lst li img,
        .ranking_lst li img, .aside .ranking_lst li img {
            transition: filter .2s ease !important;
        }
        .episode_lst li:hover .thmb img, .episode_lst li:hover img,
        .ranking_lst li:hover img, .aside .ranking_lst li:hover img {
            filter: brightness(.7) !important;
        }

        .discover_lst .info { background: transparent !important; }
        .discover_lst .subj { color: var(--wt-text) !important; }
        .discover_lst .grade_num { color: var(--wt-text-dim) !important; }
        .discover_lst .grade_area { color: var(--wt-text-dim) !important; }

        /* /canvas genre-filtered pages (DRAMA, FANTASY, …) use a different DOM:
           .challenge_cont_area > .challenge_lst > ul > li > a.challenge_item
           instead of .discover_lst / .discover_item. Without parallel rules the
           cards render as raw thumbnails on the page background — no card
           container, no border, no rounded corners. Mirror the .discover_item
           treatment so /canvas/{genre} matches /canvas home visually. */
        a.challenge_item {
            background: transparent !important;
            color: var(--wt-text) !important;
            border-radius: 6px !important;
            overflow: visible !important;
            border: 0 !important;
            box-shadow: none !important;
            display: block !important;
            /* Flat tile — parent .challenge_cont_area provides elevation.
               overflow: visible so the rounded image (clipped on the img/thmb
               itself, not the anchor) doesn't fight a square clip box. */
            transition: color .15s ease !important;
        }
        a.challenge_item:hover { background: transparent !important; }
        .challenge_item img { transition: filter .2s ease !important; }
        a.challenge_item:hover img { filter: brightness(.7) !important; }
        a.challenge_item:hover .subj,
        a.challenge_item:hover .title { color: var(--wt-accent) !important; }
        /* Kill background/border on the parent <li> that 'Cards / lists'
           rule painted — same "flat tile" reasoning. */
        .challenge_lst li,
        .challenge_cont_area .challenge_lst li {
            background: transparent !important;
            border: 0 !important;
            box-shadow: none !important;
        }
        .challenge_item .info,
        .challenge_item .info_area,
        .challenge_item .area_genre { background: transparent !important; }
        .challenge_item .subj,
        .challenge_item .title { color: var(--wt-text) !important; }
        .challenge_item .area_genre,
        .challenge_item .genre,
        .challenge_item .author { color: var(--wt-text) !important; }
        /* Like / subscriber counter on canvas cards — match the green heart
           icon so number + icon read as one element. */
        .challenge_item .grade_num,
        .challenge_item .num,
        .challenge_item .like_area .num,
        .challenge_item .like_area,
        .challenge_item .subscribe,
        .challenge_item .subscribe_count { color: var(--wt-accent) !important; }
        /* Wrap the whole canvas grid in one elevated section card to match
           how other listing pages group their content. box-sizing:border-box
           is critical — without it, the 24 px padding adds outside the
           computed width and pushes the .aside.challenge float (Top CANVAS
           / Up & Coming) down below the grid instead of beside it. */
        .challenge_cont_area {
            background: var(--wt-bg-elev) !important;
            border: 1px solid var(--wt-border) !important;
            border-radius: 12px !important;
            padding: 16px 20px !important;
            box-sizing: border-box !important;
            overflow: visible !important;
            box-shadow: var(--wt-shadow) !important;
        }
        /* Ensure no ancestor clips the rightmost card's 1 px border. */
        .challenge_cont_area .challenge_lst,
        .challenge_cont_area .challenge_lst > ul { overflow: visible !important; }
        .challenge_lst,
        .challenge_lst > ul,
        .challenge_lst > ul > li { background: transparent !important; }
        /* Grid layout with explicit gap — the base CSS lays cards out with
           no breathing room, leaving them edge-to-edge. Fixed 4 columns with
           minmax(0, 1fr) so a card's intrinsic min-width (cover image) can't
           force the column wider than the available track, which would
           otherwise push the rightmost card out of view or drop the row. */
        .challenge_lst > ul {
            display: grid !important;
            grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
            gap: 14px !important;
            padding: 0 !important;
            margin: 0 !important;
        }
        .challenge_lst > ul > li {
            width: auto !important;
            min-width: 0 !important;
            margin: 0 !important;
            padding: 0 !important;
            float: none !important;
        }
        a.challenge_item img,
        .challenge_item .thmb,
        .challenge_item .thmb img {
            width: 100% !important;
            max-width: 100% !important;
            height: auto !important;
            display: block !important;
            /* Soft rounding on all four corners of the thumbnail itself so
               the card image reads as a discrete tile (rather than a
               half-rounded slab that meets a square text strip below). */
            border-radius: 6px !important;
            overflow: hidden !important;
        }
        a.challenge_item { min-width: 0 !important; max-width: 100% !important; }
        /* Base CSS draws a 1 px light separator at the bottom of multiple
           levels in the challenge_* tree. Cast a wider net to kill it. */
        .challenge_cont_area,
        .challenge_cont_area *,
        .challenge_lst,
        .challenge_lst > ul,
        .challenge_lst > ul > li {
            border-bottom: 0 !important;
        }
        .challenge_cont_area::after,
        .challenge_lst::after,
        .challenge_lst > ul::after {
            display: none !important;
            content: none !important;
            border: 0 !important;
        }

        /* Right-rail sidebar on /canvas (Top CANVAS, Up & Coming). Each is
           a separate .lst_area inside .aside.challenge .ranking_lst.viewer
           (a flex column). Card each .lst_area individually so the two
           sections render as two distinct stacked cards, not one big block. */
        /* Shrink the floated sidebar slightly so the grid section gets more
           horizontal room (the rightmost card column was being shaved). */
        .aside.challenge { width: 280px !important; }
        /* Card-style outer wrapper for Top CANVAS (#challengeGenreRanking)
           and Up & Coming (#upcomingChallengeRanking). Match background and
           padding across both so they read as a matched pair. Extra
           padding-bottom guarantees the last ranking-row tile's bottom
           border has clearance from the panel edge. */
        .aside.challenge .lst_area,
        #challengeGenreRanking,
        #upcomingChallengeRanking,
        .ranking_lst.viewer > .lst_area,
        .ranking_lst.viewer .lst_area {
            background: var(--wt-bg-elev2) !important;
            background-color: var(--wt-bg-elev2) !important;
            border: 1px solid rgba(255,255,255,.4) !important;
            border-radius: 12px !important;
            padding: 14px !important;
            box-sizing: border-box !important;
            box-shadow:
                inset 0 0 0 1px rgba(255,255,255,.08),
                0 4px 12px rgba(0,0,0,.55),
                0 12px 28px rgba(0,0,0,.45) !important;
            margin: 0 !important;
        }
        /* Flat ranking rows inside Top CANVAS / Up & Coming — no per-row
           border or background ("volume"). Hover only turns the title green. */
        .aside.challenge .lst_type1 > li,
        #challengeGenreRanking .lst_type1 > li,
        #upcomingChallengeRanking .lst_type1 > li,
        .ranking_lst.viewer .lst_type1 > li {
            border: 0 !important;
            background: transparent !important;
            padding: 6px 0 !important;
            margin: 0 !important;
            transition: color .15s ease !important;
        }
        .aside.challenge .lst_type1 > li:hover,
        #challengeGenreRanking .lst_type1 > li:hover,
        #upcomingChallengeRanking .lst_type1 > li:hover,
        .ranking_lst.viewer .lst_type1 > li:hover {
            background: transparent !important;
        }
        .aside.challenge .lst_type1 > li:hover .subj,
        #challengeGenreRanking .lst_type1 > li:hover .subj,
        #upcomingChallengeRanking .lst_type1 > li:hover .subj,
        .ranking_lst.viewer .lst_type1 > li:hover .subj {
            color: var(--wt-accent) !important;
        }
        /* Header arrow (.ico_arr1) next to "Top CANVAS" / "Up & Coming" —
           force inline-flex alignment so the chevron sits beside the title
           text, not on the line below it. Base CSS often gives ico_arr1
           display:block or width:14px (sprite). */
        #challengeGenreRanking .ico_arr1,
        #upcomingChallengeRanking .ico_arr1,
        .ranking_lst.viewer .title_area .ico_arr1,
        .aside.challenge .title_area .ico_arr1,
        .title_area h2 .ico_arr1 {
            background-image: none !important;
            background: none !important;
            color: var(--wt-text-dim) !important;
            font-size: 18px !important;
            line-height: 1 !important;
            font-style: normal !important;
            font-weight: 400 !important;
            text-indent: 0 !important;
            overflow: visible !important;
            white-space: nowrap !important;
            filter: none !important;
            width: auto !important;
            height: auto !important;
            min-width: 0 !important;
            display: inline-flex !important;
            align-items: center !important;
            margin: 0 0 0 6px !important;
            padding: 0 !important;
            vertical-align: middle !important;
            position: static !important;
            top: auto !important;
        }
        /* Make sure the parent h2 lays children out inline. */
        #challengeGenreRanking .title_area h2,
        #upcomingChallengeRanking .title_area h2,
        .aside.challenge .title_area h2 {
            display: inline-flex !important;
            align-items: center !important;
            gap: 0 !important;
        }
        #challengeGenreRanking .title_area h2 span,
        #upcomingChallengeRanking .title_area h2 span,
        .aside.challenge .title_area h2 span {
            display: inline !important;
        }
        /* Tight stacking: kill the flex column gap entirely and zero any
           top margin on the second .lst_area so Up & Coming sits directly
           under Top CANVAS with only a hairline gap. */
        .aside.challenge .ranking_lst {
            gap: 0 !important;
            row-gap: 0 !important;
        }
        .aside.challenge .lst_area + .lst_area { margin-top: 8px !important; }
        .aside.challenge .lst_type1 { border-bottom: none !important; }
        .aside.challenge h2, .aside.challenge h3,
        .aside.challenge .title_area { color: var(--wt-text) !important; }
        /* Subscriber-count badge overlaid on each carousel card thumbnail.
           DOM: <span class="badge_discover num">7K</span> inside .discover_badge_area */
        .badge_discover {
            background: var(--wt-bg-elev2) !important;
            color: var(--wt-accent) !important;
            border: 1px solid rgba(0,213,100,.35) !important;
            border-radius: 9999px !important;
        }

        /* Sections — #content and #container are the actual IDs in the DOM.
           #wrap wraps the entire page including header so excluded here —
           html/body already covers the page background.
           .detail_header is intentionally excluded: it is 1200px centered and
           sits ON TOP of the full-width .detail_bg artwork element. Setting its
           background-color to dark would paint over the artwork in the center
           while leaving the artwork visible on the sides — the opposite of
           what we want. .detail_header background defaults to transparent, which
           lets the .detail_bg artwork show through in the header area. */
        #content, #container, .cont_area, .cont_box, .detail_body {
            background-color: var(--wt-bg) !important;
            color: var(--wt-text) !important;
        }
        /* detail_body children are floated, so detail_body collapses to height 0
           and its background paints nothing — artwork bleeds through card corners.
           display:flow-root forces detail_body to contain its floats (proper height).
           overflow:hidden clips the artwork at the rounded boundary.
           background:--wt-bg matches the page so the container is invisible below
           the shorter sidebar card, and card corners show the page colour naturally. */
        .detail_body {
            display: flow-root !important;
            overflow: hidden !important;
            border-radius: 16px !important;
            background: var(--wt-bg) !important;
            padding-top: 0 !important;
        }
        .detail_header { color: var(--wt-text) !important; }

        /* Popups / modals */
        .layer_popup, ._popupLayer, .layer_box, .pop_layer,
        .modal, .dialog, .tooltip, .balloon, .ly_box {
            background-color: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
            box-shadow: 0 8px 24px rgba(0,0,0,.5) !important;
        }

        /* Tone down the top marketing banner */
        .header_bn {
            background: var(--wt-bg-elev) !important;
            filter: brightness(.85);
        }

        /* Promotional banner on /canvas ("Try the New CANVAS Creator Dashboard!").
           Ships as <a class="contest_banner" style="background-color: #bdffdb">
           with an inline mint-green PNG inside. Override the inline style with
           !important and tone the embedded image down so it blends with dark. */
        /* Promotional banners on /canvas. The <a class="contest_banner"> ships
           with inline style="background-color: #bdffdb" (mint green) and an
           <img> child whose pixels are also mint green. We override the inline
           bg and clip the image so its mint-green padding doesn't bleed past
           the dark anchor — image stretches to fill the full anchor width
           via object-fit, with the mint-green centre portion heavily
           desaturated so it blends with the dark page. */
        .contest_banner, a.contest_banner {
            background-color: var(--wt-bg-elev) !important;
            border-radius: 0 !important;
            overflow: hidden !important;
            display: block !important;
            position: relative !important;
        }
        .contest_banner img {
            display: block !important;
            margin: 0 auto !important;
            filter: brightness(.32) saturate(.35) contrast(1.05) !important;
            transition: filter .2s !important;
        }
        .contest_banner:hover img {
            filter: brightness(.5) saturate(.55) contrast(1.05) !important;
        }

        /* Sub-nav (snb): day-of-week picker AND genre tabs share this component */
        .snb_wrap, .snb_inner, .snb {
            background-color: var(--wt-bg) !important;
            border-color: var(--wt-border) !important;
        }
        /* Force the bottom underline color so the strip reads continuously
           on dark — base CSS uses .5px solid #e0e0e0 which is invisible.
           padding-bottom:0 removes the height gap between snb_inner and
           snb_wrap. border-bottom alone gets covered by snb_inner
           (position:relative paints above its parent's border), so we draw
           the line via ::after with z-index:10 to paint over snb_inner. */
        .snb_wrap, .snb_wrap.type_sub {
            padding-bottom: 0 !important;
            position: relative !important;
            border-bottom: none !important;
            z-index: 10000 !important;
        }
        .snb_wrap::after {
            content: '' !important;
            position: absolute !important;
            bottom: 0 !important;
            left: 0 !important;
            right: 0 !important;
            height: 1px !important;
            background-color: var(--wt-border) !important;
            z-index: 10 !important;
            pointer-events: none !important;
        }
        /* Snb scroll arrow buttons (← / → on long tab strips, e.g. /canvas
           genre row that runs past HORROR). Base CSS draws the arrow as a
           dark sprite (background-image) on a #fff button — invisible on
           dark. Strip the sprite and draw a white unicode chevron via a
           pseudo-element so the glyph is always readable. */
        .snb_inner .btn_snb_prev, .snb_inner .btn_snb_next {
            background-color: var(--wt-bg-elev) !important;
            background-image: none !important;
            border-bottom: 1px solid var(--wt-border) !important;
            border-right-color: var(--wt-border) !important;
            border-left-color: var(--wt-border) !important;
            box-shadow: inset 0 0 0 1px rgba(255,255,255,.07) !important;
        }
        /* Suppress the base sprite that's drawn via ::before — without this we
           render both the dark sprite AND our white chevron (doubled arrow). */
        .snb_inner .btn_snb_prev::before, .snb_inner .btn_snb_next::before {
            content: none !important;
            background-image: none !important;
            background: none !important;
        }
        /* Heavy guillemets (❮ ❯) at large size for clear readability. */
        .snb_inner .btn_snb_prev::after, .snb_inner .btn_snb_next::after {
            position: absolute !important;
            inset: 0 !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            color: var(--wt-text) !important;
            font-size: 40px !important;
            line-height: 1 !important;
            font-weight: 300 !important;
            pointer-events: none !important;
        }
        .snb_inner .btn_snb_prev::after { content: '\\276E' !important; }
        .snb_inner .btn_snb_next::after { content: '\\276F' !important; }
        .snb_inner .btn_snb_prev:hover, .snb_inner .btn_snb_next:hover {
            background-color: var(--wt-bg-hover) !important;
        }
        .snb_inner .btn_snb_prev:hover::after,
        .snb_inner .btn_snb_next:hover::after {
            color: var(--wt-accent) !important;
        }
        .snb_item, .snb_tab, ._snb_tab_a {
            background-color: transparent !important;
            color: var(--wt-text) !important;
            border-color: var(--wt-border) !important;
        }
        .snb_item:hover .snb_tab, .snb_tab:hover {
            color: var(--wt-accent-soft) !important;
            background-color: var(--wt-bg-elev2) !important;
        }
        .snb_item.is_selected .snb_tab,
        .snb_tab[aria-current="true"],
        .snb_tab[aria-current="page"] {
            color: var(--wt-accent) !important;
            transform: none !important;
        }
        .btn_snb_prev, .btn_snb_next {
            background-color: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
        }

        /* === Homepage / listing pages: Option B elevation design ===
           Three-level hierarchy: page (--wt-bg) → section card (--wt-bg-elev)
           → comic card (--wt-bg-elev2 + shadow). */

        /* Section containers become elevated cards with rounded corners.
           .main_section = "Trending & Popular Series" carousel block.
           .webtoon_list_wrap = "Popular Series by Category", "Newly Released", etc. */
        .main_section, .webtoon_list_wrap {
            background: var(--wt-bg-elev) !important;
            border-radius: 16px !important;
            padding: 24px 24px 28px !important;
            box-shadow: 0 1px 0 rgba(255,255,255,.05), 0 8px 32px rgba(0,0,0,.35) !important;
            color: var(--wt-text) !important;
        }
        /* Faint accent line below each section header to anchor the title. */
        .main_section .section_header,
        .webtoon_list_wrap .section_header {
            border-bottom: 1px solid rgba(0, 213, 100, .18) !important;
            margin-bottom: 16px !important;
        }
        /* "View all ›" link in section header. */
        .section_header .button_view_all { color: var(--wt-text-dim) !important; }
        .section_header .button_view_all:hover { color: var(--wt-accent-soft) !important; }
        /* "more ›" link — two element variants render this:
           - .button_view_all (homepage sections) — chevron via ::after sprite
           - a.lk_more (canvas / popular-by-category) — chevron via a
             child <span class="ico_arr"> sprite
           Both inherit text colour from our a/h rules, but the chevron sprite
           ships as a dark glyph. Strip the sprite and either inject our own
           text chevron (::after) or invert the inline span. */
        a.lk_more, .lk_more {
            color: var(--wt-text-dim) !important;
        }
        a.lk_more:hover, .lk_more:hover {
            color: var(--wt-accent-soft) !important;
        }
        .lk_more .ico_arr {
            background-image: none !important;
            background: none !important;
            width: auto !important;
            height: auto !important;
            text-indent: 0 !important;
            overflow: visible !important;
            white-space: normal !important;
            display: inline-block !important;
            vertical-align: middle !important;
            margin-left: 4px !important;
        }
        .lk_more .ico_arr::after {
            content: '\\203A' !important;
            color: inherit !important;
            font-size: 18px !important;
            line-height: 1 !important;
        }
        .button_view_all::after,
        .section_header .button_view_all::after {
            background-image: none !important;
            background: none !important;
            content: '\\203A' !important;
            color: inherit !important;
            font-size: 18px !important;
            line-height: 1 !important;
            width: auto !important;
            height: auto !important;
            margin-left: 4px !important;
            text-indent: 0 !important;
            overflow: visible !important;
            white-space: normal !important;
        }

        /* Comic cards within section containers: flat tiles. The parent
           section already provides elevation, so per-card shadow + lift
           created a "double volume" effect. Hover = image darken (handled by
           the generic .card_item img rule above) + title turns accent green. */
        .main_section .card_item,
        .main_section .card_lst li,
        .main_section .webtoon_list li,
        .webtoon_list_wrap .card_item,
        .webtoon_list_wrap .card_lst li,
        .webtoon_list_wrap ._popularList li,
        .webtoon_list_wrap ._dailyList li,
        .webtoon_list_wrap .webtoon_list li {
            background: transparent !important;
            border-color: transparent !important;
            border-radius: 10px !important;
            box-shadow: none !important;
            transform: none !important;
            transition: color .15s ease !important;
        }
        .main_section .card_item:hover .title,
        .main_section .card_item:hover .subj,
        .main_section .card_lst li:hover .title,
        .main_section .card_lst li:hover .subj,
        .main_section .webtoon_list li:hover .title,
        .main_section .webtoon_list li:hover .subj,
        .webtoon_list_wrap .card_item:hover .title,
        .webtoon_list_wrap .card_item:hover .subj,
        .webtoon_list_wrap .card_lst li:hover .title,
        .webtoon_list_wrap .card_lst li:hover .subj,
        .webtoon_list_wrap ._popularList li:hover .title,
        .webtoon_list_wrap ._popularList li:hover .subj,
        .webtoon_list_wrap ._dailyList li:hover .title,
        .webtoon_list_wrap ._dailyList li:hover .subj,
        .webtoon_list_wrap .webtoon_list li:hover .title,
        .webtoon_list_wrap .webtoon_list li:hover .subj {
            color: var(--wt-accent) !important;
        }
        /* Card text area: title sits on its own row; genre + like/view sit
           on a single row beneath. flex-wrap allows fallback to two rows on
           very narrow cards. Removing the previous .view_count margin-top is
           what brings the count onto the same line as .genre. */
        .webtoon_list .info_text {
            margin-top: 10px !important;
            padding: 0 4px !important;
            display: flex !important;
            flex-direction: column !important;
            gap: 2px !important;
        }
        .webtoon_list .info_text .title {
            color: var(--wt-text) !important;
        }
        .webtoon_list .info_text > .genre,
        .webtoon_list .info_text > .view_count,
        .webtoon_list .info_text > .like_count,
        .webtoon_list .info_text > .count_like {
            display: inline-block !important;
            margin: 0 !important;
        }
        /* Heart / view counter inherits the brand-green heart-icon colour so
           number and icon read as one element. */
        .webtoon_list .view_count,
        .webtoon_list .like_count,
        .webtoon_list .count_like {
            color: var(--wt-accent) !important;
        }
        /* Pair genre with the heart/view stat on one row. Wraps both children
           in a flex row when both exist. Uses :has() so cards with only one
           child fall back gracefully. */
        .webtoon_list .info_text:has(.genre + .view_count),
        .webtoon_list .info_text:has(.genre + .like_count),
        .webtoon_list .info_text:has(.genre + .count_like) {
            display: grid !important;
            grid-template-columns: 1fr auto !important;
            grid-template-areas:
                "title title"
                "genre stat" !important;
            column-gap: 8px !important;
        }
        .webtoon_list .info_text > .title { grid-area: title !important; }
        .webtoon_list .info_text > .genre { grid-area: genre !important; }
        .webtoon_list .info_text > .view_count,
        .webtoon_list .info_text > .like_count,
        .webtoon_list .info_text > .count_like { grid-area: stat !important; justify-self: end !important; }
        /* Green hover on the heart/view number. */
        .main_section .card_item:hover .view_count,
        .main_section .card_item:hover .like_count,
        .main_section .card_item:hover .count_like,
        .webtoon_list_wrap li:hover .view_count,
        .webtoon_list_wrap li:hover .like_count,
        .webtoon_list_wrap li:hover .count_like,
        .webtoon_list li:hover .view_count,
        .webtoon_list li:hover .like_count,
        .webtoon_list li:hover .count_like {
            color: var(--wt-accent) !important;
        }

        /* Section header text. */
        .section_header { color: var(--wt-text) !important; }
        .series_count, .series_count .number, .series_count span {
            color: var(--wt-text-dim) !important;
        }
        .sort_area, .sort_by_area {
            background-color: transparent !important;
        }
        .sort_by, ._sort_by_a {
            color: var(--wt-text-dim) !important;
            background-color: transparent !important;
        }
        .sort_by[aria-current="true"], ._sort_by_a[aria-current="true"] {
            color: var(--wt-text) !important;
        }

        /* /canvas sort dropdown ("Sort by Date ▾"). Trigger is .sort_area .checked,
           panel is .sort_box. Base ships a white panel with a sprite checkmark
           — both invisible on dark. Render the trigger as a soft pill with a
           subtle chevron, and the panel as an elevated card with hover rows. */
        /* Trigger pill — best-practice sort/filter button:
           - Clear affordance: icon prefix + label + caret suffix
           - Distinct open state via [aria-expanded="true"]
           - Generous touch target, focus-visible safe (--wt-border-strong) */
        .sort_area .checked {
            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;
            text-align: center !important;
            position: relative !important;
            background: var(--wt-bg-elev2) !important;
            color: var(--wt-text) !important;
            border: 1px solid rgba(255,255,255,.22) !important;
            border-radius: 999px !important;
            padding: 8px 32px 8px 30px !important;
            font-size: 13px !important;
            font-weight: 600 !important;
            line-height: 16px !important;
            white-space: nowrap !important;
            cursor: pointer !important;
            box-shadow: 0 1px 0 rgba(255,255,255,.04) !important;
            transition: background-color .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease !important;
        }
        /* Sort-icon glyph (⇅) prefix — communicates the button's purpose. */
        .sort_area .checked::before {
            content: '\\21F5' !important;
            position: absolute !important;
            left: 12px !important;
            top: 50% !important;
            transform: translateY(-50%) !important;
            font-size: 13px !important;
            color: var(--wt-text-dim) !important;
            line-height: 1 !important;
            transition: color .15s ease !important;
        }
        .sort_area .checked:hover {
            background: var(--wt-bg-hover) !important;
            border-color: var(--wt-accent) !important;
            color: var(--wt-accent) !important;
            box-shadow: 0 0 0 3px rgba(0,213,100,.12) !important;
        }
        .sort_area .checked:hover::before { color: var(--wt-accent) !important; }
        /* Open state — solid accent border ring + brighter bg so it reads
           clearly as "active/pressed". Mirrors common dropdown patterns. */
        .sort_area .checked[aria-expanded="true"] {
            background: var(--wt-bg-hover) !important;
            border-color: var(--wt-accent) !important;
            color: var(--wt-accent) !important;
            box-shadow: 0 0 0 3px rgba(0,213,100,.18) !important;
        }
        .sort_area .checked[aria-expanded="true"]::before { color: var(--wt-accent) !important; }
        /* Outer page sort (.sort_area._sorting) on /canvas/list — lift it
           ABOVE the cards section block entirely using negative top so the
           pill sits as a free-standing control, not overlapping the grid.
           The section card has 16px+ padding so position:absolute relative
           to the section card with top:-48px clears the top edge. */
        .challenge_cont_area { position: relative !important; }
        .sort_area._sorting {
            position: absolute !important;
            right: 4px !important;
            top: -52px !important;
            left: auto !important;
            margin: 0 !important;
            display: inline-block !important;
            float: none !important;
            z-index: 50 !important;
        }
        .sort_area._sorting .checked {
            padding: 11px 32px !important;
            font-size: 15px !important;
            letter-spacing: .02em !important;
            min-width: 160px !important;
        }
        .sort_area._sorting .sort_box {
            right: 0 !important;
            top: calc(100% + 6px) !important;
            min-width: 200px !important;
            z-index: 200 !important;
        }
        /* Inner Top-CANVAS filter (.sort_area._filterArea) — shares the
           .title_area row with the "Top CANVAS" h2. Keep it compact so long
           labels (HEARTWARMING, SUPERNATURAL) don't push the row. */
        .sort_area._filterArea {
            position: relative !important;
            top: auto !important;
            right: auto !important;
            margin-left: auto !important;
            flex-shrink: 0 !important;
            z-index: 5 !important;
        }
        .sort_area._filterArea .checked {
            padding: 5px 14px !important;
            font-size: 11px !important;
            letter-spacing: .04em !important;
            min-width: 90px !important;
            max-width: 140px !important;
            text-overflow: ellipsis !important;
            overflow: hidden !important;
        }
        /* Compact filter pill: no leading ⇅ icon, no trailing caret — the
           label alone is sufficient in this context, and removing both
           glyphs frees space for long category names. */
        .sort_area._filterArea .checked::before { content: none !important; display: none !important; }
        .sort_area._filterArea .checked .ico_chk,
        .sort_area._filterArea .checked .ico_chk::after { display: none !important; content: none !important; }
        .sort_area._filterArea .sort_box._filterLayer {
            right: 0 !important;
            top: calc(100% + 4px) !important;
            min-width: 160px !important;
            max-height: 280px !important;
            overflow-y: auto !important;
            /* High z-index so the panel paints above the ranking-number
               sprites (.ico_nN) AND the body::before vignette gradient
               (z-index 9999). 10001 clears both unconditionally. */
            z-index: 10001 !important;
            background: var(--wt-bg-elev) !important;
            background-color: var(--wt-bg-elev) !important;
            isolation: isolate !important;
        }
        /* Lift the filter pill's stacking context above sibling card rows
           so the panel it spawns isn't trapped beneath them. */
        .sort_area._filterArea { z-index: 10000 !important; }
        /* Make the .title_area inside the Top CANVAS card a flex row so the
           h2 and the filter pill align horizontally without one pushing
           the other. */
        .aside.challenge .lst_area .title_area {
            display: flex !important;
            align-items: center !important;
            justify-content: space-between !important;
            gap: 8px !important;
        }
        .aside.challenge .lst_area .title_area h2 {
            flex: 1 1 auto !important;
            min-width: 0 !important;
        }
        /* Replace the sprite check-mark icon next to the trigger with a
           caret-down so users see it's a dropdown. */
        .sort_area .checked .ico_chk {
            background: none !important;
            background-image: none !important;
            width: auto !important;
            height: auto !important;
            position: absolute !important;
            top: 50% !important;
            right: 10px !important;
            transform: translateY(-50%) !important;
            line-height: 1 !important;
            pointer-events: none !important;
        }
        .sort_area .checked .ico_chk::after {
            content: '\\25BE' !important;
            color: var(--wt-text-dim) !important;
            font-size: 11px !important;
            display: block !important;
            transition: transform .15s ease, color .15s ease !important;
        }
        /* Caret rotates 180° when the panel is open — visual confirmation
           that the menu is expanded. */
        .sort_area .checked[aria-expanded="true"] .ico_chk::after {
            transform: rotate(180deg) !important;
            color: var(--wt-accent) !important;
        }
        .sort_area .checked:hover .ico_chk::after { color: var(--wt-accent) !important; }
        /* Dropdown panel — elevated card with smooth open animation and
           ≥40 px hit targets on each option for accessibility. */
        .sort_box {
            background: var(--wt-bg-elev) !important;
            border: 1px solid var(--wt-border-strong) !important;
            border-radius: 10px !important;
            padding: 6px !important;
            box-shadow:
                0 12px 32px rgba(0,0,0,.6),
                0 4px 8px rgba(0,0,0,.4) !important;
            overflow: hidden !important;
            transform-origin: top right !important;
            animation: wt-sortbox-in .14s ease-out !important;
        }
        @keyframes wt-sortbox-in {
            from { opacity: 0; transform: translateY(-4px) scale(.98); }
            to   { opacity: 1; transform: translateY(0) scale(1); }
        }
        .sort_box li {
            height: auto !important;
            padding: 0 !important;
            text-align: left !important;
            background: transparent !important;
            border-bottom: 0 !important;
        }
        .sort_box a {
            display: flex !important;
            align-items: center !important;
            justify-content: space-between !important;
            color: var(--wt-text-dim) !important;
            padding: 10px 14px !important;
            border-radius: 6px !important;
            font-size: 13px !important;
            font-weight: 500 !important;
            line-height: 1.2 !important;
            min-height: 22px !important;
            height: auto !important;
            transition: background-color .12s ease, color .12s ease, padding .12s ease !important;
        }
        .sort_box a:hover {
            background: var(--wt-bg-elev2) !important;
            color: var(--wt-text) !important;
            padding-left: 18px !important;
        }
        .sort_box .ico_chk {
            background: none !important;
            background-image: none !important;
            width: auto !important;
            height: auto !important;
            position: static !important;
            margin-left: 8px !important;
        }
        /* Active option — accent text + soft-tinted background tile so it
           reads at a glance, not just by colour difference. */
        .sort_box .on a,
        .sort_box [aria-current="true"],
        .sort_box li.on a,
        .sort_box li a[aria-current="true"] {
            color: var(--wt-accent) !important;
            background: rgba(0,213,100,.12) !important;
            font-weight: 700 !important;
        }
        .sort_box .on a:hover,
        .sort_box li a[aria-current="true"]:hover {
            background: rgba(0,213,100,.18) !important;
            padding-left: 14px !important;
        }
        .sort_box .on .ico_chk::after,
        .sort_box [aria-current="true"] .ico_chk::after,
        .sort_box li a[aria-current="true"] .ico_chk::after {
            content: '\\2713' !important;
            color: var(--wt-accent) !important;
            font-size: 13px !important;
            font-weight: 700 !important;
        }

        /* Genre labels (.genre.g_romance, .g_fantasy, …) — Webtoons' light
           theme tints these per category. Reproduce on dark, lightening the
           originally-dark hues (navy / brown / charcoal) so they stay legible. */
        .genre.g_romance, .g_romance { color: #fd337f !important; }
        .genre.g_romance_m, .g_romance_m { color: #fd337f !important; }
        .genre.g_comedy, .g_comedy { color: #ffc233 !important; }
        .genre.g_fantasy, .g_fantasy { color: #b56af2 !important; }
        .genre.g_romantic_fantasy, .g_romantic_fantasy { color: #e155f4 !important; }
        .genre.g_action, .g_action { color: #4a91ff !important; }
        .genre.g_drama, .g_drama { color: #2dd4be !important; }
        .genre.g_slice_of_life, .g_slice_of_life { color: #b8d62e !important; }
        .genre.g_supernatural, .g_supernatural { color: #9b6df2 !important; }
        .genre.g_horror, .g_horror { color: #e84545 !important; }
        .genre.g_thriller, .g_thriller { color: #e23a72 !important; }
        .genre.g_sports, .g_sports { color: #4ec4f5 !important; }
        .genre.g_sf, .g_sf { color: #8da8ce !important; }
        .genre.g_historical, .g_historical { color: #c0926a !important; }
        .genre.g_heartwarming, .g_heartwarming { color: #ff8a3d !important; }
        .genre.g_super_hero, .g_super_hero { color: #8a6fff !important; }
        .genre.g_tiptoon, .g_tiptoon { color: #ff8fd9 !important; }
        .genre.g_short_story, .g_short_story { color: #7fb2ff !important; }
        .genre.g_web_novel, .g_web_novel { color: #5fb6e0 !important; }
        .genre.g_mystery, .g_mystery { color: #a5a8c8 !important; }
        .genre.g_bl_gl, .g_bl_gl { color: #ee82ff !important; }
        .genre.g_western_palace, .g_western_palace { color: #e155f4 !important; }
        .genre.g_eastern_palace, .g_eastern_palace { color: #c0926a !important; }
        .genre.g_time_slip, .g_time_slip { color: #9085ff !important; }
        .genre.g_city_office, .g_city_office { color: #8a85e0 !important; }
        .genre.g_adaptation, .g_adaptation { color: #2ee672 !important; }
        .genre.g_school, .g_school { color: #f0a064 !important; }
        .genre.g_local, .g_local { color: #25ef92 !important; }
        .genre.g_shonen, .g_shonen { color: #6a98e0 !important; }
        .genre.g_martial_arts, .g_martial_arts { color: #c08555 !important; }
        .genre.g_graphic_novel, .g_graphic_novel { color: #7a83e8 !important; }
        .genre.g_others, .g_others { color: #9ea3ab !important; }
        .genre.g_informative { color: #7fc6a0 !important; }
        .genre.g_lgbtq { color: #ff7ed0 !important; }

        /* Notice strip above the footer (shown conditionally) */
        .notice_area, #noticeArea {
            background-color: var(--wt-bg-elev) !important;
            color: var(--wt-text-dim) !important;
            border-top: 1px solid var(--wt-border) !important;
            border-bottom: 1px solid var(--wt-border) !important;
        }
        .notice_area a, #noticeArea a { color: var(--wt-text) !important; }

        /* "Download WEBTOON now!" app-download strip in the footer */
        .foot_app, .foot_cont, .foot_down_msg, .footapp_icon_cont {
            background-color: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
            border-color: var(--wt-border) !important;
        }
        .foot_app .txt, .foot_down_msg .txt, .foot_app p { color: var(--wt-text) !important; }
        /* QR code stays on a white tile so it remains scannable */
        .ico_qrcode {
            background-color: #fff !important;
            padding: 4px;
            border-radius: 4px;
        }

        /* Buttons */
        button, .btn, .btn_area button, input[type="button"], input[type="submit"] {
            background-color: var(--wt-bg-elev2) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
        }
        button:hover, .btn:hover { background-color: var(--wt-bg-hover) !important; }
        .btn_subscribe, .btn_main, ._btnSubscribe {
            background-color: var(--wt-accent) !important;
            color: var(--wt-text-on-accent) !important;
            border-color: var(--wt-accent) !important;
        }

        /* Carousel prev/next buttons — base CSS sets background:#fff so the dark
           SVG arrow sprite is readable. Our generic button rule overrides to
           --wt-bg-elev2, making the dark arrow invisible. Restore a visible
           surface and invert the :before arrow glyph to white. */
        .carousel_wrap .carousel_paging .next,
        .carousel_wrap .carousel_paging .prev {
            background: rgba(15,17,20,.88) !important;
            border: 1px solid rgba(255,255,255,.3) !important;
            box-shadow: 0 2px 10px rgba(0,0,0,.7) !important;
        }
        .carousel_wrap .carousel_paging .next:hover,
        .carousel_wrap .carousel_paging .prev:hover {
            background: rgba(40,45,52,.95) !important;
            border-color: rgba(255,255,255,.5) !important;
        }
        .carousel_wrap .carousel_paging .next:before,
        .carousel_wrap .carousel_paging .prev:before {
            filter: brightness(0) invert(1) opacity(.9) !important;
        }
        /* /canvas RECOMMENDED SERIES carousel paging arrows + dot indicators.
           Buttons (.btn_prev / .btn_next inside .discover_spot .paging) had no
           visible glyph after our generic button rule painted them with
           --wt-bg-elev2 (which then turned solid-white under a brightness(0)
           invert(1) filter — the small white squares).  Strip the inherited
           button styling, hide the screen-reader text, and draw a white
           unicode chevron via ::after. The page-indicator dots (.ico_discover_pg)
           are sprite-based pills — restyle as flat circles. */
        /* Strip the inherited button styling (our generic rule paints it
           --wt-bg-elev2 which then turns solid-white under brightness(0)invert(1)).
           Keep transparent — the native button has its own positioned dimensions;
           we just draw a unicode chevron on top via ::after. */
        .discover_spot .paging .btn_next,
        .discover_spot .paging .btn_prev {
            position: absolute !important;
            background-color: rgba(15,17,20,.35) !important;
            background-image: none !important;
            border: 1px solid rgba(255,255,255,.2) !important;
            border-radius: 50% !important;
            font-size: 0 !important;
            color: transparent !important;
            filter: none !important;
            width: 64px !important;
            height: 64px !important;
            z-index: 10 !important;
            top: calc(50% + 35px) !important;
            transform: translateY(-50%) !important;
            transition: background-color .2s, border-color .2s, box-shadow .2s !important;
        }
        .discover_spot .paging {
            position: absolute !important;
            top: 0 !important;
            left: 0 !important;
            right: 0 !important;
            height: 100% !important;
            pointer-events: none !important;
        }
        .discover_spot .paging .btn_prev,
        .discover_spot .paging .btn_next { pointer-events: all !important; }
        .discover_spot .paging .btn_prev { left: 8px !important; }
        /* scaleX(-1) on the button mirrors the whole element including ::after,
           so ❮ inside renders as ❯ — no transform conflict on the ::after. */
        .discover_spot .paging .btn_next {
            right: 8px !important;
            transform: translateY(-50%) scaleX(-1) !important;
        }
        .discover_spot .paging .btn_prev:hover,
        .discover_spot .paging .btn_next:hover {
            background-color: rgba(0,213,100,.25) !important;
            border-color: rgba(0,213,100,.6) !important;
            box-shadow: 0 0 16px rgba(0,213,100,.35) !important;
        }
        /* Glyph is injected as a real <span> by styleCarouselArrows() in JS —
           inline styles can't be beaten by any stylesheet cascade. Suppress
           native ::before (sprite) and ::after so we don't get a double glyph. */
        .discover_spot .paging .btn_next::before,
        .discover_spot .paging .btn_prev::before,
        .discover_spot .paging .btn_next::after,
        .discover_spot .paging .btn_prev::after {
            content: none !important;
            display: none !important;
            background: none !important;
            background-image: none !important;
        }
        .discover_spot .paging .ico_discover_pg {
            background: var(--wt-bg-elev2) !important;
            background-image: none !important;
            border-radius: 50% !important;
            opacity: .8 !important;
        }
        .discover_spot .paging .ico_discover_pg.on,
        .discover_spot .paging .ico_discover_pg[aria-current="true"] {
            background: var(--wt-accent) !important;
            opacity: 1 !important;
        }

        /* Inputs */
        input, textarea, select {
            background-color: var(--wt-bg-input) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
            caret-color: var(--wt-text) !important;
        }
        input::placeholder, textarea::placeholder { color: var(--wt-text-mute) !important; }
        /* Native <select> dropdown items — the age-verification month picker
           renders its options on a system-painted listbox that ignored our
           select bg/color rule. Style <option> explicitly. */
        select option {
            background-color: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
        }
        /* Age-verification screen (.age_gate_container > .age_gate_area).
           The month picker is <a class="lk_month _selectedMonth">; the
           day/year are plain inputs. .month is the legacy span wrapper
           still present in some builds. */
        .age_gate_container,
        .age_gate_area, .age_gate_area .form_area {
            background: transparent !important;
            color: var(--wt-text) !important;
        }
        .age_gate_area .month,
        .age_gate_area .month *, .age_gate_area input {
            background-color: var(--wt-bg-input) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
            border-radius: 6px !important;
        }
        .age_gate_area .month::after { color: var(--wt-text) !important; }
        /* Continue CTA — explicit hover state so it actually reacts to the
           cursor. The button uses brand green at idle; darken on hover with
           a subtle shadow lift instead of falling back to plain inheritance. */
        /* Continue button: brand green pill with near-black bold text. Black
           reads ~9:1 against #00d564 (vs ~3.5:1 for white) and matches the
           Webtoons brand pattern of dark text on green CTAs elsewhere on
           the site. Forced via -webkit-text-fill-color on the button AND
           every descendant so an inner <span> can't override it. */
        .age_gate_area button {
            background-color: var(--wt-accent) !important;
            color: #0a0a0a !important;
            -webkit-text-fill-color: #0a0a0a !important;
            font-weight: 700 !important;
            border: none !important;
            transition: background-color .15s ease, box-shadow .15s ease, transform .1s ease !important;
        }
        .age_gate_area button * {
            color: #0a0a0a !important;
            -webkit-text-fill-color: #0a0a0a !important;
            background: transparent !important;
        }
        .age_gate_area button:hover {
            background-color: #00b855 !important;
            box-shadow: 0 4px 12px rgba(0,213,100,.35) !important;
        }
        .age_gate_area button:hover, .age_gate_area button:hover * {
            color: #0a0a0a !important;
            -webkit-text-fill-color: #0a0a0a !important;
        }
        .age_gate_area button:active { transform: translateY(1px) !important; }
        /* Continue button: <a class="btn_type9 v2 _btn_enter"> inside
           <div class="btnarea">. Brand green pill, dark text (9:1 contrast). */
        .age_gate_area .btn_type9,
        .age_gate_area ._btn_enter,
        .age_gate_area .btnarea a {
            background-color: var(--wt-accent) !important;
            color: #0a0a0a !important;
            -webkit-text-fill-color: #0a0a0a !important;
            font-weight: 700 !important;
            text-decoration: none !important;
            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;
            text-align: center !important;
            line-height: 1 !important;
            padding: 14px 36px !important;
            min-width: 160px !important;
            border-radius: 999px !important;
            border: none !important;
            box-sizing: border-box !important;
            transition: background-color .15s ease, color .15s ease, box-shadow .15s ease, transform .1s ease !important;
        }
        .age_gate_area .btn_type9 *,
        .age_gate_area ._btn_enter *,
        .age_gate_area .btnarea a * {
            color: #0a0a0a !important;
            -webkit-text-fill-color: #0a0a0a !important;
            background: transparent !important;
            text-decoration: none !important;
            transition: color .15s ease !important;
        }
        .age_gate_area .btn_type9:hover,
        .age_gate_area ._btn_enter:hover,
        .age_gate_area .btnarea a:hover {
            background-color: #00b855 !important;
            color: #ffffff !important;
            -webkit-text-fill-color: #ffffff !important;
            box-shadow: 0 4px 12px rgba(0,213,100,.35) !important;
        }
        .age_gate_area .btn_type9:hover *,
        .age_gate_area ._btn_enter:hover *,
        .age_gate_area .btnarea a:hover * {
            color: #ffffff !important;
            -webkit-text-fill-color: #ffffff !important;
        }
        .age_gate_area .btn_type9:active,
        .age_gate_area ._btn_enter:active { transform: translateY(1px) !important; }

        /* "I'll stick with limited access": <a class="lk_continue _skipAgeGate">.
           Plain white underlined text on transparent. */
        .age_gate_area .lk_continue,
        .age_gate_area ._skipAgeGate {
            background: transparent !important;
            color: var(--wt-text) !important;
            -webkit-text-fill-color: var(--wt-text) !important;
            text-decoration: underline !important;
            padding: 0 !important;
            border: none !important;
            font-weight: normal !important;
        }
        .age_gate_area .lk_continue:hover,
        .age_gate_area ._skipAgeGate:hover { opacity: .8 !important; }

        /* Month dropdown trigger + list items — keep them dark, NOT green. */
        .age_gate_area .lk_month,
        .age_gate_area ._selectedMonth {
            background-color: var(--wt-bg-input) !important;
            color: var(--wt-text) !important;
            -webkit-text-fill-color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
            border-radius: 6px !important;
            text-decoration: none !important;
            font-weight: normal !important;
        }
        .age_gate_area ._month .link {
            background-color: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
            -webkit-text-fill-color: var(--wt-text) !important;
            text-decoration: none !important;
            font-weight: normal !important;
        }
        .age_gate_area ._month .link:hover {
            background-color: var(--wt-bg-hover) !important;
            color: var(--wt-text) !important;
        }

        /* Privacy Policy inline link inside .dsc_terms — soft accent link. */
        .age_gate_area .dsc_terms a {
            color: var(--wt-link) !important;
            -webkit-text-fill-color: var(--wt-link) !important;
            text-decoration: underline !important;
        }
        .age_gate_area ::selection { background: var(--wt-bg-hover) !important; color: var(--wt-text) !important; }
        .search_box, ._searchBox {
            background-color: var(--wt-bg-elev) !important;
            border-color: var(--wt-border) !important;
        }

        /* Episode list */
        ._listInfo, .episode_lst {
            background-color: var(--wt-bg) !important;
        }
        .detail_lst li, ._episodeItem {
            background-color: var(--wt-bg-elev) !important;
            border-bottom: 1px solid var(--wt-border) !important;
        }

        /* Vignette gradient — applied to viewer, detail, and home/listing pages.
           All three classes are set/cleared by JS on every navigation; no CSS
           :has() fallback (caused false positives — see detail-page-dom.md).
           position:fixed + inset:0 locks the overlay to the viewport so it
           never scrolls away. pointer-events:none lets all clicks through. */
        body.wt-viewer::before,
        body.wt-detail::before,
        body.wt-home::before {
            content: '' !important;
            position: fixed !important;
            inset: 0 !important;
            background: linear-gradient(to right,
                #000000 0%, transparent 14%,
                transparent 86%, #000000 100%) !important;
            pointer-events: none !important;
            z-index: 9999 !important;
        }
        body.wt-viewer {
            background-color: var(--wt-bg) !important;
        }
        body.wt-viewer #container,
        body.wt-viewer #content,
        body.wt-viewer .cont_box,
        body.wt-viewer .comment_area {
            background-color: transparent !important;
        }
        .viewer_lst, .viewer_header,
        .viewer_footer, ._toolBox, .ly_episode {
            background-color: transparent !important;
            color: var(--wt-text) !important;
        }
        /* Never touch comic panels — filter:none preserves original colors. */
        ._images { filter: none !important; }

        /* Panel-strip as one "card": no shadows. Rounded corners on the
           container + overflow:hidden clip the first/last panel corners into
           the card shape, and a 1px white hairline outline (via box-shadow
           inset) defines the card boundary — same idiom as the homepage cards,
           but on the comic strip wrapper.
           - width: fit-content shrinks the container to match the actual image
             width (parent wrappers are full-width by default).
           - margin: 0 auto re-centers the shrunken container.
           - font-size:0 / line-height:0 packs stacked panels flush by killing
             text-baseline whitespace between sibling <img>s. */
        img._images {
            border-radius: 0 !important;
            display: block !important;
            vertical-align: top !important;
            box-shadow: none !important;
        }
        .viewer_img._img_viewer_area, #_imageList {
            width: fit-content !important;
            margin: 0 auto !important;
            font-size: 0 !important;
            line-height: 0 !important;
            border-radius: 16px !important;
            overflow: hidden !important;
            /* One container = one shadow = no internal seams possible (unlike
               per-image shadows). Combines a rim highlight (top edge), hairline
               outline (full perimeter), and a soft outer halo shadow on all
               sides for the lifted-card read. */
            box-shadow:
                inset 0 1px 0 rgba(255,255,255,.28),
                inset 0 0 0 1px rgba(255,255,255,.14),
                0 0 60px rgba(0,0,0,.9),
                0 16px 40px rgba(0,0,0,.7) !important;
        }
        /* font-size:0 above cascades to anything Webtoons injects between
           panels (ads, chapter links). Restore normal text metrics on
           non-img children so future inline content stays readable. */
        .viewer_img._img_viewer_area > :not(img),
        #_imageList > :not(img) {
            font-size: 14px !important;
            line-height: normal !important;
        }
        .viewer_lst, .cont_box, body.wt-viewer #content { overflow: visible !important; }

        /* Top fixed toolbar (.tool_area is natively #2f2f2f — bring it in line). */
        .tool_area {
            background: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
            border-bottom: 1px solid var(--wt-border) !important;
        }
        .tool_area .subj_info .subj, .tool_area .subj_episode { color: var(--wt-text) !important; }
        .tool_area a { color: var(--wt-text) !important; }

        /* Episode thumbnail strip — same base background as the rest of the
           viewer area so there are no visible shade shifts between sections. */
        .episode_area {
            background: var(--wt-bg) !important;
            border-color: var(--wt-border) !important;
        }
        .episode_lst { background: transparent !important; }
        /* Currently-viewing episode highlight in the thumbnail strip.
           Base CSS gives just a 3px green border on the .thmb. Make it pop more
           with a green glow halo + bold subj text so the "you are here" is
           obvious at a glance. */
        .episode_lst li .on .thmb {
            border: 3px solid var(--wt-accent) !important;
            box-shadow: 0 0 0 1px rgba(0, 213, 100, .25), 0 0 14px rgba(0, 213, 100, .55) !important;
        }
        .episode_lst li .on .subj {
            color: var(--wt-accent) !important;
            font-weight: 700 !important;
        }
        /* Episode-strip scroll arrows. Base sprite is dark glyphs on white
           that disappear into our dark surface; our generic button rule
           then paints them as small mis-aligned chips. Replace with heavy
           chevrons at thumbnail height, vertically centered against the
           87px tall thumbnail row. Scoped to .episode_lst so nothing else
           is touched even if the class names differ. */
        .episode_lst .pg_prev, .episode_lst .pg_next {
            background: rgba(15,17,20,.6) !important;
            background-image: none !important;
            border: 1px solid var(--wt-border) !important;
            border-radius: 6px !important;
            width: 40px !important;
            height: 87px !important;
            top: 12px !important;
            transform: none !important;
            font-size: 0 !important;
            color: transparent !important;
            box-shadow: 0 2px 10px rgba(0,0,0,.6) !important;
            transition: background-color .15s, border-color .15s, box-shadow .15s !important;
        }
        .episode_lst .pg_prev:hover, .episode_lst .pg_next:hover {
            background: rgba(0,213,100,.18) !important;
            border-color: rgba(0,213,100,.6) !important;
            box-shadow: 0 0 16px rgba(0,213,100,.35) !important;
        }
        /* Hide the inner <em> label so it doesn't show alongside the chevron. */
        .episode_lst .pg_prev > em, .episode_lst .pg_next > em {
            display: none !important;
        }
        /* Heavy chevron drawn via pseudo-element, absolutely positioned + flex
           centered so the glyph sits dead-center in the button. */
        .episode_lst .pg_prev::before, .episode_lst .pg_next::after {
            position: absolute !important;
            inset: 0 !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            color: var(--wt-text) !important;
            font-size: 36px !important;
            line-height: 1 !important;
            font-weight: normal !important;
            pointer-events: none !important;
        }
        .episode_lst .pg_prev::before { content: '\\276E' !important; }
        .episode_lst .pg_next::after  { content: '\\276F' !important; }
        .episode_lst .pg_prev:hover::before,
        .episode_lst .pg_next:hover::after {
            color: var(--wt-accent) !important;
        }

        /* Horizontal rules — base CSS leaves them with default browser styling. */
        hr { border-color: var(--wt-border) !important; background: var(--wt-border) !important; }

        /* Viewer sub-sections below the comic panels — explicitly dark so they
           don't reveal the cont_box background when set to transparent. */
        .viewer_info_area, .viewer_ad_area, .viewer_patron_area,
        ._patronArea, .viewer_dsc_area, .viewer_bnr {
            background: var(--wt-bg) !important;
            color: var(--wt-text) !important;
        }
        /* "Want more?" app download banner inside the viewer column — covers
           variants Webtoons may inject under nonstandard class names.
           Footer .foot_app/.foot_cont/.foot_down_msg/.footapp_icon_cont are
           already dark from the footer block above. */
        #_viewerBox .foot_app, .viewer_lst .foot_app,
        [class*="dsc_down"], [class*="DownApp"], [class*="download_app"] {
            background: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
        }

        /* === Viewer page elevation ===
           Cards are injected by buildViewerCards() in JS — CSS only provides the
           class definition and resets; JS handles the section grouping. */

        /* width:330px keeps the float from exceeding the 1200px cont_box;
           height:fit-content prevents stretching to match the comment column. */
        .aside.viewer {
            box-sizing: border-box !important;
            width: 330px !important;
            background: transparent !important;
            border-radius: 0 !important;
            padding: 0 !important;
            box-shadow: none !important;
            height: fit-content !important;
        }
        .aside .ranking_lst.viewer {
            background: transparent !important;
            display: flex !important;
            flex-direction: column !important;
            gap: 12px !important;
            padding: 0 !important;
        }
        /* Ranking list items: transparent so they don't nest card-on-card. */
        .aside.viewer .ranking_lst li {
            background-color: transparent !important;
            border-radius: 0 !important;
            border: none !important;
        }
        /* Card class injected by JS onto each section wrapper. */
        .wt-viewer-card {
            background: var(--wt-bg-elev) !important;
            border-radius: 14px !important;
            padding: 16px !important;
            border: 1px solid var(--wt-border) !important;
            box-shadow: 0 8px 32px rgba(0,0,0,.35) !important;
        }
        /* Section header arrow — sprite, needs filter not color.
           Same treatment on .aside.viewer (viewer page) and .aside.challenge
           (canvas page sidebar). */
        .aside.viewer .ico_arr1,
        .aside.challenge .ico_arr1,
        .title_area .ico_arr1 {
            filter: brightness(0) invert(1) opacity(.7) !important;
            color: var(--wt-text) !important;
        }
        .ranking_lst .title_area h2 a, .ranking_lst .title_area h2 span {
            color: var(--wt-text) !important;
        }
        .ranking_lst .title_area h2 span em { color: var(--wt-text-dim) !important; }
        .ranking_lst .ico_arr1 { color: var(--wt-text-dim) !important; }
        /* Ranking list item dividers and section separators. */
        .ranking_lst li, .aside_item, .aside_wrap,
        .cont_box .aside { border-color: var(--wt-border) !important; }
        /* .ranking_wrap inside .aside.viewer is now a card — no internal borders.
           Keep the rule for .section_wrap and non-viewer asides only. */
        .cont_box .aside:not(.viewer) .section_wrap,
        .cont_box .aside:not(.viewer) .ranking_wrap {
            border-top-color: var(--wt-border) !important;
            border-bottom-color: var(--wt-border) !important;
        }
        /* Viewer info / ad / patron section top separators — hidden entirely
           so the column under the panels reads as one continuous dark surface
           (no faint horizontal lines below the comic). */
        .viewer_lst .viewer_info_area,
        .viewer_lst .viewer_ad_area,
        .viewer_patron_area,
        .viewer_lst .viewer_dsc_area,
        .viewer_lst .viewer_bnr {
            border-top: none !important;
            border-bottom: none !important;
        }
        /* WCC comment sort tabs bottom border. */
        [class*="wcc_SortOrderTabs__root"] { border-bottom-color: var(--wt-border) !important; }
        /* Sidebar patron/section separator inside .aside.detail. */
        .aside.detail .aside_patron { border-top-color: var(--wt-border) !important; }
        /* Ranking list section bottom border (.lst_type1 = the ranked item list).
           Suppress in viewer aside — each .ranking_wrap is already a card. */
        .lst_type1 { border-bottom-color: var(--wt-border) !important; }
        .aside.viewer .lst_type1 { border-bottom: none !important; }
        /* CANVAS Weekly round-up / challenge_spot top separator. */
        .challenge_spot, .viewer .challenge_spot { border-top-color: var(--wt-border) !important; }

        /* "Share this series and show support" prompt + Like/Subscribe pills */
        .viewer_lst .dsc_encourage { color: var(--wt-text) !important; }
        .spi_area .bx {
            background: var(--wt-bg-elev2) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
            transition: background .15s, border-color .15s, box-shadow .15s, transform .1s !important;
        }
        .spi_area .bx:hover {
            background: var(--wt-bg-hover) !important;
            border-color: var(--wt-accent) !important;
            box-shadow: 0 0 0 1px var(--wt-accent), 0 4px 14px rgba(0,213,100,.15) !important;
            transform: translateY(-1px) !important;
        }
        /* Heart sprite (ico_like2) — tint red to signal "like". */
        .spi_area .ico_like2 {
            filter: brightness(0) saturate(100%) invert(47%) sepia(89%) saturate(505%) hue-rotate(314deg) brightness(95%) contrast(92%) !important;
        }
        /* Subscribe "+" icon (ico_plus3/ico_plus4) — sprite is dark-on-transparent,
           invert to white so it's visible on the dark button background. */
        .spi_area .ico_plus3, .spi_area .ico_plus4 {
            filter: brightness(0) invert(1) opacity(.9) !important;
        }
        .cont_box .viewer_lst .spi_area .lnk_favorites.on { color: var(--wt-text-dim) !important; }

        /* Comments section header + creator note card */
        .comment_area { background: var(--wt-bg) !important; color: var(--wt-text) !important; }
        .comment_head .title_comments { color: var(--wt-text) !important; }
        .comment_head .count { color: var(--wt-text-dim) !important; }
        .comment_area .creator_note {
            background: var(--wt-bg-elev) !important;
            border: 1px solid var(--wt-border) !important;
            border-radius: 8px;
            padding: 16px;
        }
        .comment_area .creator_note .title { color: var(--wt-text-dim) !important; }
        .comment_area .creator_note .author_area .author,
        .comment_area .creator_note .author_area .author_name { color: var(--wt-text) !important; }
        .comment_area .creator_note .author_area .author_name span { color: var(--wt-text) !important; }

        /* Wildcard attribute selectors catch variant class names the WCC
           component ships — avoids invisible-text regressions on updates. */
        [class*="cbox_nick"], [class*="cbox_name"], [class*="comment_nick"], [class*="user_nick"] {
            color: var(--wt-link) !important;
        }
        [class*="cbox_date"], [class*="comment_date"] { color: var(--wt-text-mute) !important; }
        [class*="cbox_sort"] a, [class*="cbox_sort"] button { color: var(--wt-text-dim) !important; }
        [class*="cbox_sort"] .on, [class*="cbox_sort"] [aria-current="true"] {
            color: var(--wt-text) !important;
        }

        /* Webtoons replaced the legacy Naver u_cbox widget with "WCC"
           (Webtoon Comment Component), which uses CSS-module class names of
           the form wcc_<Component>__<element>. Legacy .u_cbox_* kept as fallback. */

        /* WCC App MASTER container -- this is the OUTERMOST wrapper of the
           comment widget (wcc_App__root). v1.0.9 missed this; comment
           items inside were dark, but the whole widget sat on a white App. */
        [class*="wcc_App__root"], [class*="wcc_App__loader"] {
            background: var(--wt-bg) !important;
            color: var(--wt-text) !important;
        }
        /* Kebab-case wcc loaders (don't match [class*="wcc_"] -- they use dashes) */
        [class*="wcc-comment-list-loader"], [class*="wcc-sort-order-loader"] {
            background: transparent !important;
            color: var(--wt-text-dim) !important;
        }
        /* Comment editor (where the user types). Multiple sub-classes. */
        [class*="wcc_Editor__root"], [class*="wcc_Editor__content"],
        [class*="wcc_Editor__editor"], [class*="wcc_Editor__scrollArea"],
        [class*="wcc_Editor__actionBar"], [class*="wcc_Editor__toolbar"],
        [class*="wcc_Editor__attachment"], [class*="wcc_Editor__creatorPost"],
        [class*="wcc_Editor__modifyContainer"], [class*="wcc_Editor__replyContainer"],
        [class*="wcc_Editor__spoilerWrapper"], [class*="wcc_Editor__mobileShortened"],
        [class*="wcc_Editor__bottomLeftCornerIcon"] {
            background: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
            border-color: var(--wt-border) !important;
        }
        [class*="wcc_Editor__editor"] {
            caret-color: var(--wt-text) !important;
        }
        /* Comment editor toolbar action icons. The current WCC ships them as
           inline SVGs with stroke="currentColor" and fill="currentColor"
           under a TextEditor_* CSS-module class family (not wcc_Editor__).
           So setting color on the button is enough — no filters needed
           (filters were flattening the icons to solid white blobs). */
        [class*="wcc_Editor__actionBar"] button,
        [class*="wcc_Editor__toolbar"] button,
        [class*="wcc_Editor__bottomLeftCornerIcon"],
        [class*="wcc_Editor__bottomLeftCornerIcon"] button,
        [class*="TextEditor_"] button,
        [class*="TextEditor_"] [role="button"] {
            color: var(--wt-text-dim) !important;
            opacity: 1 !important;
        }
        [class*="wcc_Editor__actionBar"] button:hover,
        [class*="wcc_Editor__toolbar"] button:hover,
        [class*="wcc_Editor__bottomLeftCornerIcon"] button:hover,
        [class*="TextEditor_"] button:hover,
        [class*="TextEditor_"] [role="button"]:hover {
            color: var(--wt-text) !important;
        }
        /* Spoiler toggle inside the comment editor */
        [class*="wcc_Spoiler__root"], [class*="wcc_Spoiler__text"] {
            color: var(--wt-text) !important;
            background: transparent !important;
        }
        [class*="wcc_Spoiler__switch"] {
            background: var(--wt-bg-elev2) !important;
            border-color: var(--wt-border) !important;
        }
        [class*="wcc_Spoiler__slider"] { background: var(--wt-text-dim) !important; }
        [class*="wcc_Spoiler__disabled"] { color: var(--wt-text-mute) !important; }
        [class*="wcc_SpoilerGuard__viewText"] { color: var(--wt-text-dim) !important; }
        [class*="wcc_SpoilerGuard__viewAll"] { color: var(--wt-link) !important; }

        /* WCC widget root surfaces -- list, individual rows, body, header */
        [class*="wcc_CommentList__"], [class*="wcc_CommentLoader__"],
        [class*="wcc_CommentView__"], [class*="wcc_CommentEmpty__"] {
            background-color: var(--wt-bg) !important;
            color: var(--wt-text) !important;
        }
        [class*="wcc_CommentItem__root"] {
            background-color: var(--wt-bg-elev) !important;
            border: 1px solid var(--wt-border) !important;
            border-radius: 8px !important;
            margin-bottom: 8px !important;
            padding: 12px !important;
        }
        [class*="wcc_CommentItem__inside"], [class*="wcc_CommentItem__corner"],
        [class*="wcc_CommentItem__action"], [class*="wcc_CommentItem__bestOnly"],
        [class*="wcc_CommentItem__replied"] {
            background: transparent !important;
            color: var(--wt-text) !important;
        }
        /* Comment text body */
        [class*="wcc_CommentBody__"] {
            background: transparent !important;
            color: var(--wt-text) !important;
        }
        [class*="wcc_CommentBody__deleted"], [class*="wcc_CommentBody__blinded"],
        [class*="wcc_CommentBody__emptyBody"] {
            color: var(--wt-text-mute) !important;
        }
        /* Comment header: nickname + date + creator badge */
        [class*="wcc_CommentHeader__root"], [class*="wcc_CommentHeader__identity"] {
            background: transparent !important;
        }
        [class*="wcc_CommentHeader__name"]          { color: var(--wt-link) !important; }
        [class*="wcc_CommentHeader__createdAt"]     { color: var(--wt-text-mute) !important; }
        [class*="wcc_CommentHeader__creatorBadge"],
        [class*="wcc_CommentHeader__ownerSign"]     { color: var(--wt-accent) !important; }

        /* Sort-order tabs (TOP / NEWEST) */
        [class*="wcc_SortOrderTabs__root"], [class*="wcc_SortOrderTab__root"] {
            background: transparent !important;
            color: var(--wt-text-dim) !important;
        }
        [class*="wcc_SortOrderTab__active"] {
            color: var(--wt-accent) !important;
        }

        /* Reaction buttons (like / dislike / etc.) */
        [class*="wcc_CommentReaction__root"],
        [class*="wcc_CommentReaction__action"] {
            background: transparent !important;
            color: var(--wt-text-dim) !important;
        }
        [class*="wcc_CommentReaction__active"] { color: var(--wt-accent) !important; }
        [class*="wcc_CommentReaction__disabled"] { color: var(--wt-text-mute) !important; }

        /* "Best comment" badge + super-like badge */
        [class*="wcc_BestBadge__root"] {
            background-color: var(--wt-bg-elev2) !important;
            color: var(--wt-accent) !important;
            border: 1px solid var(--wt-border) !important;
        }
        [class*="wcc_SuperLikeBadge__root"] {
            background-color: var(--wt-bg-elev2) !important;
            color: var(--wt-accent) !important;
        }

        /* Reply folder + unfold buttons */
        [class*="wcc_ReplyFolder__root"], [class*="wcc_ReplyUnfold__root"],
        [class*="wcc_ReplyUnfold__unfold"], [class*="wcc_ReplyFolderToggle__root"] {
            background: transparent !important;
            color: var(--wt-link) !important;
        }
        [class*="wcc_ReplyUnfold__arrow"] { color: var(--wt-text-dim) !important; }

        /* "More comments" loader / pagination */
        [class*="wcc_CommentMore__root"], [class*="wcc_CommentMore__more"],
        [class*="wcc_CommentMore__prev"], [class*="wcc_CommentMore__progress"],
        [class*="wcc_CommentMore__noEditor"], [class*="wcc_CommentMore__reply"] {
            background: transparent !important;
            color: var(--wt-text-dim) !important;
        }
        [class*="wcc_CommentMore__more"]:hover,
        [class*="wcc_CommentMore__prev"]:hover { color: var(--wt-text) !important; }
        [class*="wcc_CommentMore__arrow"] { color: var(--wt-text-dim) !important; }

        /* Alert / report / option-menu popups */
        [class*="wcc_AlertPopup__overlay"],
        [class*="wcc_CommentReportPopup__overlay"] {
            background: rgba(0,0,0,.7) !important;
        }
        [class*="wcc_AlertPopup__content"],
        [class*="wcc_CommentReportPopup__content"],
        [class*="wcc_CommentOptionMenu__content"],
        [class*="wcc_CommentOptionMenu__menu"] {
            background-color: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
            box-shadow: 0 8px 24px rgba(0,0,0,.5) !important;
        }
        [class*="wcc_AlertPopup__title"],
        [class*="wcc_CommentReportPopup__title"] { color: var(--wt-text) !important; }
        /* Popup buttons — --wt-bg-hover (not --wt-bg-elev2) keeps visible
           contrast against the popup card's --wt-bg-elev background. */
        [class*="wcc_AlertPopup__content"] button,
        [class*="wcc_CommentReportPopup__content"] button,
        [class*="wcc_AlertPopup__cancel"],
        [class*="wcc_CommentReportPopup__cancel"],
        [class*="wcc_CommentReportPopup__reason"] {
            background: var(--wt-bg-hover) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
        }
        [class*="wcc_AlertPopup__content"] button:hover,
        [class*="wcc_CommentReportPopup__content"] button:hover {
            background: var(--wt-bg-elev2) !important;
            border-color: var(--wt-text-dim) !important;
        }

        /* Empty state */
        [class*="wcc_CommentEmpty__message"] { color: var(--wt-text-dim) !important; }

        /* Legacy u_cbox widget (kept as fallback for older pages) */
        #_cmtArea, .cmt_area, .u_cbox, .u_cbox_content_wrap,
        .u_cbox_comment_box, .u_cbox_write, .u_cbox_module {
            background-color: var(--wt-bg) !important;
            color: var(--wt-text) !important;
        }
        .u_cbox_comment, .u_cbox_reply_area {
            background-color: var(--wt-bg-elev) !important;
            border: 1px solid var(--wt-border) !important;
            border-radius: 4px;
        }
        .u_cbox_nick, .u_cbox_name        { color: var(--wt-link) !important; }
        .u_cbox_contents, .u_cbox_text    { color: var(--wt-text) !important; }
        .u_cbox_date, .u_cbox_info_txt    { color: var(--wt-text-mute) !important; }

        /* Footer */
        #footer, .footer, .ft_lnk, .ft_area {
            background-color: var(--wt-bg-elev) !important;
            color: var(--wt-text-dim) !important;
            border-top: 1px solid var(--wt-border) !important;
        }
        .footer a, #footer a { color: var(--wt-text-dim) !important; }

        /* Footer social icons are sprite glyphs — invert to white. Opacity .75
           balances the filled Facebook glyph against line-style Instagram/X/YouTube. */
        .btn_foot_facebook, .btn_foot_instagram, .btn_foot_twitter,
        .btn_foot_youtube, .btn_foot_pinterest, .btn_foot_line {
            filter: brightness(0) invert(1) opacity(.75) !important;
        }
        .btn_foot_facebook:hover, .btn_foot_instagram:hover, .btn_foot_twitter:hover,
        .btn_foot_youtube:hover, .btn_foot_pinterest:hover, .btn_foot_line:hover {
            filter: brightness(0) invert(1) opacity(1) !important;
        }

        /* Footer language selector (English ▾ button + dropdown) */
        .foot_menu .language .lk_lang {
            background: var(--wt-bg-elev) !important;
            border: 1px solid var(--wt-border) !important;
            color: var(--wt-text) !important;
        }
        .foot_menu .language .lk_lang:after {
            border-top-color: var(--wt-text) !important;
        }
        .foot_menu .language .ly_lang {
            background: var(--wt-bg-elev) !important;
            border: 1px solid var(--wt-border) !important;
            box-shadow: 0 0 8px rgba(0,0,0,.5) !important;
        }
        .foot_menu .language .ly_lang li        { background: transparent !important; }
        .foot_menu .language .ly_lang a         { color: var(--wt-text-dim) !important; }
        .foot_menu .language .ly_lang li:hover a,
        .foot_menu .language .ly_lang a[aria-current="true"] {
            color: var(--wt-accent) !important;
        }

        /* Scrollbars (WebKit) */
        ::-webkit-scrollbar              { width: 10px; height: 10px; }
        ::-webkit-scrollbar-track        { background: var(--wt-bg); }
        ::-webkit-scrollbar-thumb        {
            background: var(--wt-bg-elev2);
            border-radius: 5px;
            border: 2px solid var(--wt-bg);
        }
        ::-webkit-scrollbar-thumb:hover  { background: #3a4049; }

        /* Selection */
        ::selection { background: var(--wt-accent); color: var(--wt-text-on-accent); }

        /* "NEW" / "UP" badge chips — actual classes are badge_new2, badge_up2.
           Dim them slightly on dark rather than override with a solid color
           (they're sprite-based so background-color would block the graphic). */
        [class^="badge_new"], [class^="badge_up"] {
            opacity: .85 !important;
        }

        /* Mobile (m.webtoons.com) */
        .header_wrap, .navigation, .nav_wrap, .lst_episode, .episode_cont {
            background-color: var(--wt-bg) !important;
            color: var(--wt-text) !important;
        }

        /* "Recently viewed" floating bar on the right edge. */
        .recently_area {
            background: var(--wt-bg-elev) !important;
            background-image: none !important;
            border-left: 1px solid var(--wt-border) !important;
        }
        /* .menu is the collapsed tab — its white appearance comes from the sprite
           sheet, not the bg PNG. Clear the sprite and draw our own dark pill. */
        .recently_area .menu {
            background-image: none !important;
            background-color: var(--wt-bg-elev) !important;
            border-radius: 8px 0 0 8px !important;
            border: 1px solid var(--wt-border) !important;
            border-right: none !important;
        }
        /* Collapse/expand chevron (← arrow) — dark sprite glyph on white. */
        .recently_area.unfd .ico_recently {
            filter: brightness(0) invert(1) opacity(.7) !important;
        }
        .recently_area .t_recently,
        .recently_area .t_recently2,
        .recently_cont .subj { color: var(--wt-text) !important; }
        .recently_cont .episode { color: var(--wt-text-dim) !important; }
        .recently_cont .bar { background: var(--wt-border) !important; }
        .recently_area.unfd [class$="_line"] { border-left-color: var(--wt-border) !important; }

        /* Login modal (Naver SNS-login widget). Uses ._loginLayer / ._loginDimLayer
           injected by /static/bundle/common/gnb-*.js when "Log In" is clicked. */
        ._loginDimLayer { background: rgba(0,0,0,.7) !important; }
        ._loginLayer, ._defaultLoginComponent {
            background: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
            box-shadow: 0 12px 32px rgba(0,0,0,.6) !important;
        }
        ._loginLayer h1, ._loginLayer h2, ._loginLayer h3,
        ._loginLayer p, ._loginLayer label, ._loginLayer span,
        ._defaultLoginComponent h1, ._defaultLoginComponent h2,
        ._defaultLoginComponent p, ._defaultLoginComponent span,
        ._defaultLoginComponent label {
            color: var(--wt-text) !important;
            background: transparent !important;
        }
        ._btnLoginSns, .btn_sns, ._btnLoginEmail {
            background: var(--wt-bg-elev2) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
        }
        ._btnLoginSns:hover, .btn_sns:hover, ._btnLoginEmail:hover {
            background: var(--wt-bg-hover) !important;
        }
        ._btnLoginLayerClose { color: var(--wt-text) !important; }

        /* ---------- Series detail page (e.g. /<lang>/<genre>/<slug>/list?title_no=...) ---------- */

        /* === Detail page elevation ===
           Three-level hierarchy: page (--wt-bg) → episode list + sidebar cards
           (--wt-bg-elev) → episode rows (--wt-bg-elev2). */

        /* .cont_box is a sibling of .detail_bg and sits on top of the artwork
           div. Transparent here lets the artwork show through the detail_header
           area (.detail_body has its own explicit dark background, so the
           episode list area stays correctly dark). Scoped via adjacent-sibling
           so the general .cont_box dark background still applies elsewhere. */
        .detail_bg + .cont_box {
            background-color: transparent !important;
        }

        /* Episode list column — .detail_body .detail_lst is float:left, 761px wide.
           No border-top: it connects to the app-download banner above.
           No overflow:hidden: the pagination is position:absolute at the bottom
           and clips badly with hidden overflow. Base padding-bottom was 66px —
           keep that so pagination stays in its original position. */
        .detail_body .detail_lst {
            background: var(--wt-bg-elev) !important;
            border-radius: 16px !important;
            padding-bottom: 66px !important;
            border: none !important;
            box-shadow: inset 0 0 0 1px rgba(255,255,255,.1), 0 8px 32px rgba(0,0,0,.55) !important;
        }
        /* Right sidebar — its own elevated card. */
        .aside.detail {
            background: var(--wt-bg-elev) !important;
            border-radius: 16px !important;
            padding: 16px !important;
            border: none !important;
            box-shadow: inset 0 0 0 1px rgba(255,255,255,.1), 0 12px 40px rgba(0,0,0,.55) !important;
        }
        /* Sidebar CTA buttons (Continue reading / First episode). */
        .aside.detail .aside_btn .btn_type7 {
            background: var(--wt-bg-elev2) !important;
            border: 1px solid var(--wt-border) !important;
            color: var(--wt-text) !important;
            transition: background .15s, border-color .15s, box-shadow .15s, transform .1s !important;
        }
        .aside.detail .aside_btn .btn_type7:hover {
            background: var(--wt-bg-hover) !important;
            border-color: var(--wt-accent) !important;
            box-shadow: 0 0 0 1px var(--wt-accent), 0 4px 16px rgba(0,213,100,.18) !important;
            transform: translateY(-1px) !important;
            color: var(--wt-accent) !important;
        }
        /* Episode list dividers — base CSS uses #f5f5f5 (nearly white) on both
           top and bottom borders of each row. */
        .detail_body .detail_lst li,
        .detail_body .detail_lst li:first-child {
            border-color: var(--wt-border) !important;
            transition: background .12s, box-shadow .12s !important;
        }
        .detail_body .detail_lst li:hover {
            background: var(--wt-bg-elev2) !important;
        }
        /* Cap .subj so the row's total column widths fit inside the li and don't
           overflow past the ::after border. Overrides base CSS width:411px. */
        .detail_body .detail_lst .subj {
            max-width: 385px !important;
            width: 385px !important;
        }
        /* Pseudo-element border renders above thumbnail and all children. */
        .detail_body .detail_lst li:hover::after {
            content: '' !important;
            position: absolute !important;
            inset: 0 !important;
            border: 2px solid var(--wt-accent) !important;
            border-radius: 6px !important;
            pointer-events: none !important;
            z-index: 5 !important;
        }
        /* Turn date and like count accent green on hover. */
        .detail_body .detail_lst li > a:hover .date {
            color: var(--wt-accent) !important;
        }
        .detail_body .detail_lst li > a:hover .like_area {
            color: var(--wt-accent) !important;
        }
        /* Also tint the heart sprite green on hover. */
        .detail_body .detail_lst li > a:hover .ico_like {
            filter: brightness(0) saturate(100%) invert(62%) sepia(67%) saturate(475%) hue-rotate(103deg) brightness(95%) contrast(92%) !important;
        }
        .detail_body .detail_lst li > a:hover .tx {
            color: var(--wt-text-dim) !important;
        }
        /* Base CSS: .detail_body .detail_lst .subj span { color: #3d3d3d } and
           .date { color: #b1b1b1 } — invisible on dark. Restate at matching
           specificity, plus broader fallbacks to catch any internal element. */
        .detail_body .detail_lst .subj,
        .detail_body .detail_lst .subj span,
        .detail_lst .subj, .detail_lst .subj span,
        .detail_lst li a, .detail_lst li a span:not(.date):not(.tx) {
            color: var(--wt-text) !important;
        }
        .detail_body .detail_lst .date, .detail_lst .date,
        .detail_body .detail_lst .tx, .detail_lst .tx { color: var(--wt-text-dim) !important; }

        /* Paywall notice and install-app strip at the bottom of the episode list.
           Base CSS uses border-top: 1px solid #f5f5f5 which is nearly invisible
           on dark; tint to our border colour. */
        .detail_paywall, .detail_install_app {
            border-top-color: var(--wt-border) !important;
            color: var(--wt-text) !important;
        }
        .detail_install_app em { color: var(--wt-accent) !important; }

        /* Subscribe / bookmark button (.btn_favorite) — base CSS hardcodes
           background:#fff + color:#000. This element is NOT a <button> so our
           generic button rule misses it. */
        .btn_favorite {
            background: var(--wt-bg-elev2) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
        }
        .btn_favorite:hover { background: var(--wt-bg-hover) !important; }

        /* .ly_area — inline popup/dropdown used for share menus and author info
           tooltips throughout the detail page. Base CSS: background:#fff; border:
           1px solid #b4b4b4. Our general .ly_box popup rule does not catch this. */
        .ly_area {
            background: var(--wt-bg-elev) !important;
            border-color: var(--wt-border) !important;
            color: var(--wt-text) !important;
        }

        /* Subscribe-tier popup (.ly_subscribe) — white panel that appears when
           the subscribe button is clicked; right:20px, top:239px, z-index:120. */
        .ly_subscribe {
            background: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
            box-shadow: 0 8px 24px rgba(0,0,0,.5) !important;
        }

        /* Episode sort dropdown — "Latest / Oldest" selector above the
           episode list on detail pages. Scoped to .detail_body so it
           doesn't clobber the /canvas .sort_box rules above (which set
           the elevated panel + border-radius + open animation). */
        .detail_body .sort_box {
            background: var(--wt-bg-elev) !important;
            border-color: var(--wt-border) !important;
            color: var(--wt-text) !important;
            box-shadow: 0 4px 12px rgba(0,0,0,.5) !important;
        }
        .detail_body .sort_box a, .detail_body .sort_box button { color: var(--wt-text-dim) !important; background: transparent !important; }
        .detail_body .sort_box a:hover, .detail_body .sort_box button:hover,
        .detail_body .sort_box .on, .detail_body .sort_box [aria-current="true"] { color: var(--wt-text) !important; }

        /* "You may also like" recommendation card items (.other_card_item) —
           base CSS: background:#fff. They sit inside .detail_other which has
           no background set, so overriding the item itself is enough. */
        .other_card_item {
            background: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
        }
        .other_card_item:hover { background: var(--wt-bg-elev2) !important; }

        /* Scoped to .detail_other so it doesn't paint .lst_type1 rows
           inside elevated viewer/canvas sidebar cards (those should
           stay flat — see Top CANVAS / wt-viewer-card rules above). */
        .detail_other .lst_type1 li {
            background: var(--wt-bg-elev) !important;
            border-color: var(--wt-border) !important;
            border-radius: 6px;
        }
        .detail_other .lst_type1 li:hover { background: var(--wt-bg-elev2) !important; }
        .detail_other .lst_type1 .subj   { color: var(--wt-text) !important; }
        .detail_other .lst_type1 .author { color: var(--wt-text-dim) !important; }
        .detail_other .lst_type1 .grade_num, .detail_other .lst_type1 .grade_area { color: var(--wt-text-mute) !important; }
        .detail_other h2 { color: var(--wt-text) !important; }
        .detail_other h2 .point { color: var(--wt-accent) !important; }

        /* Skin image — filter:none prevents any parent rule from inverting the
           artwork. We let the artwork tile naturally: the inline background-image
           shorthand resets background-color to transparent, so the dark
           .detail_header background shows in the side margins where the image
           doesn't fully cover (no artificial clipping or masking). */
        .detail_bg { filter: none !important; }

        /* Author info icon (.ico_info2) inside the detail header is typically
           a <button> element, so our generic button rule gives it a dark
           background rectangle. Since .detail_header is now transparent (shows
           artwork), that rectangle is visible against the artwork. Clear it. */
        .detail_header .ico_info2, .detail_header [class*="ico_info"] {
            background-color: transparent !important;
            border: none !important;
            box-shadow: none !important;
        }

        /* Series title and genre text — text-shadow improves legibility on any
           artwork background; letter-spacing and uppercase on the genre label
           give the header a more editorial, polished look. */
        .detail_header .info .subj {
            letter-spacing: .04em !important;
            text-shadow: 0 2px 16px rgba(0,0,0,.95), 0 0 48px rgba(0,0,0,.6) !important;
        }
        .detail_header .info .genre {
            letter-spacing: .18em !important;
            text-transform: uppercase !important;
            font-size: 13px !important;
            font-weight: 500 !important;
            text-shadow: 0 1px 6px rgba(0,0,0,.95) !important;
        }
        .detail_header .info .author_area {
            text-shadow: 0 1px 4px rgba(0,0,0,.9) !important;
        }

        /* Subscribe button "+" icon (.ico_plus4 sprite). The sprite was designed
           for the original white btn_favorite background (dark glyph on white).
           Now that btn_favorite has a dark background, invert the sprite to white. */
        .btn_favorite .ico_plus4 {
            filter: brightness(0) invert(1) !important;
        }

        /* Episode list typography — clear visual hierarchy across the four columns:
           title > date > likes > episode number. */
        .detail_body .detail_lst .subj span {
            font-size: 17px !important;
            font-weight: 500 !important;
            color: var(--wt-text) !important;
            letter-spacing: .01em !important;
        }
        .detail_body .detail_lst li > a:hover .subj span {
            color: var(--wt-accent) !important;
        }
        .detail_body .detail_lst .date {
            font-size: 13px !important;
            color: var(--wt-text) !important;
            letter-spacing: .03em !important;
        }
        /* Like area count number — red to match the heart icon. */
        .detail_body .detail_lst .like_area {
            color: var(--wt-accent-like) !important;
            font-size: 13px !important;
        }
        /* .ico_like is a sprite (background-image), not text — filter it red. */
        .detail_body .detail_lst .ico_like {
            filter: brightness(0) saturate(100%) invert(47%) sepia(89%) saturate(505%) hue-rotate(314deg) brightness(95%) contrast(92%) !important;
        }
        /* Episode number (#5, #4 …) — slightly muted so it reads as metadata.
           padding-right keeps it clear of the 2px hover border. */
        .detail_body .detail_lst .tx {
            font-size: 14px !important;
            font-weight: 600 !important;
            color: var(--wt-text-mute) !important;
            letter-spacing: .03em !important;
            padding-right: 6px !important;
        }

        /* Series with .type_white skin (e.g. Sweet Romance, Spicy Roommates):
           the base CSS hard-codes .info text to #000 / #252525 which assumes
           the page is light. Override so author / title are readable on dark.
           (.ico_info2 is intentionally NOT filtered here — that was the
           "white blob" bug from v1.0.11.) */
        .detail_header.type_white .subj,
        .detail_header.type_white h1.subj { color: var(--wt-text) !important; }
        .detail_header.type_white .info .author,
        .detail_header.type_white .info .author_area,
        .detail_header.type_white .author,
        .detail_header.type_white .author_area {
            color: var(--wt-text-dim) !important;
        }

        /* Ranking number sprite digits (1, 2, 3, ... 10) in trending/popular
           sidebars — plain dark glyphs on transparent, invert to white. */
        .ico_n1, .ico_n2, .ico_n3, .ico_n4, .ico_n5,
        .ico_n6, .ico_n7, .ico_n8, .ico_n9, .ico_n10 {
            filter: brightness(0) invert(1) opacity(.85) !important;
        }
        /* Rank-number badges (.ranking_number_X) on homepage trending cards
           AND the /rankings listing page. The base markup is the same
           (<strong class="ranking_number_N"> with a sprite background +
           <span class="blind"> for screen readers), but the parent class
           differs per page: .webtoon_list on homepage, .ranking_text on
           /rankings. Match the sprite class globally so both render. The
           native sprite atlas covers 1–10, so /rankings 11–30 stayed blank;
           draw all 30 via ::before with content per number. */
        [class^="ranking_number_"]:before {
            background-image: none !important;
            text-indent: 0 !important;
            overflow: visible !important;
            width: auto !important;
            height: auto !important;
            font-size: 58px !important;
            font-weight: 900 !important;
            color: var(--wt-text) !important;
            text-shadow: 0 2px 10px rgba(0,0,0,.95) !important;
            white-space: normal !important;
            vertical-align: bottom !important;
            line-height: 1 !important;
            display: inline-block !important;
        }
        .ranking_number_1:before  { content: "1" !important; }
        .ranking_number_2:before  { content: "2" !important; }
        .ranking_number_3:before  { content: "3" !important; }
        .ranking_number_4:before  { content: "4" !important; }
        .ranking_number_5:before  { content: "5" !important; }
        .ranking_number_6:before  { content: "6" !important; }
        .ranking_number_7:before  { content: "7" !important; }
        .ranking_number_8:before  { content: "8" !important; }
        .ranking_number_9:before  { content: "9" !important; }
        .ranking_number_10:before { content: "10" !important; }
        .ranking_number_11:before { content: "11" !important; }
        .ranking_number_12:before { content: "12" !important; }
        .ranking_number_13:before { content: "13" !important; }
        .ranking_number_14:before { content: "14" !important; }
        .ranking_number_15:before { content: "15" !important; }
        .ranking_number_16:before { content: "16" !important; }
        .ranking_number_17:before { content: "17" !important; }
        .ranking_number_18:before { content: "18" !important; }
        .ranking_number_19:before { content: "19" !important; }
        .ranking_number_20:before { content: "20" !important; }
        .ranking_number_21:before { content: "21" !important; }
        .ranking_number_22:before { content: "22" !important; }
        .ranking_number_23:before { content: "23" !important; }
        .ranking_number_24:before { content: "24" !important; }
        .ranking_number_25:before { content: "25" !important; }
        .ranking_number_26:before { content: "26" !important; }
        .ranking_number_27:before { content: "27" !important; }
        .ranking_number_28:before { content: "28" !important; }
        .ranking_number_29:before { content: "29" !important; }
        .ranking_number_30:before { content: "30" !important; }
        /* Homepage "Trending" / "Popular" tab pills — base CSS uses
           #f3f3f3 (inactive) and #000 (active). Our generic button rule
           targets the <button> element, not <div class="button">. */
        .main_section_tab .button {
            background-color: var(--wt-bg-elev2) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
        }
        .main_section_tab .button:hover {
            background-color: var(--wt-bg-hover) !important;
            color: var(--wt-text) !important;
        }
        .main_section_tab .button[aria-selected="true"] {
            background-color: var(--wt-accent) !important;
            color: var(--wt-text-on-accent) !important;
            border-color: var(--wt-accent) !important;
        }
        /* Stats glyphs (.ico_view / .ico_view2 / .ico_subscribe / .ico_grade /
           .ico_grade2) and the author-info icon (.ico_info2) are intentionally
           NOT filtered: the sprite at those positions has the BRAND GREEN
           color baked in. Filtering bleaches it to grey/white — losing the
           brand identity AND turning the info icon into a solid white blob.
           Brand green renders fine against our dark surface as-is. */

        /* Pagination row at the bottom of the episode list. Base CSS hard-codes
           color:#070707 on both .paginate a and strong — invisible on dark.
           On detail pages the base places .paginate inside the floated
           .detail_lst column at a position the floated children paint around,
           so it renders between episode rows. Force normal flow unconditionally
           — gating on body.wt-detail (set by JS) caused a visible flash where
           pagination painted mid-list before the class was applied. /canvas
           pagination is already in normal flow, so position/clear are no-ops
           there. */
        .paginate:not(.v2) {
            position: static !important;
            display: block !important;
            clear: both !important;
            text-align: center !important;
            margin: 24px 0 8px !important;
        }
        .paginate a, .paginate strong, .paginate span,
        .paginate.v2 [class^="pg_"] {
            color: var(--wt-text) !important;
        }
        /* Pagination pills: explicit centered dimensions so both the link
           and the active <strong> render as the same shape. The base CSS
           sizes .paginate .on with a sprite background and fixed width
           which, when combined with naive padding, produces a giant green
           box with a tiny clipped "1". inline-flex with min-width + height
           gives a consistent pill regardless of base markup. */
        .paginate a,
        .paginate strong,
        .paginate .on,
        .paginate [aria-current="true"] {
            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;
            min-width: 28px !important;
            height: 28px !important;
            padding: 0 8px !important;
            margin: 0 2px !important;
            border-radius: 6px !important;
            box-sizing: border-box !important;
            background: none !important;
            background-image: none !important;
            text-indent: 0 !important;
            line-height: 1 !important;
            transition: background-color .15s ease, color .15s ease, box-shadow .15s ease !important;
        }
        .paginate a:hover {
            color: var(--wt-text) !important;
            background-color: var(--wt-bg-hover) !important;
            box-shadow: inset 0 0 0 1px var(--wt-accent-soft) !important;
        }
        .paginate .on,
        .paginate [aria-current="true"],
        .paginate strong {
            color: var(--wt-text-on-accent) !important;
            background-color: var(--wt-accent) !important;
        }
        /* The next/prev arrows are sprite-backed with a dark fill that
           disappears on the dark surface. Instead of filter-inverting (which
           also inverts the hover background, producing a stark white pill),
           kill the sprite and draw the chevron with text content. The inner
           icon span is hidden so we don't get a double glyph. */
        .paginate .pg_next, .paginate .pg_prev,
        .paginate a[class*="next"], .paginate a[class*="prev"] {
            background-image: none !important;
            text-indent: 0 !important;
            color: var(--wt-text) !important;
        }
        .paginate .pg_next > *, .paginate .pg_prev > *,
        .paginate [class*="ico_arr"] {
            display: none !important;
        }
        .paginate .pg_next::after, .paginate a[class*="next"]::after { content: '\\203A' !important; font-size: 18px !important; line-height: 1 !important; }
        .paginate .pg_prev::before, .paginate a[class*="prev"]::before { content: '\\2039' !important; font-size: 18px !important; line-height: 1 !important; }

        /* Viewer toolbar prev/next-episode buttons (.paginate.v2 around #N).
           Same class family as the bottom-of-list pager but rendered at
           toolbar scale. Three things needed:
           1. Size the buttons explicitly (the disabled .pg_next.dim is a
              <span>, not <a>, so it picks up no pill rule from the generic
              .paginate a rule — zero dimensions, chevron invisible).
           2. Match parent line-height to button height so the text "#N"
              and the buttons share the same line metrics — without this,
              vertical-align:middle still leaves the buttons below text.
           3. Replace the thin ‹ › guillemets the generic .paginate rule
              injects with the heavy chevron ornaments ❮ ❯ (same as the
              snb scroll arrows), and absolutely-position the pseudo
              inside the button — centering becomes bulletproof regardless
              of font metrics. Specificity beats the generic rule. */
        .paginate.v2 {
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            gap: 12px !important;
        }
        .paginate.v2 .pg_prev,
        .paginate.v2 .pg_next {
            display: inline-block !important;
            position: relative !important;
            width: 36px !important;
            min-width: 36px !important;
            height: 36px !important;
            padding: 0 !important;
            margin: 0 !important;
            border-radius: 6px !important;
            background: none !important;
            background-image: none !important;
            text-indent: 0 !important;
            color: var(--wt-text) !important;
            flex: 0 0 auto !important;
        }
        .paginate.v2 ._btnOpenEpisodeList,
        .paginate.v2 .tx {
            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;
            height: 36px !important;
            line-height: 1 !important;
            flex: 0 0 auto !important;
            /* Nudge text up to optically align with the chevron centers.
               Empirical offset — flex-centering the line-box doesn't match
               the digits' visual center because "#289" has no descenders. */
            transform: translateY(-5px) !important;
        }
        .paginate.v2 .pg_prev::before { content: '\\276E' !important; }
        .paginate.v2 .pg_next::after  { content: '\\276F' !important; }
        .paginate.v2 .pg_prev::before,
        .paginate.v2 .pg_next::after {
            position: absolute !important;
            inset: 0 !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            font-size: 28px !important;
            font-weight: normal !important;
            line-height: 1 !important;
            color: inherit !important;
        }
        .paginate.v2 a.pg_prev:hover,
        .paginate.v2 a.pg_next:hover {
            background-color: var(--wt-bg-hover) !important;
            color: var(--wt-accent) !important;
            box-shadow: inset 0 0 0 1px var(--wt-accent-soft) !important;
        }
        .paginate.v2 .pg_prev.dim,
        .paginate.v2 .pg_next.dim {
            opacity: 0.35 !important;
            cursor: default !important;
            pointer-events: none !important;
        }

        /* ---------- Static / policy pages ---------- */

        /* Notice list page (/<lang>/notice/list) — table on white. */
        .notice_area2 {
            background: var(--wt-bg) !important;
            color: var(--wt-text) !important;
        }
        .notice_area2 h3 { color: var(--wt-text) !important; }
        .notice_area2 .tb_notice { background: var(--wt-bg) !important; }
        .notice_area2 .tb_notice th {
            background: var(--wt-bg-elev2) !important;
            color: var(--wt-text) !important;
            border-color: var(--wt-border) !important;
        }
        .notice_area2 .tb_notice tbody tr           { color: var(--wt-text) !important; }
        .notice_area2 .tb_notice tbody tr.special   { background: var(--wt-bg-elev) !important; }
        .notice_area2 .tb_notice tbody td           { border-color: var(--wt-border) !important; }
        .notice_area2 .tb_notice a                  { color: var(--wt-text) !important; }
        .notice_area2 .tb_notice a:hover            { color: var(--wt-link) !important; }

        /* Terms / Privacy Policy pages (/<lang>/terms*, /<lang>/terms/privacyPolicy).
           Base CSS hard-codes background:#fff and color:#858585 on .terms_area. */
        .terms_area, .terms_box, .terms_card,
        .terms_lang_area, .terms_lang_desc, .terms_list {
            background: var(--wt-bg) !important;
            color: var(--wt-text) !important;
        }
        .terms_area h3, .terms_area strong { color: var(--wt-text) !important; }
        .terms_area .date                  { color: var(--wt-text-mute) !important; }
        .terms_area a                      { color: var(--wt-link) !important; }

        /* Terms / Policy language pills (English / Français / Indonesia / 中文 / ภาษาไทย).
           Base: inactive = #f3f3f3 bg + #666 text (invisible on dark).
                 active = #000 bg + #fff text (off-theme black pill). */
        .terms_lang_area .terms_lang_list .link,
        .terms_lang_area .terms_tab_list .link {
            background-color: var(--wt-bg-elev2) !important;
            color: var(--wt-text-dim) !important;
        }
        .terms_lang_area .terms_lang_list .link[aria-selected="true"],
        .terms_lang_area .terms_tab_list .link[aria-selected="true"] {
            background-color: var(--wt-accent) !important;
            color: var(--wt-text-on-accent) !important;
        }
        .terms_lang_area .terms_lang_list .link[aria-selected="false"]:hover,
        .terms_lang_area .terms_tab_list .link[aria-selected="false"]:hover {
            background-color: var(--wt-bg-hover) !important;
            color: var(--wt-text) !important;
        }

        /* Static Next.js subapp pages (About, Contact, Feedback, etc.).
           These are rendered by a separate bundle from /static/wec/.../next/...
           and use Tailwind utility classes (text-black, bg-white) that ignore
           the body color cascade. Scope overrides to the Next.js layout
           wrapper class so we don't catch other pages. */
        section[class*="layout_container"],
        section[class*="layout_container"] main {
            background-color: var(--wt-bg) !important;
            color: var(--wt-text) !important;
        }
        section[class*="layout_container"] .bg-white,
        section[class*="layout_container"] [class*="bg-white"],
        section[class*="layout_container"] [class*="bg-gray-50"],
        section[class*="layout_container"] [class*="bg-gray-100"],
        section[class*="layout_container"] [class*="bg-gray-200"] {
            background-color: var(--wt-bg-elev) !important;
        }
        section[class*="layout_container"] .text-black,
        section[class*="layout_container"] [class*="text-black"],
        section[class*="layout_container"] [class*="text-gray-9"],
        section[class*="layout_container"] [class*="text-gray-8"] {
            color: var(--wt-text) !important;
        }
        section[class*="layout_container"] [class*="text-gray-5"],
        section[class*="layout_container"] [class*="text-gray-6"],
        section[class*="layout_container"] [class*="text-gray-7"] {
            color: var(--wt-text-dim) !important;
        }

        /* Defensive catch-all for any other dialog Webtoons might add later. */
        [role="dialog"], [aria-modal="true"] {
            background-color: var(--wt-bg-elev) !important;
            color: var(--wt-text) !important;
        }
        [role="dialog"] input, [role="dialog"] textarea, [role="dialog"] select {
            background-color: var(--wt-bg-input) !important;
            color: var(--wt-text) !important;
            border: 1px solid var(--wt-border) !important;
        }
    `;

    /* Optional: dim panels in the viewer for late-night reading. */
    const dimCss = `
        .viewer_lst img, ._images img, .viewer_img img {
            filter: brightness(.78) !important;
        }
    `;

    function ensureStyle(id, css, on) {
        let el = document.getElementById(id);
        if (on) {
            if (!el) {
                el = document.createElement('style');
                el.id = id;
                el.textContent = css;
                (document.head || document.documentElement).appendChild(el);
            }
        } else if (el) {
            el.remove();
        }
    }

    // Set a hook on <html> so power users can write their own CSS like
    //     html[data-wt-dark="on"] .my-thing { ... }
    // and have it scoped to only fire when our theme is active.
    const applyTheme = (on) => {
        ensureStyle('wt-dark-style', palette + theme, on);
        document.documentElement.dataset.wtDark = on ? 'on' : 'off';
    };
    const applyDim = (on) => ensureStyle('wt-dim-style', dimCss, on);

    // First-run default follows the OS preference — once the user toggles, their
    // choice persists and OS changes are ignored.
    const themeDefault = !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);

    // Cached state — avoids GM IPC calls in the hot MutationObserver path.
    let darkOn = GM_getValue(KEY_THEME, themeDefault);
    let dimOn = GM_getValue(KEY_DIM, false);

    applyTheme(darkOn);
    applyDim(dimOn);

    // SPA / late-loading bundle defense: if our <style> ever gets removed
    // (Webtoons swaps stylesheets on some chapter transitions), put it back.
    // Cheap — only fires on direct childList changes to <head>.
    function watchHead() {
        if (!document.head) return;
        new MutationObserver(() => {
            if (darkOn && !document.getElementById('wt-dark-style')) applyTheme(true);
            if (dimOn && !document.getElementById('wt-dim-style')) applyDim(true);
            // NOTE: do NOT call syncViewerClass() here — head mutations fire during
            // SPA stylesheet swaps while the OLD page's #content.viewer is still in
            // the DOM, which would re-add wt-viewer right after pushState removed it.
            // scheduleViewerSync() on pushState handles re-adding the class when needed.
        }).observe(document.head, { childList: true });
    }
    if (document.head) watchHead();
    else document.addEventListener('DOMContentLoaded', watchHead, { once: true });

    function toggleTheme() {
        darkOn = !darkOn;
        GM_setValue(KEY_THEME, darkOn);
        applyTheme(darkOn);
        console.info('[webtoons-dark-mode] theme →', darkOn ? 'dark' : 'light');
    }
    function toggleDim() {
        dimOn = !dimOn;
        GM_setValue(KEY_DIM, dimOn);
        applyDim(dimOn);
        console.info('[webtoons-dark-mode] reader dim →', dimOn ? 'on' : 'off');
    }

    if (typeof GM_registerMenuCommand === 'function') {
        GM_registerMenuCommand('Toggle Webtoons dark mode', toggleTheme);
        GM_registerMenuCommand('Toggle reader dim', toggleDim);
    }

    // Keyboard shortcuts. Multiple combos so the user can use whichever doesn't
    // conflict with their OS / browser / keyboard-layout switcher:
    //   - Alt+Shift+T  OR  Ctrl+Alt+D       → toggle theme
    //   - Alt+Shift+N  OR  Ctrl+Alt+Shift+D → toggle reader dim
    // Note: bare Alt+D opens the address bar; Alt+Shift on Windows can also
    // trigger the input-language switcher, which can swallow Alt+Shift+T on
    // multi-language setups. The Ctrl+Alt+D backup avoids both.
    function matchCombo(e, want) {
        if (!!e.altKey !== want.alt) return false;
        if (!!e.shiftKey !== want.shift) return false;
        if (!!e.ctrlKey !== want.ctrl) return false;
        if (e.metaKey) return false; // never with Cmd
        const code = e.code;
        const key = (e.key || '').toUpperCase();
        return code === want.code || key === want.letter;
    }
    function handleKey(e) {
        const themeAltShiftT = matchCombo(e, { alt: true, shift: true, ctrl: false, code: 'KeyT', letter: 'T' });
        const themeCtrlAltD = matchCombo(e, { alt: true, shift: false, ctrl: true, code: 'KeyD', letter: 'D' });
        const dimAltShiftN = matchCombo(e, { alt: true, shift: true, ctrl: false, code: 'KeyN', letter: 'N' });
        const dimCtrlAltShD = matchCombo(e, { alt: true, shift: true, ctrl: true, code: 'KeyD', letter: 'D' });

        let handled = false;
        try {
            if (themeAltShiftT || themeCtrlAltD) {
                toggleTheme();
                handled = true;
            } else if (dimAltShiftN || dimCtrlAltShD) {
                toggleDim();
                handled = true;
            }
        } catch (err) {
            console.error('[webtoons-dark-mode] toggle failed:', err);
        }
        if (handled) {
            e.preventDefault();
            e.stopImmediatePropagation();
            e.stopPropagation();
        }
    }
    // Capture phase on window catches all keydowns before any page handler,
    // regardless of which element has focus.
    window.addEventListener('keydown', handleKey, true);

    // Sync vignette body classes for all three page types. JS is the only gate —
    // no CSS :has() fallbacks (they cause false positives during SPA transitions).
    function syncBodyClasses() {
        if (!document.body) return;
        const isViewer = !!document.querySelector('#content.viewer');
        // Detail page: series episode list (has the full-width artwork banner).
        const isDetail = !isViewer && !!document.querySelector('.detail_bg');
        // Home / genre listing pages: has the trending carousel or series grid,
        // but is not a detail or viewer page.
        const isHome = !isViewer && !isDetail && !!document.querySelector('.main_section, .webtoon_list_wrap');
        document.body.classList.toggle('wt-viewer', isViewer);
        document.body.classList.toggle('wt-detail', isDetail);
        document.body.classList.toggle('wt-home', isHome);
    }
    syncBodyClasses();
    document.addEventListener('DOMContentLoaded', syncBodyClasses);

    // SPA generation token: every navigation bumps _navGen so that timers
    // scheduled for the previous route early-out instead of mutating DOM that
    // belongs to a page the user already navigated away from.
    let _navGen = 0;
    function scheduleSpa(fn) {
        const gen = _navGen;
        SPA_RETRY_DELAYS.forEach(d => setTimeout(() => {
            if (gen !== _navGen) return;
            fn();
        }, d));
    }
    function scheduleViewerSync() { scheduleSpa(syncBodyClasses); }
    function scheduleViewerCards() { scheduleSpa(buildViewerCards); }
    function scheduleViewerBanners() { scheduleSpa(fixViewerBanners); }
    function scheduleCarouselArrows() { scheduleSpa(styleCarouselArrows); }

    function onSpaNav() {
        _navGen++;
        if (document.body) document.body.classList.remove('wt-viewer', 'wt-detail', 'wt-home');
        // Clear idempotency flag so the new page's sidebar gets re-wrapped.
        // The aside DOM node sometimes persists across viewer-to-viewer nav.
        const aside = document.querySelector('.aside.viewer');
        if (aside) delete aside.dataset.wtCards;
        scheduleViewerSync();
        scheduleViewerCards();
        scheduleViewerBanners();
        scheduleCarouselArrows();
    }
    ['pushState', 'replaceState'].forEach(fn => {
        const orig = history[fn];
        history[fn] = function () {
            orig.apply(this, arguments);
            onSpaNav();
        };
    });
    window.addEventListener('popstate', onSpaNav);

    // Inject centered chevron spans into the carousel prev/next buttons.
    // CSS pseudo-elements proved impossible to center reliably due to Webtoons'
    // own !important rules winning; inline styles on a real DOM node cannot be
    // overridden by any stylesheet.
    function styleCarouselArrows() {
        document.querySelectorAll(
            '.discover_spot .paging .btn_prev, .discover_spot .paging .btn_next'
        ).forEach(btn => {
            if (btn.dataset.wtArrow) return;
            btn.dataset.wtArrow = '1';
            const span = document.createElement('span');
            span.setAttribute('aria-hidden', 'true');
            span.style.cssText =
                'position:absolute;top:0;left:0;right:0;bottom:0;' +
                'display:flex;align-items:center;justify-content:center;' +
                'pointer-events:none;';
            // SVG chevron: pixel-perfect centering independent of font metrics.
            // btn_next has scaleX(-1) on the button which mirrors the SVG to face right.
            span.innerHTML =
                '<svg width="40" height="40" viewBox="0 0 24 24" fill="none"' +
                ' stroke="#e6e6e6" stroke-width="2.5"' +
                ' stroke-linecap="round" stroke-linejoin="round"' +
                ' style="display:block;filter:drop-shadow(0 0 6px rgba(255,255,255,.7))">' +
                '<polyline points="15,18 9,12 15,6"/></svg>';
            btn.appendChild(span);
        });
    }
    document.addEventListener('DOMContentLoaded', styleCarouselArrows);
    scheduleCarouselArrows();

    // Force uniform background throughout the viewer area. All blocks inside
    // #_viewerBox use var(--wt-bg) so there are no rogue gray/brownish strips.
    // Only the episode_area card and comic panel images are exempted.
    function fixViewerBanners() {
        const box = document.querySelector('#_viewerBox');
        if (!box) return;

        const clearBg = (el) => {
            el.style.setProperty('background-color', 'transparent', 'important');
            el.style.setProperty('color', 'var(--wt-text)', 'important');
        };

        // 1. Direct non-structural children of #_viewerBox
        for (const child of Array.from(box.children)) {
            if (child.matches('.aside, .aside.viewer, .comment_area')) continue;
            if (!child.matches('.viewer_lst')) clearBg(child);
        }

        // 2. viewer_lst children — transparent so the body gradient shows through.
        //    Only the comic panel wrapper is exempt (images must not be touched).
        const lst = box.querySelector('.viewer_lst');
        if (lst) {
            for (const child of Array.from(lst.children)) {
                if (child.matches('.viewer_img, ._img_viewer_area, #_imageList')) continue;
                clearBg(child);
                for (const gc of Array.from(child.children)) {
                    const gcBg = window.getComputedStyle(gc).backgroundColor;
                    if (gcBg && gcBg !== 'transparent' && gcBg !== 'rgba(0, 0, 0, 0)') {
                        gc.style.setProperty('background-color', 'transparent', 'important');
                    }
                }
            }
        }
    }
    document.addEventListener('DOMContentLoaded', fixViewerBanners);
    scheduleViewerBanners();

    // Inject card wrappers into the viewer sidebar. CSS selectors for inner
    // sections are unreliable (class names vary); JS groups children of
    // .ranking_lst into "header + following ULs until the next header" runs
    // and wraps each run in a card div. Idempotent via aside.dataset.wtCards;
    // the SPA nav handler clears that flag so each new page re-wraps.
    function buildViewerCards() {
        const aside = document.querySelector('.aside.viewer');
        if (!aside || aside.dataset.wtCards) return;
        const lst = aside.querySelector('.ranking_lst');
        if (!lst) return;

        const children = Array.from(lst.children);
        if (!children.length) return;

        // A new group starts on each non-UL header; ULs belong to the current
        // group. A leading UL with no preceding header gets its own group so
        // it isn't silently dropped.
        const groups = [];
        let cur = null;
        for (const el of children) {
            if (el.tagName !== 'UL' || !cur) {
                cur = [];
                groups.push(cur);
            }
            cur.push(el);
        }

        aside.dataset.wtCards = '1';

        if (groups.length < 2) {
            // Fallback: single card around the whole ranking_lst.
            lst.classList.add('wt-viewer-card');
            lst.style.flexDirection = '';
            lst.style.gap = '';
            return;
        }

        // Rebuild lst with each group wrapped in a card div.
        while (lst.firstChild) lst.removeChild(lst.firstChild);
        groups.forEach(group => {
            const card = document.createElement('div');
            card.className = 'wt-viewer-card';
            group.forEach(el => card.appendChild(el));
            lst.appendChild(card);
        });
    }
    document.addEventListener('DOMContentLoaded', buildViewerCards);
    scheduleViewerCards();

    console.info(`[webtoons-dark-mode] v${VERSION} fully loaded — Alt+Shift+T / Ctrl+Alt+D: theme | Alt+Shift+N / Ctrl+Alt+Shift+D: dim`);
})();