// ==UserScript== // @name Douban Plus // @namespace https://github.com/ZlatanCN/douban-plus // @version 1.5.0 // @author Gabriel Zhu // @description 适配 ScriptCat 和 Tampermonkey 的豆瓣作品详情页与人物页增强脚本,用 Preact 重排为 Apple TV 风格沉浸式暗色界面,并保留豆瓣原生登录、标记和跳转能力。 // @license MIT // @copyright Gabriel Zhu (MIT) // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICA8cmVjdCB4PSIzIiB5PSIzIiB3aWR0aD0iOTQiIGhlaWdodD0iOTQiIHJ4PSIyMCIgZmlsbD0iIzFjMWMxZSIvPgogIDxyZWN0IHg9IjE0IiB5PSIyNCIgd2lkdGg9IjcyIiBoZWlnaHQ9IjUyIiByeD0iNiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjEuOCIvPgogIDxjaXJjbGUgY3g9IjcyIiBjeT0iNjIiIHI9IjgiIGZpbGw9IiM0MmJkNTYiLz4KPC9zdmc+Cg== // @icon64 data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICA8cmVjdCB4PSIzIiB5PSIzIiB3aWR0aD0iOTQiIGhlaWdodD0iOTQiIHJ4PSIyMCIgZmlsbD0iIzFjMWMxZSIvPgogIDxyZWN0IHg9IjE0IiB5PSIyNCIgd2lkdGg9IjcyIiBoZWlnaHQ9IjUyIiByeD0iNiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjEuOCIvPgogIDxjaXJjbGUgY3g9IjcyIiBjeT0iNjIiIHI9IjgiIGZpbGw9IiM0MmJkNTYiLz4KPC9zdmc+Cg== // @homepageURL https://github.com/ZlatanCN/douban-plus // @supportURL https://github.com/ZlatanCN/douban-plus/issues // @include https://accounts.douban.com/passport/login* // @match *://movie.douban.com/subject/* // @match *://www.douban.com/personage/* // @match *://accounts.douban.com/passport/login* // @exclude *://movie.douban.com/subject/*/all_photos // @exclude *://movie.douban.com/subject/*/photos* // @exclude *://movie.douban.com/subject/*/photos[?]* // @exclude *://movie.douban.com/subject/*/comments* // @exclude *://movie.douban.com/subject/*/comments[?]* // @exclude *://movie.douban.com/subject/*/reviews* // @exclude *://movie.douban.com/subject/*/reviews[?]* // @tag douban // @tag 豆瓣 // @tag movie // @tag 电影 // @tag enhancement // @tag 增强 // @tag rating // @tag 评分 // @tag apple-tv // @tag dark-mode // @tag 暗色模式 // @tag redesign // @tag tampermonkey // @tag scriptcat // @tag userscript // @tag 油猴脚本 // @connect douban.com // @connect movie.douban.com // @connect graphql.imdb.com // @connect www.rottentomatoes.com // @connect www.metacritic.com // @grant GM_addStyle // @grant GM_xmlhttpRequest // @run-at document-start // @noframes // ==/UserScript== (function() { "use strict"; var s$2 = new Set(); var _css = async (t) => { if (s$2.has(t)) return; s$2.add(t); ((c) => { if (typeof GM_addStyle === "function") GM_addStyle(c); else (document.head || document.documentElement).appendChild(document.createElement("style")).append(c); })(t); }; var styles_default = "/* ATV stylesheet manifest.\n Keep this file as the single CSS entry imported from main.ts.\n Files are ordered to preserve the original cascade from the former giant stylesheet.\n Do not wrap these imports in @layer: this userscript runs inside Douban pages,\n and unlayered host author CSS would outrank layered ATV normal declarations. */\n\n:root {\n --atv-bg-primary: #0c0a09;\n --atv-bg-secondary: #1c1c1e;\n --atv-bg-tertiary: #2c2c2e;\n --atv-bg-elevated: rgb(255 255 255 / 6%);\n --atv-text-primary: #fff;\n --atv-text-secondary: rgb(255 255 255 / 72%);\n --atv-text-tertiary: rgb(255 255 255 / 45%);\n --atv-accent: #41be5d;\n --atv-accent-bright: #4cd97a;\n --atv-accent-glow: rgb(65 190 93 / 35%);\n --atv-rating-gold: #ffb800;\n --atv-border-subtle: rgb(255 255 255 / 8%);\n --atv-border-medium: rgb(255 255 255 / 16%);\n --atv-radius-sm: 8px;\n --atv-radius-md: 12px;\n --atv-radius-lg: 16px;\n --atv-radius-xl: 24px;\n --atv-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n --atv-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);\n --atv-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);\n --atv-duration-press: 160ms;\n --atv-duration-hover: 160ms;\n --atv-duration-feedback: 200ms;\n --atv-duration-content: 300ms;\n --atv-duration-overlay: 200ms;\n --atv-duration-modal-backdrop: 400ms;\n --atv-duration-modal-surface: 350ms;\n}\n\nbody.atv-enhanced > #wrapper {\n display: none !important;\n}\n\nbody.atv-enhanced {\n padding: 0 !important;\n margin: 0 !important;\n background: #000 !important;\n}\n\nbody.atv-enhanced #db-global-nav,\nbody.atv-enhanced #db-nav-movie,\nbody.atv-enhanced #db-nav-sns {\n display: none !important;\n}\n\nbody.atv-enhanced [id^=\"dale_\"],\nbody.atv-enhanced [class*=\"dale_\"] {\n display: none !important;\n}\n\n#atv-douban-root {\n position: relative;\n min-height: 100vh;\n animation: atv-fadein var(--atv-duration-feedback) var(--atv-ease-out)\n forwards;\n background: var(--atv-bg-primary);\n color: var(--atv-text-primary);\n font-family:\n -apple-system, BlinkMacSystemFont, \"SF Pro Display\", \"PingFang SC\",\n \"Helvetica Neue\", \"Microsoft YaHei\", Inter, system-ui, sans-serif;\n font-feature-settings: \"ss01\", \"cv11\";\n line-height: 1.5;\n opacity: 0;\n}\n\n@keyframes atv-fadein {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n#atv-douban-root *,\n#atv-douban-root *::before,\n#atv-douban-root *::after {\n box-sizing: border-box;\n}\n\n#atv-douban-root a {\n color: inherit;\n text-decoration: none;\n}\n\n#atv-douban-root a:hover {\n background: transparent;\n}\n\n#atv-douban-root img {\n display: block;\n max-width: 100%;\n}\n\n/* ---------- Sticky nav ---------- */\n\n.atv-stickynav {\n position: fixed;\n z-index: 9999;\n top: 0;\n right: 0;\n left: 0;\n display: flex;\n height: 56px;\n box-sizing: border-box;\n align-items: center;\n justify-content: space-between;\n padding: 0 max(28px, 5vw);\n border-bottom: 1px solid rgb(255 255 255 / 6%);\n background: rgb(10 10 12 / 95%);\n font-family:\n -apple-system, BlinkMacSystemFont, \"SF Pro Display\", \"PingFang SC\",\n \"Helvetica Neue\", \"Microsoft YaHei\", Inter, system-ui, sans-serif;\n gap: 24px;\n opacity: 0;\n pointer-events: none;\n transform: translateY(-100%);\n}\n\n/* Frosted glass effect only when NOT actively scrolling — Apple-style.\n The backdrop-filter forces per-frame compositing; dropping it during\n scroll eliminates the main source of scroll jank. */\n\n.atv-stickynav.is-visible:not(.is-scrolling) {\n -webkit-backdrop-filter: saturate(180%) blur(24px);\n backdrop-filter: saturate(180%) blur(24px);\n background: rgb(10 10 12 / 74%);\n}\n\n.atv-stickynav.is-visible {\n pointer-events: auto;\n}\n\n.atv-stickynav-title {\n overflow: hidden;\n min-width: 0;\n flex: 0 1 auto;\n color: #fff;\n font-size: 16px;\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.atv-stickynav-jumps {\n position: relative;\n display: flex;\n flex: 0 0 auto;\n gap: 24px;\n}\n\n.atv-stickynav-jumps a {\n position: relative;\n cursor: pointer;\n font-size: 14px;\n font-weight: 500;\n letter-spacing: 0.02em;\n transition: color var(--atv-duration-feedback) ease;\n white-space: nowrap;\n}\n\n#atv-douban-root .atv-stickynav-jumps a {\n color: rgb(255 255 255 / 70%);\n}\n\n#atv-douban-root .atv-stickynav-jumps a:hover {\n background: transparent;\n color: var(--atv-accent-bright);\n}\n\n#atv-douban-root .atv-stickynav-jumps a.is-active {\n color: var(--atv-accent-bright);\n}\n\n.atv-stickynav-marker {\n position: absolute;\n bottom: 0;\n left: 0;\n height: 2px;\n background: var(--atv-accent-bright);\n transform: translateX(0);\n transition: transform var(--atv-duration-feedback) var(--atv-ease-in-out);\n}\n\n@media (width <= 768px) {\n .atv-stickynav-title {\n font-size: 14px;\n }\n\n .atv-stickynav-jumps {\n gap: 14px;\n }\n\n .atv-stickynav-jumps a {\n font-size: 12px;\n }\n}\n\n/* The sliding active marker jumps to the active item without sliding\n motion under reduced-motion preference. */\n\n@media (prefers-reduced-motion: reduce) {\n .atv-stickynav-marker {\n transition: none;\n }\n}\n\n/* ---------- Subject switcher ---------- */\n\n.atv-stickynav-subject-switcher {\n position: relative;\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n.atv-subject-switcher-trigger,\n.atv-subject-switcher-close,\n.atv-subject-search-fallback,\n.atv-subject-suggestion {\n border: 0;\n appearance: none;\n color: inherit;\n cursor: pointer;\n font: inherit;\n}\n\n.atv-subject-switcher-trigger {\n display: inline-flex;\n height: 34px;\n align-items: center;\n padding: 0 13px;\n border: 1px solid var(--atv-border-subtle);\n border-radius: 999px;\n background: rgb(255 255 255 / 7%);\n color: var(--atv-text-secondary);\n font-size: 13px;\n font-weight: 600;\n gap: 7px;\n letter-spacing: 0.01em;\n transition:\n background var(--atv-duration-hover) ease,\n border-color var(--atv-duration-hover) ease,\n color var(--atv-duration-hover) ease;\n}\n\n.atv-subject-switcher-trigger:hover,\n.atv-subject-switcher-trigger:focus-visible {\n border-color: rgb(255 255 255 / 22%);\n background: rgb(255 255 255 / 12%);\n color: var(--atv-text-primary);\n outline: none;\n}\n\n.atv-subject-switcher-expanded {\n position: relative;\n display: flex;\n width: min(44vw, 520px);\n height: 38px;\n align-items: center;\n border: 1px solid rgb(255 255 255 / 22%);\n border-radius: 999px;\n background: rgb(255 255 255 / 10%);\n box-shadow: 0 12px 32px rgb(0 0 0 / 28%);\n}\n\n.atv-stickynav.has-subject-switcher-open .atv-stickynav-jumps {\n display: none;\n}\n\n.atv-stickynav.has-subject-switcher-open .atv-stickynav-subject-switcher {\n position: absolute;\n left: 50%;\n margin: 0;\n transform: translateX(-50%);\n}\n\n.atv-stickynav.has-subject-switcher-open .atv-subject-switcher-expanded {\n width: min(56vw, 620px);\n}\n\n.atv-subject-switcher-search-icon {\n display: inline-flex;\n flex: 0 0 auto;\n margin-left: 13px;\n color: var(--atv-text-tertiary);\n}\n\n.atv-subject-switcher-input {\n width: 100%;\n min-width: 0;\n height: 100%;\n padding: 0 8px;\n border: 0;\n background: transparent;\n color: var(--atv-text-primary);\n font: inherit;\n font-size: 14px;\n outline: 0;\n}\n\n.atv-subject-switcher-input::placeholder {\n color: var(--atv-text-tertiary);\n}\n\n.atv-subject-switcher-close {\n height: 24px;\n padding: 0 9px;\n border-radius: 999px;\n margin-right: 6px;\n background: rgb(255 255 255 / 10%);\n color: var(--atv-text-tertiary);\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.05em;\n}\n\n.atv-subject-switcher-close:hover,\n.atv-subject-switcher-close:focus-visible {\n background: rgb(255 255 255 / 18%);\n color: var(--atv-text-primary);\n outline: none;\n}\n\n@media (width <= 768px) {\n .atv-subject-switcher-expanded {\n width: min(78vw, 520px);\n }\n\n .atv-stickynav.has-subject-switcher-open .atv-stickynav-title {\n display: none;\n }\n\n .atv-stickynav.has-subject-switcher-open .atv-stickynav-subject-switcher {\n position: relative;\n left: auto;\n width: 100%;\n margin: 0;\n transform: none;\n }\n\n .atv-stickynav.has-subject-switcher-open .atv-subject-switcher-expanded {\n width: 100%;\n }\n}\n\n/* ---------- Subject suggestion results ---------- */\n\n.atv-subject-suggestion-rail {\n position: absolute;\n top: calc(100% + 10px);\n right: 0;\n overflow: hidden;\n width: 100%;\n border: 1px solid rgb(255 255 255 / 12%);\n border-radius: var(--atv-radius-md);\n background: rgb(21 21 23 / 96%);\n box-shadow: 0 24px 56px rgb(0 0 0 / 48%);\n}\n\n.atv-subject-suggestion {\n position: relative;\n display: flex;\n width: 100%;\n min-height: 70px;\n align-items: center;\n padding: 8px 18px 8px 9px;\n background: transparent;\n text-align: left;\n transition: background var(--atv-duration-hover) ease;\n}\n\n.atv-subject-suggestion + .atv-subject-suggestion {\n border-top: 1px solid rgb(255 255 255 / 7%);\n}\n\n.atv-subject-suggestion:hover,\n.atv-subject-suggestion.is-active {\n background: rgb(255 255 255 / 8%);\n}\n\n.atv-subject-suggestion:focus-visible {\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: -2px;\n}\n\n.atv-subject-suggestion-poster {\n position: relative;\n overflow: hidden;\n width: 36px;\n height: 54px;\n flex: 0 0 auto;\n border-radius: 4px;\n background: var(--atv-bg-tertiary);\n}\n\n.atv-subject-suggestion-poster img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.atv-subject-suggestion-copy {\n display: grid;\n min-width: 0;\n margin-left: 12px;\n gap: 4px;\n}\n\n.atv-subject-suggestion-title,\n.atv-subject-suggestion-original,\n.atv-subject-suggestion-metadata {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.atv-subject-suggestion-title {\n color: var(--atv-text-primary);\n font-size: 14px;\n font-weight: 650;\n letter-spacing: 0.01em;\n}\n\n.atv-subject-suggestion-metadata {\n margin-left: 7px;\n color: var(--atv-text-tertiary);\n font-size: 12px;\n font-weight: 500;\n}\n\n.atv-subject-suggestion-original {\n color: var(--atv-text-secondary);\n font-size: 12px;\n}\n\n.atv-subject-suggestion-marker {\n position: absolute;\n top: 11px;\n bottom: 11px;\n left: 52px;\n width: 2px;\n border-radius: 2px;\n background: var(--atv-accent-bright);\n box-shadow: 0 0 12px var(--atv-accent-glow);\n opacity: 0;\n transform: scaleY(0.3);\n transform-origin: center;\n transition:\n opacity var(--atv-duration-hover) ease,\n transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n.atv-subject-suggestion-rail.is-keyboard-navigating .atv-subject-suggestion,\n.atv-subject-suggestion-rail.is-keyboard-navigating\n .atv-subject-suggestion-marker {\n transition: none;\n}\n\n.atv-subject-suggestion.is-active .atv-subject-suggestion-marker {\n opacity: 1;\n transform: scaleY(1);\n}\n\n.atv-subject-suggestion-skeletons {\n display: grid;\n padding: 9px;\n gap: 7px;\n}\n\n.atv-subject-suggestion-skeletons span {\n display: block;\n height: 54px;\n border-radius: 6px;\n background: rgb(255 255 255 / 6%);\n}\n\n.atv-subject-search-fallback {\n display: block;\n width: 100%;\n padding: 15px 18px;\n background: transparent;\n color: var(--atv-text-secondary);\n font-size: 13px;\n text-align: left;\n}\n\n.atv-subject-search-fallback:hover,\n.atv-subject-search-fallback:focus-visible {\n background: rgb(255 255 255 / 8%);\n color: var(--atv-text-primary);\n outline: none;\n}\n\n@media (width <= 768px) {\n .atv-subject-switcher.is-open .atv-subject-suggestion-rail {\n right: -8px;\n width: calc(100vw - 24px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .atv-subject-suggestion-marker {\n transition: none;\n }\n}\n\n/* ---------- Hero ---------- */\n\n.atv-hero {\n position: relative;\n display: flex;\n overflow: visible;\n min-height: 75vh;\n flex-direction: column;\n padding: 132px max(28px, 5vw) 56px;\n isolation: isolate;\n}\n\n.atv-hero-inner-section {\n flex: 0 0 auto;\n}\n\n.atv-hero-bg {\n position: absolute;\n z-index: -4;\n top: 0;\n right: 0;\n left: 0;\n overflow: hidden;\n height: 75vh;\n background: #0c0a09;\n}\n\n.atv-hero-still {\n position: absolute;\n backface-visibility: hidden;\n background-position: center 30%;\n background-repeat: no-repeat;\n background-size: cover;\n inset: 0;\n transform: scale(1.04);\n}\n\n.atv-hero-still.is-thumb {\n filter: blur(12px) saturate(1.12) brightness(0.84);\n transform: scale(1.14);\n}\n\n.atv-hero-still.is-hd {\n filter: saturate(1.08) brightness(0.88);\n opacity: 0;\n transition: opacity var(--atv-duration-content) var(--atv-ease-out);\n}\n\n.atv-hero-still.is-hd.is-loaded {\n animation: atv-kenburns 22s linear forwards;\n opacity: 1;\n}\n\n.atv-hero-still.is-poster {\n background-position: center 22%;\n filter: blur(60px) saturate(1.25) brightness(0.78);\n transform: scale(1.25);\n}\n\n@keyframes atv-kenburns {\n from {\n transform: scale(1.04) translate(0, 0);\n }\n\n to {\n transform: scale(1.1) translate(-1.8%, -1.2%);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .atv-hero-still.is-hd {\n transition: opacity var(--atv-duration-feedback) ease;\n }\n\n .atv-hero-still.is-hd.is-loaded {\n animation: none;\n transform: scale(1.04);\n }\n}\n\n.atv-hero-vignette {\n position: absolute;\n z-index: -3;\n top: 0;\n right: 0;\n left: 0;\n height: 75vh;\n background: radial-gradient(\n 120% 90% at 70% 30%,\n transparent 0%,\n rgb(0 0 0 / 55%) 100%\n );\n}\n\n.atv-hero-overlay-x {\n position: absolute;\n z-index: -2;\n top: 0;\n right: 0;\n left: 0;\n height: 75vh;\n background: linear-gradient(\n to right,\n rgb(0 0 0 / 96%) 0%,\n rgb(0 0 0 / 82%) 32%,\n rgb(0 0 0 / 50%) 62%,\n rgb(0 0 0 / 35%) 100%\n );\n}\n\n.atv-hero-overlay-y {\n position: absolute;\n z-index: -1;\n top: 0;\n right: 0;\n left: 0;\n height: 75vh;\n background: linear-gradient(\n to bottom,\n rgb(0 0 0 / 45%) 0%,\n transparent 28%,\n transparent 55%,\n #0c0a09 100%\n );\n}\n\n.atv-hero-inner {\n display: flex;\n width: 100%;\n max-width: 1100px;\n align-items: flex-start;\n margin: 0 auto;\n gap: 64px;\n}\n\n.atv-poster-card {\n display: flex;\n overflow: hidden;\n width: 360px;\n flex: 0 0 auto;\n padding: 0;\n border: none;\n border-radius: var(--atv-radius-lg);\n appearance: none;\n aspect-ratio: 2 / 3;\n background: var(--atv-bg-tertiary);\n cursor: pointer;\n}\n\n.atv-poster-card img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.atv-poster-placeholder {\n width: 100%;\n height: 100%;\n}\n\n.atv-hero-info {\n min-width: 0;\n flex: 1 1 auto;\n}\n\n.atv-hero-title {\n margin: 0 0 8px;\n color: #fff;\n font-size: clamp(44px, 5.5vw, 80px);\n font-weight: 700;\n letter-spacing: clamp(-0.035em, -0.03em, -0.025em);\n line-height: 1;\n text-shadow: 0 4px 30px rgb(0 0 0 / 50%);\n}\n\n.atv-hero-orig {\n margin-bottom: 22px;\n color: var(--atv-text-secondary);\n font-size: clamp(18px, 1.6vw, 22px);\n font-weight: 300;\n letter-spacing: -0.01em;\n opacity: 0.85;\n}\n\n.atv-rank-label {\n display: inline-block;\n width: fit-content;\n max-width: 100%;\n margin: 0 0 25px;\n color: var(--atv-text-primary);\n text-decoration: none;\n}\n\n.atv-rank-label-entry {\n display: grid;\n min-height: 34px;\n align-items: center;\n padding: 5px 8px 5px 12px;\n border-left: 2px solid var(--atv-rating-gold);\n background: linear-gradient(\n 90deg,\n rgb(255 184 0 / 14%) 0%,\n rgb(255 184 0 / 0%) 86%\n );\n column-gap: 12px;\n grid-template-columns: auto minmax(0, 1fr) auto;\n}\n\n.atv-rank-label-entry strong {\n color: var(--atv-rating-gold);\n font-family: ui-monospace, SFMono-Regular, \"Cascadia Code\", monospace;\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n font-weight: 700;\n letter-spacing: -0.04em;\n white-space: nowrap;\n}\n\n.atv-rank-label-title {\n overflow: hidden;\n color: var(--atv-text-secondary);\n font-size: 14px;\n font-weight: 400;\n letter-spacing: -0.01em;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.atv-rank-label-arrow {\n color: var(--atv-text-tertiary);\n transition: transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n.atv-rank-label:hover,\n.atv-rank-label:focus-visible {\n outline: none;\n}\n\n.atv-rank-label:hover .atv-rank-label-entry,\n.atv-rank-label:focus-visible .atv-rank-label-entry {\n border-left-color: #ffd166;\n background: linear-gradient(\n 90deg,\n rgb(255 184 0 / 22%) 0%,\n rgb(255 184 0 / 0%) 86%\n );\n}\n\n@media (hover: hover) and (pointer: fine) {\n .atv-rank-label:hover .atv-rank-label-arrow {\n color: var(--atv-text-primary);\n transform: translateX(3px);\n }\n}\n\n@media (width <= 480px) {\n .atv-rank-label {\n margin-bottom: 21px;\n }\n\n .atv-rank-label-entry {\n max-width: min(100%, 340px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .atv-rank-label-arrow {\n transition: none;\n }\n\n .atv-rank-label:hover .atv-rank-label-arrow {\n transform: none;\n }\n}\n\n.atv-hero-meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n margin-bottom: 24px;\n color: var(--atv-text-secondary);\n font-size: 13px;\n font-weight: 500;\n gap: 10px 14px;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n\n.atv-first-broadcast-platform {\n display: inline-flex;\n align-items: center;\n}\n\n.atv-first-broadcast-platform-mark {\n display: inline-flex;\n width: 32px;\n height: 32px;\n align-items: center;\n justify-content: center;\n border: 1px solid rgb(255 255 255 / 13%);\n border-radius: 9px;\n background: rgb(255 255 255 / 8%);\n color: #fff;\n}\n\n.atv-first-broadcast-platform-mark.is-catalog svg {\n width: 22px;\n height: 22px;\n fill: currentcolor;\n}\n\n.atv-first-broadcast-platform-mark.is-intrinsic svg {\n width: 22px;\n height: 22px;\n fill: initial;\n}\n\n.atv-first-broadcast-platform-mark.is-wordmark {\n width: 68px;\n}\n\n.atv-first-broadcast-platform-mark.is-wordmark svg {\n width: 52px;\n height: 20px;\n}\n\n.atv-first-broadcast-platform-mark.is-surface-paper {\n border-color: rgb(255 255 255 / 34%);\n background: #d9dce3;\n box-shadow: inset 0 1px 0 rgb(255 255 255 / 56%);\n}\n\n.atv-first-broadcast-platform.is-unknown {\n color: var(--atv-text-tertiary);\n font-size: 11px;\n letter-spacing: 0.08em;\n text-transform: none;\n}\n\n.atv-screen-reader-only {\n position: absolute;\n overflow: hidden;\n width: 1px;\n height: 1px;\n padding: 0;\n border: 0;\n margin: -1px;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n.atv-meta-dot {\n display: inline-flex;\n align-items: center;\n}\n\n.atv-meta-dot + .atv-meta-dot::before {\n margin-right: 14px;\n color: var(--atv-text-tertiary);\n content: \"·\";\n}\n\n.atv-meta-chips {\n display: inline-flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.atv-chip {\n display: inline-flex;\n align-items: center;\n padding: 4px 11px;\n border: 1px solid var(--atv-border-subtle);\n border-radius: 999px;\n background: var(--atv-bg-elevated);\n color: var(--atv-text-secondary);\n font-size: 12px;\n font-weight: 500;\n letter-spacing: 0.02em;\n text-transform: none;\n}\n\n/* ---------- Personage hero ---------- */\n\n.atv-personage-hero {\n position: relative;\n min-height: 70vh;\n padding: clamp(96px, 13vw, 152px) max(28px, 7vw) 80px;\n}\n\n.atv-personage-hero::after {\n position: absolute;\n right: max(28px, 7vw);\n bottom: 0;\n left: max(28px, 7vw);\n height: 1px;\n background: linear-gradient(\n to right,\n transparent,\n var(--atv-border-subtle) 20%,\n var(--atv-border-subtle) 80%,\n transparent\n );\n content: \"\";\n pointer-events: none;\n}\n\n.atv-personage-hero-inner {\n display: grid;\n width: min(100%, 1120px);\n align-items: start;\n margin: 0 auto;\n gap: clamp(32px, 5vw, 72px);\n grid-template-columns: minmax(160px, 240px) minmax(0, 1fr);\n}\n\n.atv-personage-portrait {\n width: 100%;\n border-radius: 50%;\n aspect-ratio: 1;\n background: var(--atv-bg-tertiary);\n object-fit: cover;\n}\n\n.atv-personage-portrait.is-empty {\n background: radial-gradient(\n circle at 30% 25%,\n #4a4a4d,\n var(--atv-bg-tertiary) 62%\n );\n}\n\n.atv-personage-portrait-trigger {\n display: block;\n width: 100%;\n padding: 0;\n border: 0;\n border-radius: 50%;\n margin: 0;\n appearance: none;\n background: transparent;\n cursor: pointer;\n}\n\n.atv-personage-kicker {\n margin: 2px 0 16px;\n color: var(--atv-accent-bright);\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n}\n\n.atv-personage-identity h1 {\n margin: 0;\n color: var(--atv-text-primary);\n font-size: clamp(44px, 7vw, 84px);\n letter-spacing: -0.04em;\n line-height: 0.98;\n}\n\n.atv-personage-original-name {\n margin: 18px 0 0;\n color: var(--atv-text-secondary);\n font-size: clamp(19px, 2.1vw, 28px);\n font-weight: 500;\n letter-spacing: 0.02em;\n line-height: 1.2;\n}\n\n.atv-personage-facts {\n display: flex;\n flex-wrap: wrap;\n margin: 32px 0 0;\n gap: 12px 32px;\n}\n\n.atv-personage-facts div {\n display: flex;\n gap: 8px;\n}\n\n.atv-personage-facts dt {\n color: var(--atv-text-tertiary);\n font-size: 12px;\n letter-spacing: 0.03em;\n opacity: 0.7;\n}\n\n.atv-personage-facts dd {\n margin: 0;\n color: var(--atv-text-secondary);\n font-size: 14px;\n font-weight: 600;\n letter-spacing: 0.01em;\n}\n\n.atv-personage-biography {\n max-width: 680px;\n margin-top: 36px;\n}\n\n.atv-personage-biography-content {\n color: var(--atv-text-secondary);\n font-size: 16px;\n letter-spacing: 0.01em;\n line-height: 1.8;\n}\n\n.atv-personage-biography-content p {\n margin: 0;\n}\n\n.atv-personage-biography-content p + p {\n margin-top: 1em;\n}\n\n.atv-personage-biography-content.is-clamped > p:not(:first-child) {\n display: none;\n}\n\n.atv-personage-biography-content.is-clamped > p:first-child {\n display: -webkit-box;\n overflow: hidden;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n}\n\n.atv-personage-biography button {\n padding: 0;\n border: 0;\n margin-top: 12px;\n appearance: none;\n background: transparent;\n color: var(--atv-accent-bright);\n cursor: pointer;\n font: inherit;\n transition: opacity 160ms ease;\n}\n\n/* ---------- Entrance ---------- */\n\n@keyframes atv-hero-fade-in {\n from {\n opacity: 0;\n transform: translateY(12px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes atv-hero-portrait-in {\n from {\n opacity: 0;\n transform: scale(0.92);\n }\n\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n.atv-personage-hero.is-revealed .atv-personage-portrait {\n animation: atv-hero-portrait-in 500ms var(--atv-ease-out) both;\n animation-delay: 80ms;\n}\n\n.atv-personage-hero.is-revealed .atv-personage-identity h1 {\n animation: atv-hero-fade-in 500ms var(--atv-ease-out) both;\n animation-delay: 180ms;\n}\n\n.atv-personage-hero.is-revealed .atv-personage-kicker {\n animation: atv-hero-fade-in 500ms var(--atv-ease-out) both;\n animation-delay: 260ms;\n}\n\n.atv-personage-hero.is-revealed .atv-personage-original-name {\n animation: atv-hero-fade-in 500ms var(--atv-ease-out) both;\n animation-delay: 260ms;\n}\n\n.atv-personage-hero.is-revealed .atv-personage-facts div {\n animation: atv-hero-fade-in 500ms var(--atv-ease-out) both;\n animation-delay: calc(340ms + var(--fact-index, 0) * 60ms);\n}\n\n.atv-personage-hero.is-revealed .atv-personage-biography {\n animation: atv-hero-fade-in 500ms var(--atv-ease-out) both;\n animation-delay: 500ms;\n}\n\n/* ---------- Hover ---------- */\n\n@media (hover: hover) and (pointer: fine) {\n .atv-personage-biography button:hover {\n opacity: 0.8;\n }\n}\n\n/* ---------- Collaborators ---------- */\n\n.atv-personage-collaborators {\n display: flex;\n flex-direction: column;\n padding: 0;\n margin: 20px 0 0;\n list-style: none;\n}\n\n.atv-personage-collaborator {\n position: relative;\n display: grid;\n overflow: hidden;\n align-items: center;\n padding: 14px 12px;\n border-radius: var(--atv-radius-sm, 8px);\n gap: 14px;\n grid-template-columns: 28px 48px 1fr auto;\n transition: background 160ms ease;\n}\n\n.atv-section-reveal.is-revealed .atv-personage-collaborator {\n animation: atv-collaborator-in 200ms var(--atv-ease-out) both;\n animation-delay: min(calc(var(--stagger-index, 0) * 40ms + 20ms), 100ms);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .atv-personage-collaborator:hover {\n background: var(--atv-bg-secondary);\n }\n\n .atv-personage-collaborator::before {\n position: absolute;\n top: 14px;\n bottom: 14px;\n left: 0;\n width: 3px;\n border-radius: 0 3px 3px 0;\n background: var(--atv-accent);\n content: \"\";\n opacity: 0;\n transform: scaleY(0.35);\n transition:\n opacity var(--atv-duration-feedback) ease,\n transform var(--atv-duration-hover) var(--atv-ease-out);\n }\n\n .atv-personage-collaborator:hover::before {\n opacity: 1;\n transform: scaleY(1);\n }\n}\n\n.atv-personage-collaborator:active {\n transform: scale(0.98);\n}\n\n@keyframes atv-collaborator-in {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.atv-personage-collaborator-rank {\n display: flex;\n justify-content: flex-end;\n color: var(--atv-text-tertiary);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n font-weight: 600;\n letter-spacing: 0.02em;\n}\n\n.atv-personage-collaborator-avatar {\n width: 48px;\n height: 48px;\n border: 1px solid var(--atv-border-subtle);\n border-radius: 50%;\n background: var(--atv-bg-tertiary);\n object-fit: cover;\n transition: border-color 160ms ease;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .atv-personage-collaborator:hover .atv-personage-collaborator-avatar {\n border-color: var(--atv-accent-bright);\n }\n}\n\n.atv-personage-collaborator-avatar.is-fallback {\n display: grid;\n color: var(--atv-text-primary);\n font-size: 18px;\n font-weight: 600;\n place-items: center;\n}\n\n.atv-personage-collaborator-info {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 2px;\n}\n\n.atv-personage-collaborator-name {\n overflow: hidden;\n color: var(--atv-text-primary);\n font-size: 15px;\n font-weight: 650;\n letter-spacing: -0.01em;\n text-decoration: none;\n text-overflow: ellipsis;\n transition: color 160ms ease;\n white-space: nowrap;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .atv-personage-collaborator-name:hover {\n color: var(--atv-accent-bright);\n }\n}\n\n.atv-personage-collaborator-count {\n color: var(--atv-text-tertiary);\n font-size: 11px;\n font-weight: 400;\n}\n\n.atv-personage-collaborator-action {\n flex-shrink: 0;\n color: var(--atv-text-secondary);\n font-size: 12px;\n font-weight: 600;\n text-decoration: none;\n transition:\n color 160ms ease,\n opacity 200ms var(--atv-ease-out),\n transform 200ms var(--atv-ease-out);\n white-space: nowrap;\n}\n\n.atv-personage-collaborator-arrow {\n display: inline-block;\n transition:\n opacity var(--atv-duration-feedback) ease,\n transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .atv-personage-collaborator-action {\n opacity: 0;\n pointer-events: none;\n transform: translateX(8px);\n }\n\n .atv-personage-collaborator-arrow {\n opacity: 0.35;\n transform: translate(1px, -2px);\n }\n\n .atv-personage-collaborator:hover .atv-personage-collaborator-action {\n opacity: 1;\n pointer-events: auto;\n transform: translateX(0);\n }\n\n .atv-personage-collaborator:hover .atv-personage-collaborator-arrow {\n opacity: 1;\n transform: translate(0, 0);\n }\n\n .atv-personage-collaborator-action:hover {\n color: var(--atv-accent-bright);\n }\n}\n\n.atv-personage-collaborator-action:focus-visible {\n border-radius: 2px;\n color: var(--atv-accent-bright);\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: 3px;\n}\n\n.atv-personage .atv-section {\n padding: 36px 0;\n}\n\n.atv-personage .atv-section + .atv-section {\n padding-top: 0;\n}\n\n.atv-personage .atv-section-h {\n padding-left: 0;\n margin-bottom: 28px;\n color: var(--atv-text-secondary);\n font-size: 20px;\n font-weight: 600;\n letter-spacing: 0.02em;\n}\n\n.atv-personage .atv-section-h::before {\n display: none;\n}\n\n.atv-personage .atv-section-h-row {\n margin-bottom: 28px;\n}\n\n.atv-personage-gallery-rail {\n gap: 12px;\n list-style: none;\n}\n\n@keyframes atv-gallery-tile-in {\n from {\n opacity: 0;\n transform: translateY(6px) scale(0.98);\n }\n\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.atv-personage-gallery-rail li {\n min-width: 0;\n height: 256px;\n flex: 0 0 auto;\n aspect-ratio: var(--atv-personage-gallery-aspect-ratio, 4 / 3);\n scroll-snap-align: start;\n}\n\n.atv-section-reveal.is-revealed .atv-personage-gallery-rail li {\n animation: atv-gallery-tile-in 200ms var(--atv-ease-out) both;\n animation-delay: min(calc(var(--stagger-index, 0) * 40ms + 20ms), 100ms);\n}\n\n.atv-personage-gallery-rail button {\n display: block;\n overflow: hidden;\n width: 100%;\n min-width: 0;\n height: 100%;\n padding: 0;\n border: 0;\n border-radius: var(--atv-radius-sm);\n appearance: none;\n background: var(--atv-bg-tertiary);\n cursor: pointer;\n}\n\n.atv-personage-gallery-rail img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n.atv-personage-gallery-empty {\n padding: 24px;\n border-radius: var(--atv-radius-sm);\n margin: 0;\n background: var(--atv-bg-secondary);\n color: var(--atv-text-tertiary);\n}\n\n/* ---------- WorkRail (代表作品) ---------- */\n\n.atv-personage-work-rail {\n display: flex;\n flex-wrap: nowrap;\n gap: 20px;\n}\n\n.atv-personage-work-card {\n min-width: 140px;\n flex: 1 1 0;\n color: inherit;\n cursor: pointer;\n opacity: 0;\n text-decoration: none;\n transform: translateY(8px);\n transition:\n opacity 400ms var(--atv-ease-out) 200ms,\n transform 400ms var(--atv-ease-out) 200ms;\n}\n\n.atv-section-reveal.is-revealed .atv-personage-work-card {\n opacity: 1;\n transform: translateY(0);\n}\n\n.atv-personage-work-card:focus-visible {\n border-radius: var(--atv-radius-sm);\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: 4px;\n}\n\n.atv-personage-work-poster {\n position: relative;\n overflow: hidden;\n width: 100%;\n border-radius: var(--atv-radius-sm);\n aspect-ratio: 2 / 3;\n background: var(--atv-bg-tertiary);\n}\n\n.atv-personage-work-poster img,\n.atv-personage-work-poster .atv-poster-placeholder {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n/* Index number badge — 1‑based order on the card poster */\n\n.atv-work-index {\n position: absolute;\n z-index: 1;\n bottom: 0;\n left: 0;\n display: flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n border-radius: 0 4px 4px 0;\n backdrop-filter: blur(2px);\n background: rgb(0 0 0 / 60%);\n color: #fff;\n font-family:\n \"SF Pro Display\",\n -apple-system,\n BlinkMacSystemFont,\n system-ui,\n sans-serif;\n font-size: 13px;\n font-weight: 700;\n letter-spacing: 0.01em;\n line-height: 1;\n}\n\n/* Rating badge — pill badge on the card poster */\n\n.atv-work-rating {\n position: absolute;\n z-index: 1;\n top: 8px;\n right: 8px;\n display: flex;\n min-width: 28px;\n height: 22px;\n align-items: center;\n justify-content: center;\n padding: 0 7px;\n border-radius: 20px;\n backdrop-filter: blur(4px);\n background: rgb(0 0 0 / 65%);\n color: #fff;\n font-family:\n \"SF Pro Display\",\n -apple-system,\n BlinkMacSystemFont,\n system-ui,\n sans-serif;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.02em;\n line-height: 1;\n}\n\n.atv-personage-work-title {\n display: block;\n overflow: hidden;\n margin-top: 10px;\n color: var(--atv-text-primary);\n font-size: 14px;\n font-weight: 600;\n letter-spacing: 0.01em;\n line-height: 1.3;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.atv-personage-work-year {\n display: block;\n margin-top: 4px;\n color: var(--atv-text-tertiary);\n font-size: 12px;\n font-weight: 500;\n}\n\n/* ---------- Timeline (Axis + Dots) ---------- */\n\n/* Vertical axis line — accent at top fades into the dark background toward\n the past, so newer years read as more present. */\n\n.atv-timeline {\n position: relative;\n margin-top: 20px;\n font-size: clamp(28px, 3.5vw, 40px);\n}\n\n.atv-timeline::before {\n position: absolute;\n z-index: 0;\n top: calc(0.5em);\n bottom: 10px;\n left: 15px;\n width: 1px;\n background: linear-gradient(\n to bottom,\n var(--atv-accent-bright) 0%,\n var(--atv-accent) 35%,\n var(--atv-border-subtle) 100%\n );\n content: \"\";\n pointer-events: none;\n}\n\n.atv-timeline-year-group {\n position: relative;\n padding-bottom: 24px;\n padding-left: 44px;\n font-size: clamp(28px, 3.5vw, 40px);\n}\n\n.atv-timeline-year-group:last-child {\n padding-bottom: 0;\n}\n\n.atv-timeline-year-group::before {\n position: absolute;\n z-index: 1;\n top: calc(0.5em - 5px);\n left: 10px;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: var(--atv-accent-bright);\n box-shadow: 0 0 0 transparent;\n content: \"\";\n opacity: 0;\n transform: scale(0.5);\n transition:\n opacity 400ms var(--atv-ease-out),\n transform 400ms var(--atv-ease-out),\n box-shadow 400ms var(--atv-ease-out);\n transition-delay: calc(var(--group-index, 0) * 60ms);\n}\n\n.atv-section-reveal.is-revealed .atv-timeline-year-group::before {\n box-shadow:\n 0 0 10px var(--atv-accent),\n 0 0 24px color-mix(in srgb, var(--atv-accent) 25%, transparent);\n opacity: 1;\n transform: scale(1);\n}\n\n.atv-timeline-year-num {\n display: block;\n margin-bottom: 16px;\n color: var(--atv-text-primary);\n font-size: clamp(28px, 3.5vw, 40px);\n font-weight: 700;\n letter-spacing: -0.02em;\n line-height: 1;\n}\n\n.atv-section-reveal.is-revealed .atv-timeline-year-num {\n animation: atv-timeline-year-in 200ms var(--atv-ease-out) both;\n animation-delay: min(calc(var(--group-index, 0) * 40ms + 20ms), 100ms);\n}\n\n@keyframes atv-timeline-year-in {\n from {\n opacity: 0;\n transform: translateY(4px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Card row */\n\n.atv-timeline-row {\n display: flex;\n flex-wrap: wrap;\n gap: 16px;\n}\n\n@keyframes atv-timeline-card-in {\n from {\n opacity: 0;\n transform: translateY(8px) scale(0.97);\n }\n\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.atv-timeline-card {\n min-width: 120px;\n max-width: 200px;\n flex: 1 1 160px;\n color: inherit;\n cursor: pointer;\n text-decoration: none;\n transition: transform var(--atv-duration-hover) var(--atv-ease-out);\n will-change: transform;\n}\n\n.atv-section-reveal.is-revealed .atv-timeline-card {\n animation: atv-timeline-card-in 200ms var(--atv-ease-out) both;\n animation-delay: min(\n calc(var(--stagger-index, 0) * 40ms + var(--group-index, 0) * 40ms + 20ms),\n 100ms\n );\n}\n\n.atv-timeline-card:active {\n transform: scale(0.97);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .atv-timeline-card:hover {\n transform: translateY(-3px);\n }\n}\n\n.atv-timeline-card:focus-visible {\n border-radius: var(--atv-radius-sm);\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: 4px;\n}\n\n.atv-timeline-card-poster {\n position: relative;\n overflow: hidden;\n width: 100%;\n border-radius: var(--atv-radius-sm);\n aspect-ratio: 2 / 3;\n background: var(--atv-bg-tertiary);\n}\n\n.atv-timeline-card-poster img,\n.atv-timeline-card-poster .atv-poster-placeholder {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.atv-timeline-card-rating {\n position: absolute;\n z-index: 1;\n top: 8px;\n right: 8px;\n display: flex;\n min-width: 28px;\n height: 22px;\n align-items: center;\n justify-content: center;\n padding: 0 7px;\n border-radius: 20px;\n backdrop-filter: blur(4px);\n background: rgb(0 0 0 / 65%);\n color: #fff;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.02em;\n line-height: 1;\n}\n\n.atv-timeline-card-title {\n display: block;\n overflow: hidden;\n margin-top: 10px;\n color: var(--atv-text-secondary);\n font-size: 14px;\n font-weight: 600;\n letter-spacing: 0.01em;\n line-height: 1.3;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.atv-personage-awards-list {\n padding: 0;\n margin: 0;\n list-style: none;\n}\n\n.atv-personage-awards-list li {\n display: grid;\n padding: 20px 0;\n border-bottom: 1px solid var(--atv-border-subtle);\n gap: 24px;\n grid-template-columns: minmax(84px, auto) 1fr;\n}\n\n.atv-personage-awards-list li:last-child {\n border-bottom: none;\n}\n\n.atv-personage-award-year {\n padding-top: 4px;\n color: var(--atv-text-tertiary);\n font-size: 26px;\n font-weight: 300;\n letter-spacing: 0.02em;\n line-height: 1;\n}\n\n.atv-personage-award-detail {\n position: relative;\n min-width: 0;\n padding-left: 16px;\n border-left: 2px solid var(--atv-accent);\n}\n\n.atv-personage-award-ceremony,\n.atv-personage-award-ceremony a {\n color: var(--atv-text-tertiary);\n font-size: 11px;\n font-weight: 600;\n}\n\n.atv-personage-award-ceremony a {\n border-bottom: 1px solid transparent;\n transition:\n color var(--atv-duration-feedback) ease,\n border-color var(--atv-duration-feedback) ease;\n}\n\n.atv-personage-award-ceremony a:hover {\n border-bottom-color: var(--atv-accent-bright);\n color: var(--atv-accent-bright);\n}\n\n.atv-personage-award-name {\n margin: 6px 0 0;\n color: var(--atv-text-primary);\n font-size: 17px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.atv-personage-award-work {\n margin: 6px 0 0;\n color: var(--atv-text-tertiary);\n font-size: 14px;\n}\n\n.atv-personage-award-work a {\n border-bottom: 1px solid transparent;\n color: inherit;\n transition:\n color var(--atv-duration-feedback) ease,\n border-color var(--atv-duration-feedback) ease;\n}\n\n.atv-personage-award-work a:hover {\n border-bottom-color: var(--atv-accent-bright);\n color: var(--atv-accent-bright);\n}\n\n.atv-personage-awards-empty {\n padding: 24px 0;\n margin: 0;\n color: var(--atv-text-tertiary);\n}\n\n@media (width <= 640px) {\n .atv-personage-hero-bg-image {\n filter: blur(40px) saturate(1.25) brightness(0.5);\n }\n\n .atv-personage-hero-bg {\n height: 60vh;\n }\n\n .atv-personage-hero-inner {\n gap: 24px;\n grid-template-columns: 112px minmax(0, 1fr);\n }\n\n .atv-personage-identity h1 {\n font-size: clamp(34px, 10vw, 50px);\n }\n\n .atv-personage-facts {\n margin-top: 20px;\n }\n\n .atv-personage-biography {\n margin-top: 0;\n grid-column: 1 / -1;\n }\n\n .atv-personage-collaborator {\n padding: 14px 8px;\n gap: 10px;\n grid-template-columns: 20px 42px 1fr auto;\n }\n\n .atv-personage-collaborator-avatar,\n .atv-personage-collaborator-avatar.is-fallback {\n width: 42px;\n height: 42px;\n }\n\n .atv-personage-gallery-rail li {\n height: 160px;\n }\n\n .atv-personage-awards-list li {\n padding: 16px 0;\n gap: 10px;\n grid-template-columns: 1fr;\n }\n\n .atv-personage-award-year {\n padding-top: 0;\n font-size: 14px;\n font-weight: 600;\n }\n\n .atv-personage-award-detail {\n padding-left: 12px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .atv-personage-hero .atv-personage-portrait,\n .atv-personage-hero .atv-personage-identity h1,\n .atv-personage-hero .atv-personage-original-name,\n .atv-personage-hero .atv-personage-facts div,\n .atv-personage-hero .atv-personage-biography {\n animation: none;\n opacity: 1;\n transform: none;\n }\n\n .atv-personage-collaborator {\n animation: none;\n opacity: 1;\n transform: none;\n }\n\n .atv-personage-collaborator:active {\n transform: none;\n }\n\n .atv-personage-gallery-rail li {\n animation: none;\n opacity: 1;\n transform: none;\n }\n\n /* Timeline: dots always visible, no entrance animations */\n .atv-timeline-year-group::before {\n opacity: 1;\n transform: scale(1);\n transition: none;\n }\n\n .atv-section-reveal.is-revealed .atv-timeline-year-num {\n animation: none;\n opacity: 1;\n }\n\n .atv-personage-work-card,\n .atv-section-reveal.is-revealed .atv-timeline-card {\n animation: none;\n opacity: 1;\n transform: none;\n }\n\n .atv-personage-work-card,\n .atv-timeline-card {\n transition: none;\n will-change: auto;\n }\n}\n\n/* Shared SafeImage implementation styles.\n Page modules own crop, geometry, and motion around this primitive. */\n\n.atv-safe-image-container {\n width: 100%;\n height: 100%;\n}\n\n.atv-safe-image-container > img {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.atv-safe-image-fallback {\n width: 100%;\n height: 100%;\n min-height: 48px;\n background: var(--atv-bg-tertiary);\n}\n\n/* ── Ratings Panel ──────────────────────────────── */\n\n/* Unified card presenting Douban + IMDb + RT side by side */\n\n.atv-rating-panel {\n display: flex;\n width: fit-content;\n min-width: 320px;\n flex-wrap: wrap;\n align-items: stretch;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: var(--atv-radius-md);\n margin-bottom: 28px;\n gap: 0;\n}\n\n.atv-rating-panel-douban,\n.atv-rating-panel-imdb,\n.atv-rating-panel-rt,\n.atv-rating-panel-mc {\n display: grid;\n flex: 1;\n padding: 16px 24px 14px;\n grid-template-rows: 28px 44px 20px 1fr;\n place-items: center center;\n text-align: center;\n transition: background var(--atv-duration-feedback) ease;\n}\n\n.atv-rating-panel-douban:hover,\n.atv-rating-panel-imdb:hover,\n.atv-rating-panel-rt:hover,\n.atv-rating-panel-mc:hover {\n background: rgb(255 255 255 / 3%);\n}\n\n.atv-rating-panel-douban {\n border-right: 1px solid rgb(255 255 255 / 6%);\n}\n\n.atv-rating-panel-imdb {\n border-right: 1px solid rgb(255 255 255 / 6%);\n}\n\n.atv-rating-panel-mc {\n border-right: 1px solid rgb(255 255 255 / 6%);\n}\n\n.atv-rating-panel-logo {\n display: inline-flex;\n align-items: center;\n align-self: center;\n opacity: 0.85;\n transition: opacity var(--atv-duration-feedback) ease;\n}\n\n.atv-rating-panel-logo:hover {\n opacity: 1;\n}\n\n.atv-rating-panel-logo svg {\n display: block;\n}\n\n.atv-rating-panel .atv-rating-panel-score {\n color: var(--atv-text-primary);\n font-family:\n \"SF Pro Display\",\n -apple-system,\n BlinkMacSystemFont,\n system-ui,\n sans-serif;\n font-size: 38px;\n font-weight: 700;\n letter-spacing: -0.03em;\n line-height: 1;\n}\n\n/* ── MC Score color by range ────────────────────── */\n\n.atv-rating-panel-score.is-high {\n color: #3bb33b;\n}\n\n.atv-rating-panel-score.is-medium {\n color: #ffb800;\n}\n\n.atv-rating-panel-score.is-low {\n color: #fa320a;\n}\n\n/* ── MC label row (score bar + Chinese word label) ── */\n\n.atv-mc-label-row {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.atv-mc-bar-track {\n display: inline-block;\n overflow: hidden;\n width: 40px;\n height: 4px;\n flex-shrink: 0;\n border-radius: 2px;\n background: rgb(255 255 255 / 10%);\n}\n\n.atv-mc-bar-fill {\n display: block;\n height: 100%;\n border-radius: 2px;\n}\n\n.atv-mc-bar-fill.is-high {\n background: #3bb33b;\n}\n\n.atv-mc-bar-fill.is-medium {\n background: #ffb800;\n}\n\n.atv-mc-bar-fill.is-low {\n background: #fa320a;\n}\n\n.atv-mc-word-label {\n color: var(--atv-text-tertiary);\n font-size: 11px;\n font-weight: 500;\n letter-spacing: 0.02em;\n line-height: 1;\n white-space: nowrap;\n}\n\n.atv-rating-stars {\n display: inline-flex;\n color: var(--atv-rating-gold);\n gap: 2px;\n}\n\n.atv-rating-stars svg {\n display: block;\n}\n\n/* ── RT Score Row (values side by side) ─────────── */\n\n.atv-rt-score-row,\n.atv-rt-label-row,\n.atv-rt-count-row {\n display: grid;\n width: 100%;\n align-items: center;\n gap: 8px;\n grid-template-columns: 1fr auto 1fr;\n}\n\n.atv-rt-score-value {\n font-family:\n \"SF Pro Display\",\n -apple-system,\n BlinkMacSystemFont,\n system-ui,\n sans-serif;\n font-size: 30px;\n font-weight: 700;\n letter-spacing: -0.03em;\n line-height: 1;\n}\n\n.atv-rt-score-value.is-fresh {\n color: var(--atv-text-primary);\n}\n\n.atv-rt-score-row > .atv-rt-score-value.is-rotten:first-child {\n color: rgb(255 107 91 / 85%);\n}\n\n.atv-rt-score-row > .atv-rt-score-value.is-rotten:last-child {\n color: rgb(255 167 38 / 85%);\n}\n\n.atv-rt-score-row > .atv-rt-score-value:first-child {\n justify-self: center;\n text-align: right;\n}\n\n.atv-rt-score-row > .atv-rt-score-value:last-child {\n justify-self: center;\n text-align: left;\n}\n\n/* ── RT Label Row (icons + text) ────────────────── */\n\n.atv-rt-label-row {\n gap: 8px;\n grid-template-columns: 1fr 1fr;\n}\n\n.atv-rt-label-item {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n}\n\n.atv-rt-label-row > .atv-rt-label-item:first-child {\n justify-self: center;\n}\n\n.atv-rt-label-row > .atv-rt-label-item:last-child {\n justify-self: center;\n}\n\n.atv-rt-score-icon {\n display: inline-flex;\n width: 16px;\n height: 16px;\n color: var(--atv-text-tertiary);\n opacity: 0.7;\n}\n\n.atv-rt-score-icon svg {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.atv-rt-label-item.is-critics.is-fresh .atv-rt-score-icon {\n color: #ff6b5b;\n opacity: 1;\n}\n\n.atv-rt-label-item.is-critics.is-rotten .atv-rt-score-icon {\n color: #50b85e;\n opacity: 0.6;\n}\n\n.atv-rt-label-item.is-audience.is-fresh .atv-rt-score-icon {\n color: #ffb800;\n opacity: 1;\n}\n\n.atv-rt-label-item.is-audience.is-rotten .atv-rt-score-icon {\n color: #888;\n opacity: 0.6;\n}\n\n.atv-rt-score-label {\n color: var(--atv-text-tertiary);\n font-size: 11px;\n font-weight: 500;\n letter-spacing: 0.02em;\n white-space: nowrap;\n}\n\n/* ── Shared count text (Douban / IMDb) ──────────── */\n\n.atv-rating-panel-count {\n color: var(--atv-text-tertiary);\n font-size: 12px;\n font-weight: 500;\n letter-spacing: 0.02em;\n white-space: nowrap;\n}\n\n/* ── RT Count Row (e.g. 评价 300 | 50,000) ──────── */\n\n.atv-rt-count-row {\n gap: 8px;\n grid-template-columns: 1fr 1fr;\n}\n\n.atv-rt-count-value {\n color: var(--atv-text-tertiary);\n font-size: 12px;\n font-weight: 500;\n letter-spacing: 0.02em;\n white-space: nowrap;\n}\n\n.atv-rt-count-row > .atv-rt-count-value:first-child {\n justify-self: center;\n}\n\n.atv-rt-count-row > .atv-rt-count-value:last-child {\n justify-self: center;\n}\n\n/* ── RT Divider ─────────────────────────────────── */\n\n.atv-rt-divider {\n width: 1px;\n flex-shrink: 0;\n align-self: stretch;\n margin: 2px 0;\n background: rgb(255 255 255 / 12%);\n}\n\n@media (width <= 768px) {\n .atv-rating-panel .atv-rating-panel-score {\n font-size: 32px;\n }\n\n .atv-rating-panel-douban,\n .atv-rating-panel-imdb,\n .atv-rating-panel-rt,\n .atv-rating-panel-mc {\n padding: 12px 14px 10px;\n grid-template-rows: 24px 38px 18px 1fr;\n }\n\n .atv-rt-score-value {\n font-size: 26px;\n }\n\n .atv-rt-score-icon {\n width: 14px;\n height: 14px;\n }\n\n .atv-rt-score-label {\n font-size: 10px;\n }\n\n .atv-mc-bar-track {\n width: 32px;\n }\n\n .atv-mc-word-label {\n font-size: 10px;\n }\n\n .atv-rating-panel {\n min-width: 0;\n }\n}\n\n.atv-rating-empty {\n padding: 10px 0;\n color: var(--atv-text-tertiary);\n font-size: 13px;\n letter-spacing: 0.03em;\n}\n\n/* ── Skeleton (loading) ─────────────────────────── */\n\n.atv-rating-panel-skeleton {\n width: 120px;\n height: 22px;\n border-radius: 4px;\n background: rgb(255 255 255 / 6%);\n}\n\n.atv-actions {\n display: flex;\n flex-wrap: wrap;\n margin-bottom: 26px;\n gap: 12px;\n}\n\n.atv-btn {\n display: inline-flex;\n height: 44px;\n align-items: center;\n padding: 0 22px;\n border: none;\n border-radius: 999px;\n appearance: none;\n background: none;\n color: inherit;\n cursor: pointer;\n font-family: inherit;\n font-size: 14px;\n font-weight: 600;\n gap: 8px;\n letter-spacing: 0.01em;\n -webkit-tap-highlight-color: transparent;\n transition:\n transform var(--atv-duration-press) var(--atv-ease-out),\n background var(--atv-duration-feedback) ease,\n color var(--atv-duration-feedback) ease;\n}\n\n.atv-btn:active {\n transform: scale(0.97);\n}\n\n.atv-btn-primary {\n background: var(--atv-accent);\n box-shadow: 0 8px 24px var(--atv-accent-glow);\n color: #fff;\n}\n\n.atv-btn-secondary {\n border: 1px solid var(--atv-border-medium);\n backdrop-filter: blur(10px);\n background: var(--atv-bg-elevated);\n color: var(--atv-text-primary);\n}\n\n.atv-btn.is-active {\n border-color: transparent;\n background: var(--atv-accent);\n box-shadow: 0 6px 20px var(--atv-accent-glow);\n color: #fff;\n}\n\n.atv-hero-summary {\n margin-top: 16px;\n}\n\n.atv-hero-teaser {\n display: -webkit-box;\n overflow: hidden;\n max-width: 660px;\n margin: 0 0 12px;\n -webkit-box-orient: vertical;\n color: var(--atv-text-secondary);\n font-size: 15px;\n line-height: 1.75;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.atv-hero-teaser.is-clamped {\n -webkit-line-clamp: 3;\n}\n\n.atv-hero-more {\n display: inline-flex;\n align-items: center;\n padding: 0;\n border: none;\n appearance: none;\n background: none;\n color: var(--atv-accent-bright);\n cursor: pointer;\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n gap: 6px;\n letter-spacing: 0.02em;\n}\n\n.atv-hero-more:hover {\n color: var(--atv-accent);\n}\n\n.atv-hero-more svg {\n transition: transform var(--atv-duration-hover) var(--atv-ease-in-out);\n}\n\n.atv-hero-more.is-open svg {\n transform: rotate(180deg);\n}\n\n/* ---------- Section ---------- */\n\n.atv-section {\n max-width: 1280px;\n padding: 52px max(28px, 5vw);\n margin: 0 auto;\n contain-intrinsic-size: auto 400px;\n content-visibility: auto;\n scroll-margin-top: 64px;\n}\n\n.atv-section + .atv-section {\n padding-top: 0;\n}\n\n.atv-section-h {\n position: relative;\n display: flex;\n align-items: center;\n padding-left: 14px;\n margin: 0 0 24px;\n font-size: 24px;\n font-weight: 700;\n letter-spacing: -0.02em;\n}\n\n.atv-section-h::before {\n position: absolute;\n top: 50%;\n left: 0;\n width: 3px;\n height: 20px;\n border-radius: 2px;\n background: var(--atv-accent);\n content: \"\";\n transform: translateY(-50%);\n}\n\n.atv-section-h-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n margin-bottom: 24px;\n gap: 16px;\n}\n\n.atv-section-h-row .atv-section-h {\n margin-bottom: 0;\n}\n\n.atv-section-more {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n color: var(--atv-text-tertiary);\n font-size: 14px;\n font-weight: 500;\n gap: 4px;\n letter-spacing: 0.02em;\n transition:\n color var(--atv-duration-feedback) ease,\n transform var(--atv-duration-hover) var(--atv-ease-out);\n white-space: nowrap;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .atv-section-more:hover {\n color: var(--atv-accent-bright);\n transform: translateX(3px);\n }\n}\n\n.atv-section-more:focus-visible {\n color: var(--atv-accent-bright);\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: 3px;\n}\n\n/* ---------- Section reveal on scroll ---------- */\n\n/* Below-the-fold sections start offset and fade in once they enter the\n viewport. The motion here is opacity + a small translateY only. */\n\n.atv-section-reveal {\n opacity: 0;\n transform: translateY(12px);\n}\n\n.atv-section-reveal.is-revealed {\n opacity: 1;\n transform: none;\n transition:\n opacity var(--atv-duration-content) var(--atv-ease-out),\n transform var(--atv-duration-content) var(--atv-ease-out);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .atv-section-reveal,\n .atv-section-reveal.is-revealed {\n opacity: 1;\n transform: none;\n transition: none;\n }\n}\n\n/* ---------- Carousel ---------- */\n\n.atv-carousel {\n display: flex;\n padding: 4px 4px 16px;\n margin: 0 -4px;\n contain: paint layout;\n gap: 16px;\n overflow-x: auto;\n scroll-behavior: smooth;\n scrollbar-width: none;\n}\n\n.atv-carousel::-webkit-scrollbar {\n display: none;\n}\n\n/* ---------- Page scrollbar ---------- */\n\n:root {\n color-scheme: dark;\n}\n\n::-webkit-scrollbar {\n width: 5px;\n height: 5px;\n}\n\n::-webkit-scrollbar-track {\n background: #1c1c1e;\n}\n\n::-webkit-scrollbar-thumb {\n border-radius: 3px;\n background: #3a3a3c;\n}\n\n::-webkit-scrollbar-thumb:hover {\n background: #48484a;\n}\n\n::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n* {\n scrollbar-color: #3a3a3c #1c1c1e;\n scrollbar-width: thin;\n}\n\n/* ---------- Series ---------- */\n\n.atv-series-card {\n min-width: 0;\n flex: 0 0 158px;\n cursor: pointer;\n scroll-snap-align: start;\n transition: transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n/* ── Active season (user is currently on this season's page) ── */\n\n.atv-series-card.is-active .atv-series-poster {\n box-shadow:\n inset 0 0 0 2px var(--atv-accent),\n 0 0 20px var(--atv-accent-glow);\n}\n\n.atv-series-card.is-active .atv-series-info::after {\n width: 20px;\n opacity: 1;\n}\n\n/* ── Poster ── */\n\n.atv-series-poster {\n position: relative;\n overflow: hidden;\n width: 100%;\n border-radius: var(--atv-radius-sm);\n aspect-ratio: 2 / 3;\n background: var(--atv-bg-tertiary);\n}\n\n/* Poster bottom gradient overlay (Apple TV+ style depth) */\n\n.atv-series-poster::after {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n height: 45%;\n background: linear-gradient(to top, rgb(0 0 0 / 55%) 0%, transparent 100%);\n content: \"\";\n pointer-events: none;\n}\n\n.atv-series-poster img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n/* ── Rating badge (overlay on poster top-right) ── */\n\n.atv-series-badge {\n position: absolute;\n z-index: 1;\n top: 8px;\n right: 8px;\n display: flex;\n min-width: 28px;\n height: 22px;\n align-items: center;\n justify-content: center;\n padding: 0 7px;\n border-radius: 20px;\n backdrop-filter: blur(4px);\n background: rgb(0 0 0 / 65%);\n color: #fff;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.02em;\n line-height: 1;\n}\n\n/* ── Info row ── */\n\n.atv-series-info {\n position: relative;\n margin-top: 10px;\n}\n\n.atv-series-info::after {\n display: block;\n width: 100%;\n height: 2px;\n border-radius: 1px;\n margin-top: 4px;\n background: var(--atv-accent);\n content: \"\";\n opacity: 0;\n transform: scaleX(0);\n transform-origin: left;\n transition: opacity var(--atv-duration-hover) ease;\n}\n\n.atv-series-title {\n display: block;\n overflow: hidden;\n color: var(--atv-text-primary);\n font-size: 14px;\n font-weight: 600;\n letter-spacing: 0.01em;\n line-height: 1.3;\n text-overflow: ellipsis;\n transition: color var(--atv-duration-feedback) ease;\n white-space: nowrap;\n}\n\n/* ---------- Cast ---------- */\n\n.atv-cast-card {\n min-width: 0;\n flex: 0 0 160px;\n cursor: pointer;\n scroll-snap-align: start;\n text-align: center;\n transition: transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n.atv-cast-avatar {\n width: 160px;\n height: 160px;\n border: 2px solid transparent;\n border-radius: 50%;\n background-color: var(--atv-bg-tertiary);\n background-position: center top;\n background-size: cover;\n}\n\n.atv-cast-name {\n overflow: hidden;\n margin-top: 16px;\n color: var(--atv-text-primary);\n font-size: 16px;\n font-weight: 600;\n line-height: 1.3;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.atv-cast-role {\n display: -webkit-box;\n overflow: hidden;\n margin-top: 4px;\n -webkit-box-orient: vertical;\n color: rgb(255 255 255 / 55%);\n font-size: 13px;\n font-weight: 400;\n -webkit-line-clamp: 2;\n line-height: 1.35;\n}\n\n/* ---------- Photos ---------- */\n\n.atv-photos {\n gap: 12px;\n}\n\n@keyframes atv-photo-in {\n from {\n opacity: 0;\n transform: translateY(6px) scale(0.98);\n }\n\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.atv-photo-tile {\n display: flex;\n overflow: hidden;\n height: 225px;\n flex: 0 0 auto;\n padding: 0;\n border: none;\n border-radius: var(--atv-radius-md);\n appearance: none;\n aspect-ratio: var(--atv-photo-aspect-ratio, 16 / 9);\n background: var(--atv-bg-tertiary);\n cursor: pointer;\n scroll-snap-align: start;\n}\n\n.atv-photo-tile-content {\n display: block;\n width: 100%;\n height: 100%;\n animation: atv-photo-in 400ms var(--atv-ease-out) both;\n animation-delay: calc(var(--stagger-index, 0) * 60ms + 100ms);\n}\n\n.atv-photo-tile img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n.atv-photo-rail-reserve {\n height: 225px;\n flex: 0 0 100%;\n}\n\n/* ---------- Recommendations ---------- */\n\n.atv-recs {\n display: grid;\n gap: 24px;\n grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));\n}\n\n.atv-rec-card {\n cursor: pointer;\n}\n\n.atv-rec-poster {\n overflow: hidden;\n width: 100%;\n border-radius: var(--atv-radius-sm);\n aspect-ratio: 2 / 3;\n background: var(--atv-bg-tertiary);\n transition: transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n.atv-rec-poster img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .atv-series-card:hover .atv-series-title {\n color: var(--atv-accent-bright);\n }\n}\n\n.atv-rec-title {\n display: -webkit-box;\n overflow: hidden;\n margin-top: 12px;\n -webkit-box-orient: vertical;\n color: var(--atv-text-primary);\n font-size: 15px;\n font-weight: 600;\n -webkit-line-clamp: 2;\n text-overflow: ellipsis;\n}\n\n/* ---------- Info grid ---------- */\n\n.atv-info-grid {\n display: grid;\n gap: 20px 48px;\n grid-template-columns: 140px 1fr;\n}\n\n/* Each row = label + value. We alternate even/odd children.\n The grid children are label/value siblings, so row pairs are (4n+1, 4n+2).\n Stagger: each row enters 24ms after the previous one, capped at 120ms.\n The animation itself is handled by the section-reveal —\n this just adds per-row delay on top. */\n\n.atv-section-reveal.is-revealed .atv-info-grid > * {\n animation: atv-info-row-enter 180ms var(--atv-ease-out) both;\n}\n\n.atv-info-grid > :nth-child(1),\n.atv-info-grid > :nth-child(2) {\n animation-delay: 0ms;\n}\n\n.atv-info-grid > :nth-child(3),\n.atv-info-grid > :nth-child(4) {\n animation-delay: 24ms;\n}\n\n.atv-info-grid > :nth-child(5),\n.atv-info-grid > :nth-child(6) {\n animation-delay: 48ms;\n}\n\n.atv-info-grid > :nth-child(7),\n.atv-info-grid > :nth-child(8) {\n animation-delay: 72ms;\n}\n\n.atv-info-grid > :nth-child(9),\n.atv-info-grid > :nth-child(10) {\n animation-delay: 96ms;\n}\n\n.atv-info-grid > :nth-child(11),\n.atv-info-grid > :nth-child(12) {\n animation-delay: 120ms;\n}\n\n.atv-info-grid > :nth-child(13),\n.atv-info-grid > :nth-child(14) {\n animation-delay: 120ms;\n}\n\n.atv-info-grid > :nth-child(15),\n.atv-info-grid > :nth-child(16) {\n animation-delay: 120ms;\n}\n\n.atv-info-grid > :nth-child(17),\n.atv-info-grid > :nth-child(18) {\n animation-delay: 120ms;\n}\n\n.atv-info-grid > :nth-child(19),\n.atv-info-grid > :nth-child(20) {\n animation-delay: 120ms;\n}\n\n@keyframes atv-info-row-enter {\n from {\n opacity: 0;\n transform: translateY(6px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* ---------- Labels ---------- */\n\n.atv-info-label {\n padding-top: 3px;\n color: rgb(255 255 255 / 40%);\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n}\n\n/* ---------- Values ---------- */\n\n.atv-info-value {\n color: rgb(255 255 255 / 82%);\n font-size: 15px;\n font-weight: 400;\n line-height: 1.65;\n overflow-wrap: break-word;\n}\n\n/* Links inside values — subtle underline that glows on hover */\n\n.atv-info-value a {\n border-bottom: 1px solid rgb(255 255 255 / 12%);\n color: inherit;\n text-decoration: none;\n transition:\n border-color var(--atv-duration-feedback) ease,\n color var(--atv-duration-feedback) ease;\n}\n\n.atv-info-value a:hover {\n border-color: var(--atv-accent-bright);\n color: var(--atv-accent-bright);\n}\n\n/* Separator between linked items: a thin middle-dot */\n\n.atv-info-sep {\n margin: 0 2px;\n color: rgb(255 255 255 / 18%);\n}\n\n/* ---------- Awards (gold accent row) ---------- */\n\n.atv-info-label-award {\n color: var(--atv-rating-gold);\n}\n\n.atv-info-label-award a {\n border-bottom: 1px solid transparent;\n color: inherit;\n text-decoration: none;\n transition: border-color var(--atv-duration-feedback) ease;\n}\n\n.atv-info-label-award a:hover {\n border-color: var(--atv-rating-gold);\n}\n\n/* ---------- Reduced motion ---------- */\n\n@media (prefers-reduced-motion: reduce) {\n .atv-section-reveal.is-revealed .atv-info-grid > * {\n animation: none;\n opacity: 1;\n transform: none;\n }\n}\n\n/* ---------- Mobile (<=768px) ---------- */\n\n@media (width <= 768px) {\n .atv-info-grid {\n gap: 14px 0;\n grid-template-columns: 1fr;\n }\n\n .atv-info-label {\n padding-top: 0;\n }\n\n .atv-section-reveal.is-revealed .atv-info-grid > * {\n animation: none;\n opacity: 1;\n transform: none;\n }\n}\n\n/* ---------- Streaming ---------- */\n\n.atv-stream-row {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n}\n\n.atv-stream-card {\n --atv-stream-brand: var(--atv-accent-bright);\n\n position: relative;\n display: inline-flex;\n max-width: 240px;\n height: 52px;\n align-items: center;\n padding: 0 20px 0 14px;\n border: 1px solid var(--atv-border-medium);\n border-radius: var(--atv-radius-md);\n background: var(--atv-bg-elevated);\n color: var(--atv-text-primary);\n cursor: pointer;\n font-family: inherit;\n font-size: 20px;\n font-weight: 600;\n gap: 11px;\n letter-spacing: 0.01em;\n -webkit-tap-highlight-color: transparent;\n transition:\n background var(--atv-duration-feedback) ease,\n border-color var(--atv-duration-feedback) ease,\n transform var(--atv-duration-press) var(--atv-ease-out);\n}\n\n/* Center-glow overlay — subtle brand bloom from card center */\n\n.atv-stream-card::after {\n position: absolute;\n border-radius: inherit;\n background: radial-gradient(\n circle at center,\n var(--atv-stream-brand, var(--atv-accent-bright)) 0%,\n transparent 70%\n );\n content: \"\";\n inset: 0;\n opacity: 0;\n pointer-events: none;\n transition: opacity var(--atv-duration-feedback) ease;\n}\n\n/* ── Combined SVG cards ── */\n\n.atv-stream-card-combined {\n height: 52px;\n padding: 0 16px;\n gap: 0;\n}\n\n.atv-stream-card-combined svg {\n display: block;\n width: auto;\n max-width: 160px;\n height: 32px;\n}\n\n.atv-stream-logo.is-catalog svg {\n display: block;\n width: 18px;\n height: 18px;\n fill: currentcolor;\n}\n\n.atv-stream-logo.is-intrinsic svg {\n display: block;\n width: 18px;\n height: 18px;\n fill: initial;\n}\n\n.atv-stream-vendor-icon {\n display: block;\n width: 32px;\n height: 32px;\n flex: 0 0 auto;\n object-fit: contain;\n}\n\n.atv-stream-logo {\n display: inline-flex;\n overflow: hidden;\n width: 32px;\n height: 32px;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: 9px;\n background:\n linear-gradient(180deg, rgb(255 255 255 / 12%), rgb(255 255 255 / 0%)),\n rgb(255 255 255 / 5%);\n box-shadow:\n inset 0 1px 0 rgb(255 255 255 / 12%),\n 0 8px 18px rgb(0 0 0 / 18%);\n color: var(--atv-stream-brand, var(--atv-accent-bright));\n}\n\n.atv-stream-logo.is-surface-paper {\n border-color: rgb(255 255 255 / 34%);\n background: #d9dce3;\n box-shadow:\n inset 0 1px 0 rgb(255 255 255 / 56%),\n 0 8px 18px rgb(0 0 0 / 18%);\n}\n\n.atv-stream-logo-fallback {\n display: inline-flex;\n width: 100%;\n height: 100%;\n align-items: center;\n justify-content: center;\n color: currentcolor;\n font-size: 15px;\n font-weight: 800;\n line-height: 1;\n text-transform: uppercase;\n}\n\n.atv-stream-name {\n overflow: hidden;\n min-width: 0;\n text-overflow: ellipsis;\n transition: color var(--atv-duration-feedback) ease;\n white-space: nowrap;\n}\n\n.atv-stream-card:focus-visible {\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: 3px;\n}\n\n/* ---------- Comments ---------- */\n\n.atv-comments {\n display: grid;\n gap: 16px;\n grid-template-columns: repeat(2, 1fr);\n}\n\n.atv-comment-card {\n display: flex;\n flex-direction: column;\n padding: 16px 20px;\n border: none;\n border-radius: var(--atv-radius-sm);\n background: #121214;\n transition:\n transform var(--atv-duration-hover) var(--atv-ease-out),\n background var(--atv-duration-hover) ease;\n}\n\n.atv-comment-top {\n display: flex;\n align-items: center;\n margin-bottom: 14px;\n gap: 12px;\n}\n\n.atv-comment-avatar {\n display: flex;\n overflow: hidden;\n width: 36px;\n height: 36px;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n border: 1px solid rgb(255 255 255 / 6%);\n border-radius: 50%;\n background-color: var(--atv-accent);\n background-position: center;\n background-size: cover;\n color: #fff;\n font-size: 15px;\n font-weight: 600;\n}\n\n.atv-comment-meta {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 6px;\n}\n\n.atv-comment-author,\na.atv-comment-author {\n overflow: hidden;\n color: var(--atv-text-primary);\n font-size: 14px;\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\na.atv-comment-author:hover {\n background: none;\n color: var(--atv-text-primary);\n}\n\n.atv-comment-stars {\n display: inline-flex;\n align-items: center;\n color: var(--atv-rating-gold);\n gap: 2px;\n}\n\n.atv-comment-stars svg {\n display: block;\n width: 14px;\n height: 14px;\n}\n\n.atv-comment-body {\n position: relative;\n display: flex;\n width: 100%;\n flex: 1 1 auto;\n flex-direction: column;\n padding: 0;\n border: none;\n margin: 0 0 14px;\n appearance: none;\n background: none;\n color: rgb(255 255 255 / 85%);\n cursor: pointer;\n font: inherit;\n font-size: 15px;\n line-height: 1.65;\n overflow-wrap: break-word;\n text-align: left;\n}\n\n.atv-comment-body-text {\n display: -webkit-box;\n overflow: hidden;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 4;\n overflow-wrap: break-word;\n}\n\n.atv-comment-foot {\n display: flex;\n align-items: center;\n justify-content: space-between;\n color: var(--atv-text-tertiary);\n font-size: 12px;\n font-weight: 500;\n gap: 12px;\n letter-spacing: 0.02em;\n}\n\n.atv-comment-votes {\n display: inline-flex;\n align-items: center;\n padding: 4px 10px;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: 8px;\n background: none;\n color: var(--atv-text-tertiary);\n cursor: pointer;\n font-family: inherit;\n font-size: 12px;\n font-weight: 500;\n gap: 5px;\n letter-spacing: 0.02em;\n transition:\n color var(--atv-duration-feedback) ease,\n border-color var(--atv-duration-feedback) ease,\n background var(--atv-duration-feedback) ease,\n transform var(--atv-duration-press) var(--atv-ease-out);\n}\n\n.atv-comment-votes:hover {\n border-color: rgb(65 190 93 / 20%);\n background: rgb(65 190 93 / 4%);\n color: var(--atv-accent);\n}\n\n.atv-comment-votes:focus-visible {\n outline: 2px solid rgb(65 190 93 / 75%);\n outline-offset: 3px;\n}\n\n.atv-comment-votes:active {\n transform: scale(0.97);\n}\n\n.atv-comment-votes.is-voted {\n border-color: rgb(65 190 93 / 25%);\n background: rgb(65 190 93 / 6%);\n color: var(--atv-accent);\n}\n\n.atv-comment-votes svg {\n display: block;\n width: 13px;\n height: 13px;\n}\n\n.atv-vote-count {\n font-variant-numeric: tabular-nums;\n}\n\n/* ---------- Comment Expand Overlay ---------- */\n\n.atv-comment-foot-right {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.atv-comment-expand {\n display: none;\n width: 24px;\n height: 24px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 1px solid rgb(255 255 255 / 6%);\n border-radius: 6px;\n appearance: none;\n backdrop-filter: blur(4px);\n background: rgb(0 0 0 / 30%);\n color: var(--atv-text-tertiary);\n cursor: pointer;\n font: inherit;\n transition:\n color var(--atv-duration-feedback) ease,\n border-color var(--atv-duration-feedback) ease,\n background var(--atv-duration-feedback) ease,\n transform var(--atv-duration-press) var(--atv-ease-out);\n}\n\n.atv-comment-card.has-overflow .atv-comment-expand {\n display: inline-flex;\n}\n\n.atv-comment-expand:hover {\n border-color: var(--atv-accent);\n background: rgb(65 190 93 / 10%);\n color: var(--atv-accent);\n}\n\n.atv-comment-expand:active {\n transform: scale(0.97);\n}\n\n.atv-comment-expand svg {\n display: block;\n width: 12px;\n height: 12px;\n}\n\n/* ── Overlay ── */\n\n.atv-comment-overlay {\n position: fixed;\n z-index: 10000;\n display: flex;\n width: 100vw;\n height: 100vh;\n align-items: center;\n justify-content: center;\n padding: 24px;\n border: none;\n margin: 0;\n background: rgb(0 0 0 / 72%);\n inset: 0;\n}\n\n.atv-comment-overlay-inner {\n position: relative;\n width: 100%;\n max-width: 580px;\n max-height: 80vh;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: 20px;\n background: #121214;\n box-shadow: 0 24px 80px rgb(0 0 0 / 55%);\n overflow-y: auto;\n}\n\n.atv-modal-close.atv-comment-overlay-close {\n position: absolute;\n z-index: 2;\n top: 16px;\n right: 16px;\n width: 32px;\n height: 32px;\n border-color: rgb(255 255 255 / 10%);\n background: rgb(255 255 255 / 6%);\n color: rgb(255 255 255 / 60%);\n}\n\n.atv-modal-close.atv-comment-overlay-close:hover {\n border-color: rgb(255 255 255 / 20%);\n background: rgb(255 255 255 / 12%);\n color: #fff;\n}\n\n.atv-modal-close.atv-comment-overlay-close svg {\n width: 16px;\n height: 16px;\n}\n\n.atv-comment-overlay-top {\n display: flex;\n align-items: center;\n padding: 28px 28px 0;\n gap: 14px;\n}\n\n.atv-comment-overlay-avatar {\n display: flex;\n overflow: hidden;\n width: 44px;\n height: 44px;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n border: 2px solid var(--atv-border-subtle);\n border-radius: 50%;\n background-color: var(--atv-accent);\n background-position: center;\n background-size: cover;\n box-shadow: 0 2px 6px rgb(0 0 0 / 15%);\n color: #fff;\n font-size: 18px;\n font-weight: 600;\n transition:\n border-color var(--atv-duration-feedback) ease,\n box-shadow var(--atv-duration-feedback) ease;\n}\n\n.atv-comment-overlay-meta {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 6px;\n}\n\n.atv-comment-overlay-author,\na.atv-comment-overlay-author {\n overflow: hidden;\n color: var(--atv-text-primary);\n font-size: 16px;\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\na.atv-comment-overlay-author:hover {\n background: none;\n color: var(--atv-text-primary);\n}\n\n.atv-comment-overlay-stars {\n display: inline-flex;\n color: var(--atv-rating-gold);\n gap: 2px;\n}\n\n.atv-comment-overlay-stars svg {\n display: block;\n width: 16px;\n height: 16px;\n}\n\n.atv-comment-overlay-body {\n padding: 20px 28px;\n color: rgb(255 255 255 / 85%);\n font-size: 15px;\n line-height: 1.75;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.atv-comment-overlay-foot {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0 28px 28px;\n}\n\n.atv-comment-overlay-time {\n color: var(--atv-text-tertiary);\n font-size: 13px;\n font-weight: 500;\n letter-spacing: 0.02em;\n}\n\n.atv-comment-overlay-votes {\n display: inline-flex;\n align-items: center;\n padding: 6px 14px;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: 100px;\n appearance: none;\n background: rgb(255 255 255 / 4%);\n color: var(--atv-text-tertiary);\n cursor: pointer;\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n gap: 5px;\n letter-spacing: 0.02em;\n transition:\n color var(--atv-duration-feedback) ease,\n border-color var(--atv-duration-feedback) ease,\n background var(--atv-duration-feedback) ease,\n transform var(--atv-duration-press) var(--atv-ease-out);\n}\n\n.atv-comment-overlay-votes:hover {\n border-color: rgb(65 190 93 / 20%);\n background: rgb(65 190 93 / 4%);\n color: var(--atv-accent);\n}\n\n.atv-comment-overlay-votes:focus-visible {\n outline: 2px solid rgb(65 190 93 / 75%);\n outline-offset: 3px;\n}\n\n.atv-comment-overlay-votes:active {\n transform: scale(0.97);\n}\n\n.atv-comment-overlay-votes.is-voted {\n border-color: rgb(65 190 93 / 25%);\n background: rgb(65 190 93 / 6%);\n color: var(--atv-accent);\n}\n\n.atv-comment-overlay-votes svg {\n display: block;\n width: 14px;\n height: 14px;\n}\n\n.atv-modal-overlay {\n position: fixed;\n z-index: 10000;\n display: flex;\n width: 100vw;\n height: 100vh;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: none;\n margin: 0;\n -webkit-backdrop-filter: blur(16px) saturate(1.15);\n backdrop-filter: blur(16px) saturate(1.15);\n background: rgb(0 0 0 / 78%);\n inset: 0;\n}\n\n.atv-modal-img {\n max-width: 90vw;\n max-height: 90vh;\n border-radius: var(--atv-radius-lg);\n box-shadow: 0 40px 80px rgb(0 0 0 / 75%);\n object-fit: contain;\n transition: opacity var(--atv-duration-feedback) ease;\n}\n\n.atv-image-modal-frame {\n position: relative;\n display: grid;\n max-width: 90vw;\n max-height: 90vh;\n place-items: center;\n}\n\n.atv-image-modal-frame > .atv-modal-img,\n.atv-image-modal-frame > .atv-modal-preview,\n.atv-image-modal-loading {\n grid-area: 1 / 1;\n}\n\n.atv-modal-preview {\n max-width: 90vw;\n max-height: 90vh;\n border-radius: var(--atv-radius-lg);\n box-shadow: 0 40px 80px rgb(0 0 0 / 75%);\n object-fit: contain;\n transition:\n filter var(--atv-duration-feedback) ease,\n opacity var(--atv-duration-feedback) ease,\n transform var(--atv-duration-feedback) ease;\n}\n\n.atv-image-modal-frame.is-loading > .atv-modal-preview {\n filter: blur(10px) saturate(0.82);\n opacity: 0.72;\n transform: scale(1.02);\n}\n\n.atv-image-modal-frame.is-loading > .atv-modal-img {\n opacity: 0;\n}\n\n.atv-image-modal-frame.is-preview > .atv-modal-img {\n opacity: 0;\n pointer-events: none;\n}\n\n.atv-image-modal-frame.is-loaded > .atv-modal-preview {\n opacity: 0;\n pointer-events: none;\n}\n\n.atv-image-modal-loading {\n z-index: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n color: #fff;\n font-size: 14px;\n gap: 10px;\n text-shadow: 0 1px 8px rgb(0 0 0 / 65%);\n}\n\n.atv-modal-accent-bar {\n position: absolute;\n z-index: 1;\n top: 0;\n right: 0;\n left: 0;\n height: 3px;\n background: linear-gradient(\n to right,\n transparent,\n var(--atv-accent) 15%,\n var(--atv-accent) 85%,\n transparent\n );\n opacity: 1;\n}\n\n.atv-modal-close {\n position: fixed;\n z-index: 10001;\n top: 24px;\n right: 24px;\n display: flex;\n width: 44px;\n height: 44px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 1px solid rgb(255 255 255 / 18%);\n border-radius: 50%;\n appearance: none;\n background: rgb(255 255 255 / 8%);\n color: #fff;\n cursor: pointer;\n font: inherit;\n -webkit-tap-highlight-color: transparent;\n touch-action: manipulation;\n transition:\n transform var(--atv-duration-press) var(--atv-ease-out),\n color var(--atv-duration-feedback) ease,\n background var(--atv-duration-feedback) ease,\n border-color var(--atv-duration-feedback) ease;\n}\n\n.atv-modal-close.atv-image-modal-close {\n position: absolute;\n z-index: 2;\n top: 12px;\n right: 12px;\n}\n\n.atv-modal-close:hover {\n border-color: rgb(255 255 255 / 30%);\n background: rgb(255 255 255 / 16%);\n color: #fff;\n}\n\n.atv-modal-close:active {\n transform: scale(0.97);\n}\n\n.atv-modal-close svg {\n display: block;\n}\n\n.atv-comment-overlay,\n.atv-interest-modal,\n.atv-login-modal,\n.atv-review-modal {\n -webkit-backdrop-filter: blur(16px) saturate(1.15);\n backdrop-filter: blur(16px) saturate(1.15);\n}\n\n@keyframes atv-spin {\n from {\n transform: rotate(0deg);\n }\n\n to {\n transform: rotate(360deg);\n }\n}\n\n.atv-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid rgb(255 255 255 / 20%);\n border-radius: 50%;\n border-top-color: #41be5d;\n animation: atv-spin 0.8s linear infinite;\n}\n\n.atv-modal-loading {\n display: flex;\n flex-direction: column;\n align-items: center;\n color: #fff;\n font-size: 15px;\n gap: 16px;\n}\n\n.atv-footer-spacer {\n height: 64px;\n}\n\n/* ---------- Login Prompt Modal ---------- */\n\ndialog::backdrop {\n background: transparent;\n}\n\n.atv-login-modal {\n position: fixed;\n z-index: 10000;\n display: flex;\n width: 100vw;\n min-height: 100vh;\n align-items: center;\n justify-content: center;\n padding: max(24px, env(safe-area-inset-top))\n max(24px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom))\n max(24px, env(safe-area-inset-left));\n border: none;\n margin: 0;\n background: rgb(0 0 0 / 72%);\n inset: 0;\n overscroll-behavior: contain;\n}\n\n.atv-login-modal-inner {\n position: relative;\n overflow: auto;\n width: min(100%, 424px);\n max-height: min(736px, calc(100dvh - 48px));\n box-sizing: border-box;\n padding: 30px 22px 22px;\n border: 1px solid rgb(255 255 255 / 10%);\n border-radius: 20px;\n background: var(--atv-bg-secondary);\n box-shadow:\n inset 0 1px 0 rgb(255 255 255 / 8%),\n 0 24px 80px rgb(0 0 0 / 55%);\n color: var(--atv-text-primary);\n font-family:\n -apple-system, BlinkMacSystemFont, \"SF Pro Display\", \"PingFang SC\",\n \"Helvetica Neue\", \"Microsoft YaHei\", Inter, system-ui, sans-serif;\n scrollbar-width: none;\n text-align: center;\n}\n\n.atv-login-modal-inner::-webkit-scrollbar {\n display: none;\n}\n\n.atv-modal-close.atv-login-modal-close {\n position: absolute;\n z-index: 2;\n top: 12px;\n right: 12px;\n width: 32px;\n height: 32px;\n border-color: rgb(255 255 255 / 10%);\n background: rgb(255 255 255 / 6%);\n color: rgb(255 255 255 / 60%);\n}\n\n.atv-modal-close.atv-login-modal-close:hover {\n border-color: rgb(255 255 255 / 20%);\n background: rgb(255 255 255 / 12%);\n color: #fff;\n}\n\n.atv-login-modal-title {\n margin: 0 42px 6px;\n color: var(--atv-text-primary);\n font-size: 18px;\n font-weight: 650;\n line-height: 1.35;\n text-wrap: balance;\n}\n\n.atv-login-modal-desc {\n max-width: 300px;\n margin: 0 auto 14px;\n color: var(--atv-text-tertiary);\n font-size: 13px;\n line-height: 1.55;\n text-wrap: pretty;\n}\n\n.atv-login-modal-status {\n min-height: 20px;\n margin: 8px 0 0;\n color: var(--atv-text-tertiary);\n font-size: 12px;\n line-height: 1.5;\n}\n\n.atv-login-modal-status[hidden] {\n display: none;\n}\n\n.atv-login-modal-native {\n position: relative;\n overflow: hidden;\n width: 100%;\n box-sizing: border-box;\n padding: 10px;\n border-radius: 16px;\n margin-top: 10px;\n}\n\n.atv-login-modal-native::before {\n position: absolute;\n z-index: 2;\n border-radius: 10px;\n background: rgb(255 255 255 / 3.5%);\n content: \"\";\n inset: 10px;\n opacity: 1;\n pointer-events: none;\n transition: opacity var(--atv-duration-feedback) ease;\n}\n\n.atv-login-modal-native:empty {\n min-height: 360px;\n}\n\n.atv-login-modal-native[aria-busy=\"true\"] {\n min-height: 360px;\n}\n\n.atv-login-modal-native.is-ready::before {\n opacity: 0;\n}\n\n.atv-login-modal-native > iframe,\n.atv-login-modal-iframe {\n position: relative;\n z-index: 1;\n display: block !important;\n overflow: hidden !important;\n width: 340px !important;\n max-width: 100% !important;\n height: 448px !important;\n max-height: calc(100vh - 168px) !important;\n box-sizing: border-box !important;\n border: 0 !important;\n border-radius: 10px !important;\n margin: 0 auto !important;\n background: #fff !important;\n box-shadow: none !important;\n\n /* Cross-origin iframe dark-mode override (first-principles):\n invert(0.89) maps white #fff → #1c1c1c, which matches\n --atv-bg-secondary: #1c1c1e within 2 RGB points — no more\n pure-black void. Text #333 → #bbb, borders #e0 → #343434.\n hue-rotate(180deg) counter-rotates the color shift from\n invert so green stays green-ish and blue stays blue-ish.\n invert(0.89) is the precise value calculated to hit the\n existing modal surface color while keeping text readable. */\n filter: invert(0.89) hue-rotate(180deg) !important;\n opacity: 0;\n transition: opacity var(--atv-duration-feedback) ease;\n}\n\n.atv-login-modal-native.is-ready > iframe {\n opacity: 1;\n}\n\n.atv-login-modal-close:focus-visible {\n outline: 2px solid var(--atv-accent);\n outline-offset: 3px;\n}\n\n/* ---------- Interest Modal ---------- */\n\n.atv-interest-modal {\n position: fixed;\n z-index: 10000;\n display: flex;\n width: 100vw;\n height: 100vh;\n align-items: center;\n justify-content: center;\n padding: 24px;\n border: none;\n margin: 0;\n background: rgb(0 0 0 / 72%);\n inset: 0;\n}\n\n.atv-interest-modal-inner {\n position: relative;\n width: 100%;\n max-width: 456px;\n max-height: 90vh;\n border: 1px solid rgb(255 255 255 / 10%);\n border-radius: 20px;\n background: var(--atv-bg-secondary);\n box-shadow: 0 24px 80px rgb(0 0 0 / 55%);\n color-scheme: dark;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.atv-interest-modal-header {\n position: relative;\n display: flex;\n align-items: flex-start;\n justify-content: flex-start;\n padding: 28px 56px 0 24px;\n}\n\n.atv-interest-modal-header-title {\n margin: 3px 0 0;\n color: var(--atv-text-primary);\n font-size: 20px;\n font-weight: 600;\n letter-spacing: 0.01em;\n line-height: 1.35;\n}\n\n.atv-interest-modal-eyebrow {\n margin: 0;\n color: var(--atv-text-tertiary);\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n\n.atv-modal-close.atv-interest-modal-close {\n position: absolute;\n z-index: 2;\n top: 14px;\n right: 14px;\n width: 32px;\n height: 32px;\n border-color: rgb(255 255 255 / 10%);\n background: rgb(255 255 255 / 6%);\n color: rgb(255 255 255 / 60%);\n}\n\n.atv-modal-close.atv-interest-modal-close:hover {\n border-color: rgb(255 255 255 / 20%);\n background: rgb(255 255 255 / 12%);\n color: #fff;\n}\n\n.atv-modal-close.atv-interest-modal-close:active {\n border-color: rgb(255 255 255 / 30%);\n background: rgb(255 255 255 / 16%);\n}\n\n.atv-interest-modal-body {\n padding: 24px 20px 0;\n}\n\n.atv-interest-modal-statuses {\n position: relative;\n display: grid;\n min-width: 0;\n padding: 0;\n border: 0;\n border-bottom: 1px solid var(--atv-border-subtle);\n margin-bottom: 24px;\n gap: 0;\n grid-template-columns: repeat(var(--atv-interest-status-count), 1fr);\n}\n\n.atv-interest-modal-status-indicator {\n position: absolute;\n bottom: -1px;\n left: 0;\n width: calc(100% / var(--atv-interest-status-count));\n height: 2px;\n border-radius: 2px 2px 0 0;\n background: var(--atv-accent);\n pointer-events: none;\n transform: translateX(calc(var(--atv-interest-status-index) * 100%));\n transition: transform var(--atv-duration-feedback) var(--atv-ease-in-out);\n}\n\n.atv-interest-modal-status {\n position: relative;\n z-index: 1;\n display: inline-flex;\n height: 42px;\n flex: 1;\n align-items: center;\n justify-content: center;\n padding: 0 12px;\n border: none;\n border-radius: var(--atv-radius-sm) var(--atv-radius-sm) 0 0;\n appearance: none;\n background: transparent;\n color: var(--atv-text-tertiary);\n cursor: pointer;\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.01em;\n -webkit-tap-highlight-color: transparent;\n transition: color var(--atv-duration-feedback) ease;\n}\n\n.atv-interest-modal-status:hover {\n color: var(--atv-text-secondary);\n}\n\n.atv-interest-modal-status.is-active {\n color: var(--atv-text-primary);\n}\n\n.atv-interest-modal-rating {\n min-width: 0;\n padding: 0 0 20px;\n border: 0;\n border-bottom: 1px solid var(--atv-border-subtle);\n margin-bottom: 20px;\n}\n\n.atv-interest-modal-rating-header {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n margin-bottom: 8px;\n color: var(--atv-text-secondary);\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.01em;\n}\n\n.atv-interest-modal-rating-header > span:last-child {\n color: var(--atv-text-tertiary);\n font-size: 11px;\n font-weight: 500;\n letter-spacing: 0.02em;\n}\n\n.atv-interest-modal-stars {\n display: flex;\n gap: 4px;\n}\n\n.atv-interest-modal-star {\n display: flex;\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n appearance: none;\n background: none;\n color: rgb(255 255 255 / 20%);\n cursor: pointer;\n transition:\n color var(--atv-duration-feedback) ease,\n transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n.atv-interest-modal-star.is-full {\n color: var(--atv-rating-gold);\n}\n\n.atv-interest-modal-star svg {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.atv-interest-modal-comment {\n display: block;\n width: 100%;\n min-height: 78px;\n box-sizing: border-box;\n padding: 10px 14px;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: 10px;\n margin-bottom: 10px;\n background: rgb(255 255 255 / 4%);\n color: var(--atv-text-primary);\n font-family: inherit;\n font-size: 13px;\n line-height: 1.5;\n resize: none;\n -webkit-tap-highlight-color: transparent;\n transition:\n border-color var(--atv-duration-feedback) ease,\n background var(--atv-duration-feedback) ease,\n box-shadow var(--atv-duration-feedback) ease;\n}\n\n.atv-interest-modal-comment::placeholder {\n color: var(--atv-text-tertiary);\n}\n\n.atv-interest-modal-comment:focus {\n border-color: rgb(255 255 255 / 20%);\n background: rgb(255 255 255 / 6%);\n outline: none;\n}\n\n.atv-interest-modal-comment:focus-visible {\n border-color: var(--atv-accent-bright);\n box-shadow: 0 0 0 2px rgb(65 190 93 / 22%);\n}\n\n.atv-interest-modal-submit {\n display: flex;\n width: 100%;\n height: 44px;\n align-items: center;\n justify-content: center;\n padding: 0 24px;\n border: none;\n border-radius: var(--atv-radius-md);\n margin-bottom: 0;\n appearance: none;\n background: var(--atv-accent);\n color: #fff;\n cursor: pointer;\n font: inherit;\n font-size: 14px;\n -webkit-font-smoothing: antialiased;\n font-weight: 600;\n letter-spacing: 0.01em;\n -webkit-tap-highlight-color: transparent;\n transition:\n background var(--atv-duration-feedback) ease,\n transform var(--atv-duration-press) var(--atv-ease-out);\n}\n\n.atv-interest-modal-submit:hover {\n background: var(--atv-accent-bright);\n}\n\n.atv-interest-modal-submit:active {\n transform: scale(0.97);\n}\n\n.atv-interest-modal-submit:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.atv-interest-modal-remove {\n display: flex;\n width: 100%;\n height: 36px;\n align-items: center;\n justify-content: center;\n padding: 0 24px;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: var(--atv-radius-md);\n margin: 0;\n appearance: none;\n background: transparent;\n color: var(--atv-text-tertiary);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n font-weight: 500;\n letter-spacing: 0.02em;\n -webkit-tap-highlight-color: transparent;\n transition:\n color var(--atv-duration-feedback) ease,\n border-color var(--atv-duration-feedback) ease,\n background var(--atv-duration-feedback) ease;\n}\n\n.atv-interest-modal-remove:hover {\n border-color: rgb(255 255 255 / 15%);\n background: rgb(255 255 255 / 4%);\n color: var(--atv-text-secondary);\n}\n\n.atv-interest-modal-remove:active {\n background: rgb(255 255 255 / 8%);\n}\n\n.atv-interest-modal-error {\n border-radius: 10px;\n color: #ff453a;\n font-size: 12px;\n font-weight: 500;\n text-align: center;\n}\n\n.atv-interest-modal-error:empty {\n display: none;\n}\n\n.atv-interest-modal-error:not(:empty) {\n padding: 8px 16px;\n margin-top: 8px;\n background: rgb(255 69 58 / 8%);\n}\n\n.atv-interest-modal-field-header {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n margin: 0 0 8px;\n color: var(--atv-text-secondary);\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.01em;\n}\n\n.atv-interest-modal-field-header > span:last-child {\n color: var(--atv-text-tertiary);\n font-size: 11px;\n font-weight: 500;\n letter-spacing: 0.02em;\n}\n\n.atv-interest-modal-tags {\n padding-bottom: 20px;\n border-bottom: 1px solid var(--atv-border-subtle);\n margin-bottom: 20px;\n}\n\n.atv-interest-modal-tag-input-wrap {\n display: flex;\n min-height: 42px;\n flex-wrap: wrap;\n align-items: center;\n padding: 5px 8px;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: 10px;\n background: rgb(255 255 255 / 4%);\n gap: 5px;\n transition:\n border-color var(--atv-duration-feedback) ease,\n background var(--atv-duration-feedback) ease,\n box-shadow var(--atv-duration-feedback) ease;\n}\n\n.atv-interest-modal-tag-input-wrap:focus-within {\n border-color: rgb(255 255 255 / 20%);\n background: rgb(255 255 255 / 6%);\n}\n\n.atv-interest-modal-tag-input-wrap:has(\n .atv-interest-modal-tag-input:focus-visible\n) {\n border-color: var(--atv-accent-bright);\n box-shadow: 0 0 0 2px rgb(65 190 93 / 22%);\n}\n\n.atv-interest-modal-tag-input {\n min-width: 84px;\n flex: 1 1 84px;\n padding: 4px 2px;\n border: 0;\n background: transparent;\n color: var(--atv-text-primary);\n font: inherit;\n font-size: 13px;\n outline: 0;\n}\n\n.atv-interest-modal-tag-input::placeholder {\n color: var(--atv-text-tertiary);\n}\n\n.atv-interest-modal-tag {\n display: inline-flex;\n min-height: 26px;\n align-items: center;\n padding: 3px 9px;\n border: 1px solid rgb(255 255 255 / 10%);\n border-radius: 999px;\n appearance: none;\n background: rgb(255 255 255 / 5%);\n color: var(--atv-text-secondary);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n line-height: 1;\n transition:\n border-color var(--atv-duration-feedback) ease,\n background var(--atv-duration-feedback) ease,\n color var(--atv-duration-feedback) ease;\n}\n\n.atv-interest-modal-tag.is-current {\n border-color: rgb(65 190 93 / 45%);\n background: rgb(65 190 93 / 16%);\n color: #c3f6d0;\n}\n\n.atv-interest-modal-tag.is-selected {\n border-color: rgb(65 190 93 / 38%);\n background: rgb(65 190 93 / 12%);\n color: var(--atv-text-primary);\n}\n\n.atv-interest-modal-tag-suggestions {\n display: grid;\n margin-top: 10px;\n color: var(--atv-text-tertiary);\n font-size: 11px;\n gap: 6px;\n grid-template-columns: 48px minmax(0, 1fr);\n}\n\n.atv-interest-modal-tag-suggestions > span {\n padding-top: 5px;\n letter-spacing: 0.02em;\n}\n\n.atv-interest-modal-tag-suggestions > div {\n display: flex;\n flex-wrap: wrap;\n gap: 5px;\n}\n\n.atv-interest-modal-tag-skeleton {\n display: flex;\n height: 90px;\n flex-wrap: wrap;\n align-content: center;\n padding: 12px;\n border: 1px solid rgb(255 255 255 / 6%);\n border-radius: 10px;\n margin-bottom: 18px;\n background: rgb(255 255 255 / 3%);\n gap: 8px;\n}\n\n.atv-interest-modal-tag-skeleton span {\n display: block;\n height: 22px;\n border-radius: 999px;\n background: rgb(255 255 255 / 8%);\n}\n\n.atv-interest-modal-tag-skeleton span:nth-child(1) {\n width: 78px;\n}\n\n.atv-interest-modal-tag-skeleton span:nth-child(2) {\n width: 54px;\n}\n\n.atv-interest-modal-tag-skeleton span:nth-child(3) {\n width: 96px;\n}\n\n.atv-interest-modal-source-error {\n display: flex;\n min-height: 90px;\n align-items: center;\n justify-content: space-between;\n padding: 12px;\n border: 1px solid rgb(255 69 58 / 22%);\n border-radius: 10px;\n margin-bottom: 18px;\n background: rgb(255 69 58 / 8%);\n color: var(--atv-text-secondary);\n font-size: 12px;\n gap: 12px;\n}\n\n.atv-interest-modal-source-error button {\n padding: 0;\n border: 0;\n appearance: none;\n background: transparent;\n color: var(--atv-accent-bright);\n cursor: pointer;\n font: inherit;\n font-weight: 600;\n}\n\n.atv-interest-modal-visibility {\n display: grid;\n padding: 0;\n border: 0;\n margin: 0;\n gap: 10px;\n}\n\n.atv-interest-modal-visibility legend {\n padding: 0;\n margin-bottom: 10px;\n color: var(--atv-text-secondary);\n font-size: 13px;\n font-weight: 600;\n}\n\n.atv-interest-modal-visibility-option,\n.atv-interest-modal-broadcast-option {\n display: flex;\n align-items: center;\n color: var(--atv-text-secondary);\n cursor: pointer;\n font-size: 13px;\n gap: 8px;\n}\n\n.atv-interest-modal-visibility input {\n width: 16px;\n height: 16px;\n margin: 0;\n accent-color: var(--atv-accent);\n}\n\n.atv-interest-modal-broadcast-option {\n padding-left: 24px;\n color: var(--atv-text-tertiary);\n font-size: 12px;\n}\n\n.atv-interest-modal-visibility-note {\n padding-left: 24px;\n margin: -3px 0 0;\n color: var(--atv-text-tertiary);\n font-size: 12px;\n}\n\n.atv-interest-modal-footer {\n position: sticky;\n z-index: 1;\n bottom: 0;\n padding: 16px 20px 20px;\n border-top: 1px solid var(--atv-border-subtle);\n margin: 20px -20px 0;\n background: var(--atv-bg-secondary);\n box-shadow: 0 -12px 24px rgb(0 0 0 / 22%);\n}\n\n.atv-interest-modal-actions {\n display: grid;\n gap: 8px;\n}\n\n.atv-interest-modal-removal-confirmation {\n display: grid;\n padding: 12px;\n border: 1px solid rgb(255 69 58 / 22%);\n border-radius: 10px;\n background: rgb(255 69 58 / 8%);\n color: var(--atv-text-secondary);\n font-size: 12px;\n gap: 10px;\n}\n\n.atv-interest-modal-removal-confirmation > div {\n display: grid;\n gap: 8px;\n grid-template-columns: 1fr 1fr;\n}\n\n.atv-interest-modal-removal-confirmation button {\n height: 34px;\n border: 1px solid rgb(255 255 255 / 12%);\n border-radius: 999px;\n appearance: none;\n background: rgb(255 255 255 / 6%);\n color: var(--atv-text-secondary);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n font-weight: 600;\n}\n\n.atv-interest-modal-removal-confirmation button:last-child {\n border-color: rgb(255 69 58 / 35%);\n background: rgb(255 69 58 / 18%);\n color: #ff9f99;\n}\n\n.atv-interest-modal button:focus-visible,\n.atv-interest-modal-visibility input:focus-visible {\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: 2px;\n}\n\n/* ---------- Interest Panel (S3 marked state) ---------- */\n\n.atv-interest-panel {\n display: flex;\n width: 100%;\n flex-direction: column;\n gap: 10px;\n}\n\n.atv-interest-panel-header {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 12px;\n}\n\n.atv-interest-badge {\n cursor: pointer;\n}\n\n.atv-interest-panel-stars {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n}\n\n.atv-interest-panel-stars svg {\n display: block;\n width: 18px;\n height: 18px;\n}\n\n.atv-interest-panel-date {\n color: var(--atv-text-tertiary);\n font-size: 13px;\n letter-spacing: 0.02em;\n}\n\n.atv-interest-panel-comment {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n padding: 6px 0 2px;\n color: var(--atv-text-secondary);\n font-size: 14px;\n font-style: italic;\n gap: 8px;\n line-height: 1.6;\n}\n\n.atv-interest-panel-tags {\n overflow: hidden;\n min-width: 0;\n max-width: 36ch;\n color: var(--atv-text-tertiary);\n font-size: 13px;\n letter-spacing: 0.02em;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.atv-useful-badge {\n display: inline-flex;\n align-items: center;\n color: var(--atv-text-tertiary);\n font-size: 12px;\n font-style: normal;\n gap: 3px;\n letter-spacing: 0.02em;\n white-space: nowrap;\n}\n\n/* ---------- Trailer Tile ---------- */\n\n.atv-trailer-tile {\n position: relative;\n overflow: hidden;\n flex: 0 0 400px;\n border-radius: var(--atv-radius-md);\n aspect-ratio: 16 / 9;\n background: var(--atv-bg-tertiary);\n background-position: center;\n background-size: cover;\n cursor: pointer;\n scroll-snap-align: start;\n transition: transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n.atv-trailer-play-overlay {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgb(0 0 0 / 12%);\n inset: 0;\n}\n\n.atv-trailer-play-btn {\n display: flex;\n width: 56px;\n height: 56px;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background: rgb(0 0 0 / 60%);\n}\n\n.atv-trailer-play-btn svg {\n display: block;\n width: 24px;\n height: 24px;\n margin-left: 3px;\n color: #fff;\n}\n\n.atv-trailer-label {\n position: absolute;\n bottom: 12px;\n left: 12px;\n padding: 4px 10px;\n border-radius: 6px;\n backdrop-filter: blur(4px);\n background: rgb(0 0 0 / 60%);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n}\n\n/* ---------- Video Modal ---------- */\n\n.atv-modal-overlay.is-video {\n backdrop-filter: none;\n background: #000;\n}\n\n.atv-modal-video {\n display: block;\n max-width: 95vw;\n max-height: 90vh;\n border-radius: var(--atv-radius-lg);\n box-shadow: 0 40px 80px rgb(0 0 0 / 75%);\n}\n\n/* ---------- Reviews ---------- */\n\n.atv-reviews {\n display: grid;\n gap: 20px;\n grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));\n}\n\n.atv-review-card {\n position: relative;\n display: flex;\n flex-direction: column;\n padding: 20px;\n border: none;\n border-radius: var(--atv-radius-sm);\n appearance: none;\n background: #121214;\n color: inherit;\n cursor: pointer;\n font: inherit;\n -webkit-tap-highlight-color: transparent;\n text-align: left;\n touch-action: manipulation;\n transition:\n transform var(--atv-duration-hover) var(--atv-ease-out),\n background var(--atv-duration-hover) ease;\n}\n\n.atv-review-open-button {\n position: absolute;\n z-index: 0;\n padding: 0;\n border: 0;\n border-radius: var(--atv-radius-md);\n appearance: none;\n background: transparent;\n cursor: pointer;\n inset: 0;\n}\n\n.atv-review-content {\n position: relative;\n z-index: 1;\n display: contents;\n}\n\n.atv-review-card:active {\n box-shadow: none;\n transform: translateY(0);\n}\n\n.atv-review-card:focus-visible {\n background: rgb(255 255 255 / 4%);\n box-shadow: 0 0 0 5px rgb(65 190 93 / 12%);\n outline: 2px solid var(--atv-accent);\n outline-offset: 4px;\n}\n\n.atv-review-open-button:focus-visible {\n outline: 2px solid var(--atv-accent);\n outline-offset: 4px;\n}\n\n.atv-review-top {\n display: flex;\n align-items: center;\n margin-bottom: 12px;\n gap: 10px;\n pointer-events: none;\n}\n\n.atv-review-avatar {\n display: flex;\n width: 36px;\n height: 36px;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border: 1px solid rgb(255 255 255 / 6%);\n border-radius: 50%;\n background: var(--atv-accent);\n background-position: center;\n background-size: cover;\n color: #fff;\n font-size: 14px;\n font-weight: 600;\n}\n\n.atv-review-meta {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 3px;\n pointer-events: none;\n}\n\n.atv-review-author {\n position: relative;\n z-index: 2;\n overflow: hidden;\n color: var(--atv-text-primary);\n font-size: 14px;\n font-weight: 600;\n pointer-events: auto;\n text-decoration: none;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.atv-review-author:hover {\n color: var(--atv-accent);\n}\n\n.atv-review-stars {\n display: inline-flex;\n align-items: center;\n color: var(--atv-rating-gold);\n gap: 2px;\n}\n\n.atv-review-stars svg {\n display: block;\n width: 14px;\n height: 14px;\n}\n\n.atv-review-title {\n display: -webkit-box;\n overflow: hidden;\n margin-bottom: 8px;\n -webkit-box-orient: vertical;\n color: #f0f0f0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: -0.01em;\n -webkit-line-clamp: 2;\n line-height: 1.35;\n pointer-events: none;\n}\n\n.atv-review-excerpt {\n display: -webkit-box;\n overflow: hidden;\n flex: 1;\n margin-bottom: 14px;\n -webkit-box-orient: vertical;\n color: rgb(255 255 255 / 72%);\n font-size: 14px;\n -webkit-line-clamp: 4;\n line-height: 1.7;\n pointer-events: none;\n}\n\n.atv-review-foot {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: 14px;\n border-top: 1px solid rgb(255 255 255 / 5%);\n gap: 8px;\n pointer-events: none;\n}\n\n.atv-review-time {\n color: rgb(255 255 255 / 35%);\n font-size: 12px;\n font-weight: 400;\n letter-spacing: 0.02em;\n}\n\n.atv-review-readmore {\n color: var(--atv-accent);\n font-size: 11px;\n font-weight: 500;\n letter-spacing: 0.04em;\n opacity: 0;\n pointer-events: none;\n transform: translateX(-3px);\n transition:\n opacity var(--atv-duration-hover) var(--atv-ease-out),\n transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n.atv-review-card:focus-visible .atv-review-readmore,\n.atv-review-card:hover .atv-review-readmore {\n opacity: 1;\n transform: translateX(0);\n}\n\n.atv-review-card:has(.atv-review-open-button:focus-visible) {\n background: rgb(255 255 255 / 4%);\n box-shadow: 0 0 0 5px rgb(65 190 93 / 12%);\n}\n\n.atv-review-card:has(.atv-review-open-button:focus-visible)\n .atv-review-readmore {\n opacity: 1;\n transform: translateX(0);\n}\n\n.atv-review-actions {\n position: relative;\n z-index: 2;\n display: flex;\n margin-left: auto;\n gap: 6px;\n pointer-events: auto;\n}\n\n.atv-vote-btn {\n display: inline-flex;\n align-items: center;\n padding: 5px 12px;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: 8px;\n appearance: none;\n background: rgb(255 255 255 / 3%);\n color: rgb(255 255 255 / 55%);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n font-weight: 500;\n gap: 5px;\n line-height: 1;\n transition:\n color var(--atv-duration-feedback) ease,\n background var(--atv-duration-feedback) ease,\n border-color var(--atv-duration-feedback) ease,\n transform var(--atv-duration-press) var(--atv-ease-out);\n white-space: nowrap;\n}\n\n.atv-vote-btn:hover {\n border-color: var(--atv-accent);\n background: rgb(65 190 93 / 6%);\n color: var(--atv-accent);\n}\n\n.atv-vote-btn:focus-visible {\n outline: 2px solid rgb(65 190 93 / 75%);\n outline-offset: 3px;\n}\n\n.atv-vote-btn:active {\n transform: scale(0.97);\n}\n\n.atv-vote-btn.is-lg {\n padding: 8px 20px;\n font-size: 14px;\n}\n\n.atv-vote-btn svg {\n display: block;\n width: 11px;\n height: 11px;\n transform-box: fill-box;\n transform-origin: center;\n}\n\n.atv-vote-btn.down:hover {\n border-color: #ff453a;\n background: rgb(255 69 58 / 6%);\n color: #ff453a;\n}\n\n.atv-vote-btn.down svg {\n transform: rotate(180deg);\n}\n\n.atv-vote-btn.is-voted {\n border-color: rgb(65 190 93 / 20%);\n background: rgb(65 190 93 / 6%);\n color: var(--atv-accent);\n cursor: default;\n}\n\n.atv-vote-btn.down.is-voted {\n border-color: rgb(255 69 58 / 22%);\n background: rgb(255 69 58 / 6%);\n color: #ff453a;\n}\n\n/* ---------- Review Modal ---------- */\n\n.atv-review-modal {\n position: fixed;\n z-index: 10000;\n display: flex;\n width: 100vw;\n height: 100vh;\n align-items: center;\n justify-content: center;\n padding: 48px;\n border: none;\n margin: 0;\n background: rgb(0 0 0 / 72%);\n inset: 0;\n}\n\n.atv-review-modal .atv-modal-close {\n position: absolute;\n z-index: 2;\n top: 16px;\n right: 16px;\n width: 32px;\n height: 32px;\n border-color: rgb(255 255 255 / 10%);\n background: rgb(255 255 255 / 6%);\n color: rgb(255 255 255 / 60%);\n}\n\n.atv-review-modal .atv-modal-close:hover {\n border-color: rgb(255 255 255 / 20%);\n background: rgb(255 255 255 / 12%);\n color: #fff;\n}\n\n.atv-review-modal-scroll {\n position: relative;\n width: 100%;\n max-width: 800px;\n max-height: 85vh;\n padding: 48px 56px 32px;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: var(--atv-radius-lg);\n background: #121214;\n box-shadow: 0 24px 80px rgb(0 0 0 / 55%);\n overflow-y: auto;\n scrollbar-color: rgb(255 255 255 / 12%) transparent;\n scrollbar-width: thin;\n}\n\n.atv-review-modal-header {\n padding-bottom: 14px;\n border-bottom: 1px solid rgb(255 255 255 / 5%);\n margin-bottom: 16px;\n}\n\n.atv-review-modal-title {\n margin-bottom: 4px;\n color: #f0f0f0;\n font-size: 24px;\n font-weight: 600;\n letter-spacing: -0.01em;\n line-height: 1.32;\n}\n\n.atv-review-modal-title:focus {\n outline: none;\n}\n\n.atv-review-modal-byline {\n display: flex;\n align-items: center;\n margin-top: 4px;\n gap: 10px;\n}\n\n.atv-review-modal-avatar {\n display: flex;\n width: 28px;\n height: 28px;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border: 1.5px solid rgb(255 255 255 / 6%);\n border-radius: 50%;\n background: var(--atv-accent);\n background-position: center;\n background-size: cover;\n color: #fff;\n font-size: 12px;\n font-weight: 600;\n}\n\n.atv-review-modal-byline-text {\n display: flex;\n align-items: center;\n font-size: 13px;\n gap: 4px;\n line-height: 1;\n}\n\n.atv-review-modal-byline-name {\n color: var(--atv-text-secondary);\n}\n\n.atv-review-modal-byline-time {\n color: var(--atv-text-tertiary);\n}\n\n.atv-review-modal-body {\n color: rgb(255 255 255 / 82%);\n font-size: 16px;\n line-height: 1.85;\n}\n\n.atv-review-modal-body h2 {\n margin-bottom: 16px;\n color: #f0f0f0;\n font-size: 20px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.atv-review-modal-body h2 a {\n background: transparent;\n color: inherit;\n text-decoration: none;\n}\n\n.atv-review-modal-body h2 a:hover {\n background: transparent;\n color: var(--atv-accent);\n}\n\n.atv-review-modal-body p {\n margin-bottom: 16px;\n}\n\n.atv-review-modal-body blockquote {\n padding: 16px 20px;\n border-radius: 0 var(--atv-radius-sm) var(--atv-radius-sm) 0;\n border-left: 3px solid var(--atv-accent);\n margin: 20px 0;\n background: rgb(255 255 255 / 3%);\n color: rgb(255 255 255 / 82%);\n}\n\n.atv-review-modal-body blockquote p {\n margin-bottom: 8px;\n}\n\n.atv-review-modal-body blockquote p:last-child {\n margin-bottom: 0;\n}\n\n.atv-review-modal-body blockquote cite,\n.atv-review-modal-body blockquote footer {\n display: block;\n margin-top: 8px;\n color: var(--atv-text-tertiary);\n font-size: 13px;\n font-style: normal;\n font-weight: 500;\n}\n\n.atv-review-modal-body blockquote cite::before,\n.atv-review-modal-body blockquote footer::before {\n content: \"\\2014\\00a0\";\n}\n\n.atv-review-modal-body blockquote blockquote {\n border-left-color: rgb(255 255 255 / 20%);\n margin-left: 0;\n background: rgb(255 255 255 / 6%);\n}\n\n/* ---------- Group discussions ---------- */\n\n.atv-discussion-board {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.atv-discussion-row {\n position: relative;\n display: flex;\n overflow: hidden;\n flex-direction: column;\n padding: 18px 20px;\n border: none;\n border-radius: var(--atv-radius-sm);\n background: #121214;\n gap: 6px;\n isolation: isolate;\n transition:\n transform var(--atv-duration-press) var(--atv-ease-out),\n background var(--atv-duration-hover) ease;\n}\n\n/* Accent bar — reveals on hover/focus */\n\n.atv-discussion-row::before {\n position: absolute;\n top: 14px;\n bottom: 14px;\n left: 0;\n width: 3px;\n border-radius: 0 3px 3px 0;\n background: var(--atv-accent);\n content: \"\";\n opacity: 0;\n transform: scaleY(0);\n transform-origin: top;\n transition:\n opacity var(--atv-duration-hover) var(--atv-ease-out),\n transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n/* Entire row is clickable */\n\n.atv-discussion-topic-link {\n position: absolute;\n z-index: 1;\n border-radius: inherit;\n inset: 0;\n}\n\n.atv-discussion-topic-link:focus-visible {\n outline: 2px solid var(--atv-accent);\n outline-offset: -3px;\n}\n\n/* ---------- Copy (title + metadata) ---------- */\n\n.atv-discussion-copy {\n position: relative;\n z-index: 2;\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 6px;\n pointer-events: none;\n}\n\n/* Title + arrow in one row */\n\n.atv-discussion-title-row {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n}\n\n.atv-discussion-title {\n display: -webkit-box;\n overflow: hidden;\n min-width: 0;\n flex: 1;\n margin: 0;\n -webkit-box-orient: vertical;\n color: rgb(255 255 255 / 90%);\n font-size: 15px;\n font-weight: 500;\n letter-spacing: -0.015em;\n -webkit-line-clamp: 2;\n line-height: 1.5;\n overflow-wrap: break-word;\n transition: color var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n/* Arrow — always in flow, hidden until hover */\n\n.atv-discussion-arrow {\n flex-shrink: 0;\n color: var(--atv-text-tertiary);\n font-size: 15px;\n line-height: 1.5;\n opacity: 0;\n transform: translateX(-6px);\n transition:\n opacity var(--atv-duration-hover) var(--atv-ease-out),\n transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n/* ---------- Metadata row ---------- */\n\n.atv-discussion-meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n color: var(--atv-text-tertiary);\n font-size: 12px;\n gap: 0;\n line-height: 1.35;\n}\n\n.atv-discussion-author {\n display: block;\n overflow: hidden;\n width: fit-content;\n max-width: 100%;\n color: inherit;\n font-size: inherit;\n line-height: inherit;\n pointer-events: auto;\n text-decoration: none;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\na.atv-discussion-author:hover {\n text-decoration: underline;\n}\n\na.atv-discussion-author:focus-visible {\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: 2px;\n text-decoration: underline;\n}\n\n/* Activity wraps replies + time inline */\n\n.atv-discussion-activity {\n display: inline;\n color: inherit;\n font-size: inherit;\n line-height: inherit;\n pointer-events: none;\n}\n\n/* Replies — plain text, no pill */\n\n.atv-discussion-replies {\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.01em;\n white-space: nowrap;\n}\n\n/* Time — date + time on one line */\n\n.atv-discussion-time {\n display: inline-flex;\n align-items: center;\n font-variant-numeric: tabular-nums;\n gap: 3px;\n white-space: nowrap;\n}\n\n/* ---------- Dot separators ---------- */\n\n.atv-discussion-author + .atv-discussion-activity::before {\n margin: 0 7px;\n color: rgb(255 255 255 / 20%);\n content: \"·\";\n}\n\n.atv-discussion-replies + .atv-discussion-time::before {\n margin: 0 7px;\n color: rgb(255 255 255 / 20%);\n content: \"·\";\n}\n\n/* ---------- Footer ---------- */\n\n.atv-discussion-footer {\n margin-top: 20px;\n text-align: right;\n}\n\n.atv-discussion-footer a {\n display: inline-flex;\n align-items: center;\n color: var(--atv-text-secondary);\n font-size: 13px;\n font-weight: 500;\n gap: 4px;\n letter-spacing: 0.01em;\n text-decoration: none;\n transition:\n color var(--atv-duration-hover) var(--atv-ease-out),\n transform var(--atv-duration-hover) var(--atv-ease-out);\n}\n\n.atv-discussion-footer a:focus-visible {\n color: var(--atv-accent-bright);\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: 3px;\n text-decoration: underline;\n}\n\n/* ---------- Interactive states ---------- */\n\n/* Row hover → accent bar reveals, bg lifts */\n\n.atv-discussion-row:has(.atv-discussion-topic-link:focus-visible)::before,\n.atv-discussion-row:has(.atv-discussion-author:focus-visible)::before {\n opacity: 1;\n transform: scaleY(1);\n}\n\n.atv-discussion-row:has(.atv-discussion-topic-link:focus-visible)\n .atv-discussion-title,\n.atv-discussion-row:has(.atv-discussion-author:focus-visible)\n .atv-discussion-title {\n color: #fff;\n}\n\n.atv-discussion-row:has(.atv-discussion-topic-link:focus-visible)\n .atv-discussion-arrow,\n.atv-discussion-row:has(.atv-discussion-author:focus-visible)\n .atv-discussion-arrow {\n opacity: 1;\n transform: translateX(0);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .atv-discussion-footer a:hover {\n color: var(--atv-accent-bright);\n text-decoration: underline;\n transform: translateX(2px);\n }\n\n .atv-discussion-row:hover .atv-discussion-title {\n color: #fff;\n }\n\n .atv-discussion-row:hover .atv-discussion-arrow {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n/* ---------- Mobile (<=768px) ---------- */\n\n@media (width <= 768px) {\n .atv-discussion-board {\n gap: 6px;\n }\n\n .atv-discussion-row {\n padding: 14px 16px;\n gap: 5px;\n }\n\n .atv-discussion-copy {\n gap: 5px;\n }\n\n .atv-discussion-title-row {\n gap: 8px;\n }\n\n .atv-discussion-title {\n overflow-wrap: anywhere;\n }\n\n .atv-discussion-arrow {\n font-size: 14px;\n line-height: 1.45;\n }\n\n .atv-discussion-meta {\n flex-wrap: wrap;\n row-gap: 2px;\n }\n}\n\n/* ---------- Reduced motion ---------- */\n\n@media (prefers-reduced-motion: reduce) {\n .atv-discussion-row::before,\n .atv-discussion-title,\n .atv-discussion-arrow,\n .atv-discussion-row,\n .atv-discussion-footer a {\n transition: none;\n }\n\n .atv-discussion-row::before {\n opacity: 0;\n transform: none;\n }\n\n .atv-discussion-row:hover::before,\n .atv-discussion-row:has(.atv-discussion-topic-link:focus-visible)::before,\n .atv-discussion-row:has(.atv-discussion-author:focus-visible)::before {\n opacity: 1;\n }\n\n .atv-discussion-arrow {\n opacity: 0;\n transform: none;\n }\n\n .atv-discussion-row:hover .atv-discussion-arrow {\n opacity: 1;\n }\n\n .atv-discussion-row:active {\n transform: none;\n }\n\n .atv-discussion-footer a:hover {\n transform: none;\n }\n}\n\n/* ── Review content images (Douban image-container) ── */\n\n.atv-review-modal-body .image-container {\n margin: 24px 0;\n clear: both;\n}\n\n.atv-review-modal-body .image-container.image-float-left {\n max-width: 50%;\n margin: 8px 24px 8px 0;\n float: left;\n}\n\n.atv-review-modal-body .image-container.image-float-right {\n max-width: 50%;\n margin: 8px 0 8px 24px;\n float: right;\n}\n\n.atv-review-modal-body .image-wrapper {\n overflow: hidden;\n border-radius: var(--atv-radius-sm);\n line-height: 0;\n}\n\n.atv-review-modal-body .image-wrapper img {\n display: block;\n width: auto;\n max-width: 100%;\n height: auto;\n margin: 0 auto;\n}\n\n.atv-review-modal-body .image-caption-wrapper {\n margin-top: 8px;\n text-align: center;\n}\n\n.atv-review-modal-body .image-caption {\n display: inline-block;\n color: var(--atv-text-tertiary);\n font-size: 13px;\n font-style: italic;\n font-weight: 400;\n letter-spacing: 0.02em;\n line-height: 1.6;\n}\n\n/* ── Review content subject reference (Douban subject-container) ── */\n\n.atv-review-modal-body .subject-container {\n overflow: hidden;\n padding-left: 2px;\n border: 1px solid rgb(255 255 255 / 8%);\n border-radius: var(--atv-radius-md);\n border-left: 2px solid var(--atv-accent);\n margin: 24px 0;\n background: linear-gradient(\n 105deg,\n rgb(65 190 93 / 7%),\n rgb(18 18 20 / 94%) 42%\n );\n}\n\n.atv-review-modal-body .subject-wrapper > a,\n.atv-review-modal-body .subject-wrapper > a:link,\n.atv-review-modal-body .subject-wrapper > a:visited {\n display: grid;\n padding: 12px;\n border-bottom: 0;\n background: transparent;\n color: inherit;\n gap: 12px;\n grid-template-columns: 56px minmax(0, 1fr);\n text-decoration: none;\n transition: none;\n}\n\n.atv-review-modal-body .subject-wrapper > a:hover,\n.atv-review-modal-body .subject-wrapper > a:active {\n border-bottom: 0;\n background: rgb(255 255 255 / 3%);\n color: inherit;\n}\n\n.atv-review-modal-body .subject-wrapper > a:focus-visible {\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: -2px;\n}\n\n.atv-review-modal-body .subject-cover {\n overflow: hidden;\n width: 56px;\n height: 84px;\n border-radius: 5px;\n background: var(--atv-bg-tertiary);\n box-shadow: 0 8px 20px rgb(0 0 0 / 30%);\n}\n\n.atv-review-modal-body .subject-cover img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.atv-review-modal-body .subject-info {\n display: flex;\n min-width: 0;\n flex-direction: column;\n justify-content: center;\n}\n\n.atv-review-modal-body .subject-title {\n display: flex;\n min-width: 0;\n flex-wrap: wrap;\n align-items: baseline;\n color: var(--atv-text-primary);\n font-size: 16px;\n font-weight: 650;\n gap: 4px;\n letter-spacing: 0.01em;\n line-height: 1.35;\n}\n\n.atv-review-modal-body .title-tail {\n color: var(--atv-text-tertiary);\n font-size: 13px;\n font-weight: 500;\n}\n\n.atv-review-modal-body .subject-rating {\n display: flex;\n align-items: center;\n margin-top: 7px;\n gap: 1px;\n line-height: 1;\n}\n\n.atv-review-modal-body .rating-star1::before,\n.atv-review-modal-body .rating-star2::before,\n.atv-review-modal-body .rating-star0::before {\n content: \"★\";\n font-size: 13px;\n}\n\n.atv-review-modal-body .rating-star1::before {\n color: var(--atv-rating-gold);\n}\n\n.atv-review-modal-body .rating-star2::before {\n background: linear-gradient(\n 90deg,\n var(--atv-rating-gold) 50%,\n rgb(255 255 255 / 18%) 50%\n );\n -webkit-background-clip: text;\n background-clip: text;\n color: transparent;\n}\n\n.atv-review-modal-body .rating-star0::before {\n color: rgb(255 255 255 / 18%);\n}\n\n.atv-review-modal-body .rating-score {\n margin-left: 6px;\n color: var(--atv-text-secondary);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n font-weight: 600;\n}\n\n.atv-review-modal-body .subject-summary {\n margin-top: 7px;\n color: var(--atv-text-tertiary);\n font-size: 12px;\n line-height: 1.55;\n}\n\n.atv-review-modal-body .subject-caption-wrapper {\n padding: 0 12px 12px 80px;\n}\n\n.atv-review-modal-body .subject-caption {\n color: var(--atv-text-secondary);\n font-size: 12px;\n font-style: italic;\n line-height: 1.55;\n}\n\n/* ── Review content links (Douban a.link / generic) ── */\n\n.atv-review-modal-body a:link,\n.atv-review-modal-body a:visited {\n border-bottom: 1px solid rgb(65 190 93 / 25%);\n background: transparent;\n color: var(--atv-accent);\n overflow-wrap: break-word;\n text-decoration: none;\n transition:\n border-color var(--atv-duration-feedback) ease,\n color var(--atv-duration-feedback) ease;\n}\n\n.atv-review-modal-body a:hover,\n.atv-review-modal-body a:active {\n border-bottom-color: var(--atv-accent-bright);\n background: transparent;\n color: var(--atv-accent-bright);\n}\n\n/* ============================================\n 7. Review Content Typography — All Elements\n Target ALL HTML tags that may appear inside\n Douban review rich-text content.\n ============================================ */\n\n/* --- Headings --- */\n\n.atv-review-modal-body h3 {\n margin: 28px 0 12px;\n color: var(--atv-text-primary);\n font-size: 18px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.atv-review-modal-body h4 {\n margin: 24px 0 10px;\n color: var(--atv-text-primary);\n font-size: 16px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.atv-review-modal-body h5,\n.atv-review-modal-body h6 {\n margin: 20px 0 8px;\n color: var(--atv-text-secondary);\n font-size: 14px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n/* --- Horizontal Rule --- */\n\n.atv-review-modal-body hr {\n height: 0;\n border: none;\n border-top: 1px solid var(--atv-border-subtle);\n margin: 28px 0;\n}\n\n/* --- Lists --- */\n\n.atv-review-modal-body ul,\n.atv-review-modal-body ol {\n padding-left: 24px;\n margin: 0 0 16px;\n line-height: 1.7;\n}\n\n.atv-review-modal-body ul {\n list-style: disc;\n}\n\n.atv-review-modal-body ol {\n list-style: decimal;\n}\n\n.atv-review-modal-body li {\n margin-bottom: 6px;\n line-height: 1.7;\n}\n\n.atv-review-modal-body li:last-child {\n margin-bottom: 0;\n}\n\n/* --- Inline Text Semantics --- */\n\n.atv-review-modal-body strong,\n.atv-review-modal-body b {\n color: rgb(255 255 255 / 92%);\n font-weight: 700;\n}\n\n.atv-review-modal-body em,\n.atv-review-modal-body i {\n font-style: italic;\n}\n\n.atv-review-modal-body small {\n color: var(--atv-text-tertiary);\n font-size: 0.85em;\n}\n\n.atv-review-modal-body q {\n font-style: italic;\n}\n\n.atv-review-modal-body q::before {\n content: \"\\201C\";\n}\n\n.atv-review-modal-body q::after {\n content: \"\\201D\";\n}\n\n.atv-review-modal-body u {\n text-decoration: underline;\n text-decoration-thickness: 1px;\n text-underline-offset: 2px;\n}\n\n.atv-review-modal-body s,\n.atv-review-modal-body del {\n color: var(--atv-text-tertiary);\n text-decoration: line-through;\n}\n\n.atv-review-modal-body sup {\n font-size: 0.75em;\n line-height: 1;\n vertical-align: super;\n}\n\n.atv-review-modal-body sub {\n font-size: 0.75em;\n line-height: 1;\n vertical-align: sub;\n}\n\n/* --- Code --- */\n\n.atv-review-modal-body code {\n padding: 2px 6px;\n border-radius: 4px;\n background: rgb(255 255 255 / 6%);\n font-family:\n \"SF Mono\", Monaco, \"Cascadia Code\", \"JetBrains Mono\", \"Fira Code\", Consolas,\n monospace;\n font-size: 0.9em;\n overflow-wrap: break-word;\n}\n\n.atv-review-modal-body pre {\n padding: 16px 20px;\n border: 1px solid var(--atv-border-subtle);\n border-radius: var(--atv-radius-sm);\n margin: 0 0 20px;\n background: rgb(0 0 0 / 40%);\n line-height: 1.6;\n -webkit-overflow-scrolling: touch;\n overflow-x: auto;\n}\n\n.atv-review-modal-body pre code {\n padding: 0;\n background: none;\n font-size: 14px;\n word-break: normal;\n}\n\n/* --- Tables --- */\n\n.atv-review-modal-body table {\n width: 100%;\n margin: 20px 0;\n border-collapse: collapse;\n line-height: 1.6;\n}\n\n.atv-review-modal-body thead {\n border-bottom: 2px solid var(--atv-border-medium);\n}\n\n.atv-review-modal-body th {\n padding: 10px 14px;\n color: var(--atv-text-primary);\n font-weight: 600;\n text-align: left;\n white-space: nowrap;\n}\n\n.atv-review-modal-body td {\n padding: 10px 14px;\n border-bottom: 1px solid var(--atv-border-subtle);\n color: rgb(255 255 255 / 82%);\n}\n\n.atv-review-modal-body tbody tr:last-child td {\n border-bottom: none;\n}\n\n.atv-review-modal-body .review-content,\n.atv-review-modal-body .review-content p,\n.atv-review-modal-body .review-content div,\n.atv-review-modal-body .review-content span {\n color: rgb(255 255 255 / 82%);\n}\n\n.atv-review-modal-body .spoiler-tip {\n margin-bottom: 12px;\n color: #ff9f0a;\n font-size: 13px;\n font-weight: 600;\n}\n\n.atv-review-modal-body .main-hd {\n display: flex;\n align-items: center;\n margin-bottom: 16px;\n gap: 10px;\n}\n\n.atv-review-modal-body .main-hd a.name {\n color: var(--atv-accent);\n font-size: 14px;\n font-weight: 500;\n text-decoration: none;\n}\n\n.atv-review-modal-body .main-hd a.name:hover {\n text-decoration: underline;\n}\n\n.atv-review-modal-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: 20px;\n border-top: 1px solid rgb(255 255 255 / 4%);\n margin-top: 28px;\n}\n\n.atv-review-modal-votes {\n display: flex;\n gap: 14px;\n}\n\n.atv-review-modal-link {\n display: flex;\n align-items: center;\n}\n\n#atv-douban-root .atv-review-modal-link-a {\n background: transparent;\n color: rgb(255 255 255 / 50%);\n font-size: 13px;\n text-decoration: none;\n transition: color var(--atv-duration-feedback) ease;\n}\n\n#atv-douban-root .atv-review-modal-link-a:link,\n#atv-douban-root .atv-review-modal-link-a:visited {\n background: transparent;\n color: rgb(255 255 255 / 50%);\n}\n\n#atv-douban-root .atv-review-modal-link-a:hover,\n#atv-douban-root .atv-review-modal-link-a:active {\n background: transparent;\n color: var(--atv-accent);\n}\n\n.atv-review-modal-stars {\n display: inline-flex;\n align-items: center;\n margin: 0 0 4px;\n color: var(--atv-rating-gold);\n gap: 2px;\n}\n\n.atv-review-modal-stars svg {\n display: block;\n width: 14px;\n height: 14px;\n}\n\n.atv-review-modal-body.is-skeleton {\n position: relative;\n min-height: 140px;\n color: transparent;\n}\n\n.atv-review-modal-body.is-skeleton::before,\n.atv-review-modal-body.is-skeleton::after {\n display: block;\n height: 14px;\n border-radius: 4px;\n background: rgb(255 255 255 / 6%);\n content: \"\";\n}\n\n.atv-review-modal-body.is-skeleton::before {\n width: 92%;\n margin-bottom: 14px;\n}\n\n.atv-review-modal-body.is-skeleton::after {\n width: 68%;\n}\n\n.atv-review-modal-body.is-error {\n display: flex;\n min-height: 140px;\n align-items: center;\n justify-content: center;\n color: var(--atv-text-tertiary);\n text-align: center;\n}\n\n.atv-review-modal-error {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 14px;\n}\n\n.atv-review-modal-error p {\n margin: 0;\n}\n\n.atv-review-modal-retry {\n display: inline-flex;\n min-height: 34px;\n align-items: center;\n justify-content: center;\n padding: 0 18px;\n border: 1px solid rgb(255 255 255 / 10%);\n border-radius: 999px;\n appearance: none;\n background: rgb(255 255 255 / 4%);\n color: var(--atv-text-secondary);\n cursor: pointer;\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n transition:\n color var(--atv-duration-feedback) ease,\n border-color var(--atv-duration-feedback) ease,\n background var(--atv-duration-feedback) ease;\n}\n\n.atv-review-modal-retry:hover,\n.atv-review-modal-retry:focus-visible {\n border-color: rgb(65 190 93 / 35%);\n background: rgb(65 190 93 / 8%);\n color: var(--atv-accent);\n}\n\n.atv-review-modal-retry:focus-visible,\n.atv-review-modal .atv-modal-close:focus-visible,\n#atv-douban-root .atv-review-modal-link-a:focus-visible {\n outline: 2px solid var(--atv-accent);\n outline-offset: 3px;\n}\n\n/* ---------- Responsive ---------- */\n\n@media (width <= 1024px) {\n .atv-hero {\n min-height: 64vh;\n padding: 104px 24px 48px;\n }\n\n .atv-hero-inner {\n flex-direction: column;\n align-items: flex-start;\n gap: 28px;\n }\n\n .atv-poster-card {\n width: 220px;\n }\n\n .atv-section {\n padding: 44px 24px;\n }\n\n .atv-info-grid {\n column-gap: 24px;\n grid-template-columns: 160px 1fr;\n }\n}\n\n@media (width <= 768px) {\n .atv-hero {\n min-height: 56vh;\n padding: 88px 20px 40px;\n }\n\n .atv-poster-card {\n width: 180px;\n }\n\n .atv-section {\n padding: 36px 20px;\n }\n\n .atv-info-grid {\n grid-template-columns: 1fr;\n row-gap: 4px;\n }\n\n .atv-info-label {\n padding-top: 12px;\n }\n\n .atv-recs {\n gap: 18px;\n grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n }\n\n .atv-cast-card {\n flex-basis: 120px;\n }\n\n .atv-cast-avatar {\n width: 120px;\n height: 120px;\n }\n\n .atv-series-card {\n flex-basis: 120px;\n }\n\n .atv-photo-tile:not(.atv-trailer-tile),\n .atv-photo-rail-reserve {\n height: 157.5px;\n }\n\n .atv-rating-panel .atv-rating-panel-score {\n font-size: 32px;\n }\n\n .atv-rating-panel-douban,\n .atv-rating-panel-imdb,\n .atv-rating-panel-rt,\n .atv-rating-panel-mc {\n padding: 12px 14px 10px;\n }\n\n .atv-rating-panel {\n min-width: 0;\n }\n\n .atv-comments {\n grid-template-columns: 1fr;\n }\n\n .atv-comment-overlay-inner {\n max-width: 95vw;\n border-radius: 16px;\n }\n\n .atv-comment-overlay-top {\n padding: 24px 20px 0;\n }\n\n .atv-comment-overlay-body {\n padding: 16px 20px;\n font-size: 14px;\n }\n\n .atv-comment-overlay-foot {\n padding: 0 20px 24px;\n }\n\n .atv-reviews {\n grid-template-columns: 1fr;\n }\n\n .atv-review-modal-scroll {\n max-width: 100vw;\n max-height: 92vh;\n padding: 24px 18px 22px;\n border-radius: 20px 20px 0 0;\n }\n\n .atv-review-modal {\n align-items: flex-end;\n padding: 0;\n overscroll-behavior: contain;\n }\n\n .atv-review-modal .atv-modal-close {\n top: 14px;\n right: 14px;\n }\n\n .atv-review-modal-body blockquote {\n padding: 12px 14px;\n margin: 16px 0;\n }\n\n .atv-review-modal-body blockquote blockquote {\n padding: 10px 12px;\n }\n\n .atv-review-modal-body .image-container {\n margin: 16px 0;\n }\n\n .atv-review-modal-body .image-container.image-float-left,\n .atv-review-modal-body .image-container.image-float-right {\n max-width: 100%;\n margin: 16px 0;\n float: none;\n }\n\n .atv-review-modal-body .subject-wrapper > a {\n padding: 10px;\n gap: 10px;\n grid-template-columns: 44px minmax(0, 1fr);\n }\n\n .atv-review-modal-body .subject-cover {\n width: 44px;\n height: 66px;\n }\n\n .atv-review-modal-body .subject-title {\n font-size: 15px;\n }\n\n .atv-review-modal-body .subject-caption-wrapper {\n padding: 0 10px 10px 66px;\n }\n\n /* Review content typography — responsive */\n .atv-review-modal-body h3 {\n margin-top: 22px;\n }\n\n .atv-review-modal-body h4 {\n margin-top: 18px;\n }\n\n .atv-review-modal-body hr {\n margin: 22px 0;\n }\n\n .atv-review-modal-body pre {\n padding: 12px 16px;\n }\n\n .atv-review-modal-body th,\n .atv-review-modal-body td {\n padding: 8px 10px;\n }\n\n .atv-login-modal {\n align-items: flex-end;\n padding: 16px;\n }\n\n .atv-login-modal-inner {\n width: 100%;\n padding: 28px 20px 20px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .atv-section {\n animation: none;\n }\n\n .atv-trailer-tile,\n .atv-trailer-tile:hover {\n filter: none;\n transform: none;\n }\n\n .atv-trailer-play-overlay {\n transition: none;\n }\n\n .atv-trailer-play-btn {\n transition: none;\n }\n\n .atv-trailer-tile:hover .atv-trailer-play-btn {\n box-shadow: none;\n transform: none;\n }\n\n .atv-modal-overlay.is-open .atv-modal-video {\n transform: none;\n }\n\n .atv-review-card,\n .atv-review-card:hover,\n .atv-review-card:active {\n box-shadow: none;\n transform: none;\n transition: none;\n }\n\n .atv-review-modal-scroll {\n transform: none;\n transition: none;\n }\n\n .atv-review-modal.is-open .atv-review-modal-scroll {\n transform: none;\n }\n\n .atv-review-modal {\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n transition: none;\n }\n\n /* Modal overlay fades */\n .atv-modal-overlay,\n .atv-comment-overlay,\n .atv-interest-modal,\n .atv-login-modal {\n transition: none;\n }\n\n .atv-login-modal-inner,\n .atv-login-modal.is-open .atv-login-modal-inner {\n transform: none;\n transition: none;\n }\n\n /* Accent edge glow */\n .atv-modal-accent-bar {\n transform: scaleX(1);\n transition: none;\n }\n}\n\n@media (width <= 768px) {\n .atv-trailer-tile {\n height: 157.5px;\n flex-basis: 280px;\n }\n}\n\n/* Cross-experience motion policy: pointer capability and motion preferences. */\n\n@media (hover: hover) and (pointer: fine) {\n .atv-btn-primary:hover {\n background: var(--atv-accent-bright);\n transform: translateY(-1px);\n }\n\n .atv-btn-secondary:hover {\n background: rgb(255 255 255 / 12%);\n transform: translateY(-1px);\n }\n\n .atv-comment-card:hover {\n background: rgb(255 255 255 / 3%);\n }\n\n .atv-review-card:hover {\n background: rgb(255 255 255 / 3%);\n }\n\n .atv-review-card:hover .atv-review-readmore {\n opacity: 1;\n transform: translateX(0);\n }\n\n .atv-stream-card:not(.atv-stream-card-combined):hover::after {\n opacity: 0.18;\n }\n\n .atv-stream-card:hover {\n border-color: rgb(255 255 255 / 18%);\n background: rgb(255 255 255 / 9%);\n }\n\n .atv-interest-modal-star:hover {\n transform: scale(1.15);\n }\n\n .atv-discussion-row:hover {\n background: rgb(255 255 255 / 3%);\n }\n\n .atv-discussion-row:hover::before {\n opacity: 1;\n transform: scaleY(1);\n }\n\n .atv-discussion-row:hover .atv-discussion-arrow {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n.atv-hero-teaser-content {\n display: block;\n}\n\n/* Shared affordance for every control that opens the image-preview modal. */\n\n.atv-image-preview-trigger {\n box-shadow: 0 16px 40px rgb(0 0 0 / 46%);\n transition:\n transform var(--atv-duration-hover) var(--atv-ease-out),\n box-shadow var(--atv-duration-hover) var(--atv-ease-out);\n will-change: transform;\n}\n\n.atv-image-preview-trigger:focus-visible {\n outline: 2px solid var(--atv-accent-bright);\n outline-offset: 4px;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .atv-image-preview-trigger:hover {\n box-shadow: 0 24px 64px rgb(0 0 0 / 56%);\n transform: translateY(-3px);\n }\n}\n\n.atv-image-preview-trigger:active {\n transform: scale(0.97);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .atv-image-preview-trigger {\n transition: none;\n will-change: auto;\n }\n\n .atv-carousel {\n scroll-behavior: auto;\n }\n\n .atv-spinner {\n animation: none;\n }\n\n .atv-modal-accent-bar {\n opacity: 1;\n }\n\n .atv-btn-primary:hover,\n .atv-btn-secondary:hover,\n .atv-btn:active,\n .atv-modal-close:active,\n .atv-image-preview-trigger:hover,\n .atv-image-preview-trigger:active,\n .atv-comment-card:hover,\n .atv-comment-votes:active,\n .atv-comment-overlay-votes:active,\n .atv-comment-expand:active,\n .atv-review-card:hover,\n .atv-review-card:hover .atv-review-readmore,\n .atv-review-card:focus-visible .atv-review-readmore,\n .atv-review-card:has(.atv-review-open-button:focus-visible)\n .atv-review-readmore,\n .atv-vote-btn:active,\n .atv-stream-card:hover,\n .atv-interest-modal-star:hover,\n .atv-interest-modal-submit:active,\n .atv-discussion-row:hover::before,\n .atv-discussion-row:has(.atv-discussion-topic-link:focus-visible)::before,\n .atv-discussion-row:has(.atv-discussion-author:focus-visible)::before,\n .atv-discussion-row:active,\n .atv-series-card:hover,\n .atv-cast-card:hover,\n .atv-rec-card:hover .atv-rec-poster,\n .atv-trailer-tile:hover,\n .atv-hero-more.is-open svg {\n transform: none;\n }\n\n .atv-photo-tile {\n animation: none;\n opacity: 1;\n transform: none;\n }\n\n .atv-interest-modal-status-indicator {\n opacity: 0;\n transform: none;\n }\n\n .atv-interest-modal-status.is-active {\n background: rgb(255 255 255 / 12%);\n }\n}\n\n/* ---------- Accessibility: reduced transparency ---------- */\n\n@media (prefers-reduced-transparency: reduce) {\n .atv-stickynav.is-visible:not(.is-scrolling) {\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n background: #0a0a0c;\n }\n\n .atv-modal-overlay,\n .atv-comment-overlay,\n .atv-interest-modal,\n .atv-login-modal,\n .atv-review-modal {\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n background: rgb(0 0 0 / 90%);\n }\n\n .atv-btn-secondary {\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n }\n\n .atv-chip {\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n }\n\n .atv-series-badge {\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n background: rgb(0 0 0 / 80%);\n }\n\n .atv-trailer-label {\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n background: rgb(0 0 0 / 80%);\n }\n\n .atv-comment-expand {\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n }\n}\n\n/* ---------- Accessibility: high contrast ---------- */\n\n@media (prefers-contrast: more) {\n .atv-modal-overlay,\n .atv-comment-overlay,\n .atv-interest-modal,\n .atv-login-modal,\n .atv-review-modal {\n -webkit-backdrop-filter: none;\n backdrop-filter: none;\n background: rgb(0 0 0 / 92%);\n }\n\n .atv-comment-card,\n .atv-review-card {\n background: #181818;\n }\n\n .atv-review-card,\n .atv-stream-card,\n .atv-chip,\n .atv-comment-votes {\n border-color: rgb(255 255 255 / 25%);\n }\n\n .atv-discussion-row {\n border: 1px solid rgb(255 255 255 / 20%);\n border-radius: var(--atv-radius-sm);\n background: #181818;\n }\n\n .atv-chip {\n color: var(--atv-text-primary);\n }\n\n .atv-comment-expand {\n border-color: rgb(255 255 255 / 25%);\n background: rgb(0 0 0 / 50%);\n }\n\n .atv-btn-secondary {\n border-color: rgb(255 255 255 / 30%);\n background: rgb(255 255 255 / 12%);\n }\n\n .atv-series-badge {\n background: rgb(0 0 0 / 85%);\n }\n\n .atv-trailer-label {\n background: rgb(0 0 0 / 85%);\n }\n}\n\n/* ---------- Card press states ---------- */\n\n.atv-poster-card:active {\n transform: scale(0.97);\n}\n\n.atv-stream-card:active {\n transform: scale(0.97);\n}\n\n.atv-photo-tile:active {\n transform: scale(0.97);\n}\n\n.atv-cast-card:active {\n transform: scale(0.97);\n}\n\n.atv-series-card:active {\n transform: scale(0.97);\n}\n\n.atv-comment-card:active {\n transform: scale(0.97);\n}\n\n.atv-discussion-row:active {\n transform: scale(0.97);\n}\n", n, l$1, u$2, i$2, r$1, o$2, e$1, f$2, c$1, a$1, s$1, h$1, p$1, v$1, y$1, d$1 = {}, w$1 = [], _$1 = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i, g$1 = Array.isArray; function m$1(n, l) { for (var u in l) n[u] = l[u]; return n; } function b(n) { n && n.parentNode && n.parentNode.removeChild(n); } function k$1(l, u, t) { var i, r, o, e = {}; for (o in u) "key" == o ? i = u[o] : "ref" == o ? r = u[o] : e[o] = u[o]; if (arguments.length > 2 && (e.children = arguments.length > 3 ? n.call(arguments, 2) : t), "function" == typeof l && null != l.defaultProps) for (o in l.defaultProps) void 0 === e[o] && (e[o] = l.defaultProps[o]); return x$1(l, e, i, r, null); } function x$1(n, t, i, r, o) { var e = { type: n, props: t, key: i, ref: r, __k: null, __: null, __b: 0, __e: null, __c: null, constructor: void 0, __v: null == o ? ++u$2 : o, __i: -1, __u: 0 }; return null == o && null != l$1.vnode && l$1.vnode(e), e; } function S(n) { return n.children; } function C$1(n, l) { this.props = n, this.context = l; } function $$1(n, l) { if (null == l) return n.__ ? $$1(n.__, n.__i + 1) : null; for (var u; l < n.__k.length; l++) if (null != (u = n.__k[l]) && null != u.__e) return u.__e; return "function" == typeof n.type ? $$1(n) : null; } function I(n) { if (n.__P && n.__d) { var u = n.__v, t = u.__e, i = [], r = [], o = m$1({}, u); o.__v = u.__v + 1, l$1.vnode && l$1.vnode(o), q$1(n.__P, o, u, n.__n, n.__P.namespaceURI, 32 & u.__u ? [t] : null, i, null == t ? $$1(u) : t, !!(32 & u.__u), r), o.__v = u.__v, o.__.__k[o.__i] = o, D$1(i, o, r), u.__e = u.__ = null, o.__e != t && P(o); } } function P(n) { if (null != (n = n.__) && null != n.__c) return n.__e = n.__c.base = null, n.__k.some(function(l) { if (null != l && null != l.__e) return n.__e = n.__c.base = l.__e; }), P(n); } function A$1(n) { (!n.__d && (n.__d = !0) && i$2.push(n) && !H.__r++ || r$1 != l$1.debounceRendering) && ((r$1 = l$1.debounceRendering) || o$2)(H); } function H() { try { for (var n, l = 1; i$2.length;) i$2.length > l && i$2.sort(e$1), n = i$2.shift(), l = i$2.length, I(n); } finally { i$2.length = H.__r = 0; } } function L(n, l, u, t, i, r, o, e, f, c, a) { var s, h, p, v, y, _, g, m = t && t.__k || w$1, b = l.length; for (f = T$1(u, l, m, f, b), s = 0; s < b; s++) null != (p = u.__k[s]) && (h = -1 != p.__i && m[p.__i] || d$1, p.__i = s, _ = q$1(n, p, h, i, r, o, e, f, c, a), v = p.__e, p.ref && h.ref != p.ref && (h.ref && J(h.ref, null, p), a.push(p.ref, p.__c || v, p)), null == y && null != v && (y = v), (g = !!(4 & p.__u)) || h.__k === p.__k ? (f = j$1(p, f, n, g), g && h.__e && (h.__e = null)) : "function" == typeof p.type && void 0 !== _ ? f = _ : v && (f = v.nextSibling), p.__u &= -7); return u.__e = y, f; } function T$1(n, l, u, t, i) { var r, o, e, f, c, a = u.length, s = a, h = 0; for (n.__k = new Array(i), r = 0; r < i; r++) null != (o = l[r]) && "boolean" != typeof o && "function" != typeof o ? ("string" == typeof o || "number" == typeof o || "bigint" == typeof o || o.constructor == String ? o = n.__k[r] = x$1(null, o, null, null, null) : g$1(o) ? o = n.__k[r] = x$1(S, { children: o }, null, null, null) : void 0 === o.constructor && o.__b > 0 ? o = n.__k[r] = x$1(o.type, o.props, o.key, o.ref ? o.ref : null, o.__v) : n.__k[r] = o, f = r + h, o.__ = n, o.__b = n.__b + 1, e = null, -1 != (c = o.__i = O(o, u, f, s)) && (s--, (e = u[c]) && (e.__u |= 2)), null == e || null == e.__v ? (-1 == c && (i > a ? h-- : i < a && h++), "function" != typeof o.type && (o.__u |= 4)) : c != f && (c == f - 1 ? h-- : c == f + 1 ? h++ : (c > f ? h-- : h++, o.__u |= 4))) : n.__k[r] = null; if (s) for (r = 0; r < a; r++) null != (e = u[r]) && 0 == (2 & e.__u) && (e.__e == t && (t = $$1(e)), K(e, e)); return t; } function j$1(n, l, u, t) { var i, r; if ("function" == typeof n.type) { for (i = n.__k, r = 0; i && r < i.length; r++) i[r] && (i[r].__ = n, l = j$1(i[r], l, u, t)); return l; } n.__e != l && (t && (l && n.type && !l.parentNode && (l = $$1(n)), u.insertBefore(n.__e, l || null)), l = n.__e); do l = l && l.nextSibling; while (null != l && 8 == l.nodeType); return l; } function O(n, l, u, t) { var i, r, o, e = n.key, f = n.type, c = l[u], a = null != c && 0 == (2 & c.__u); if (null === c && null == e || a && e == c.key && f == c.type) return u; if (t > (a ? 1 : 0)) { for (i = u - 1, r = u + 1; i >= 0 || r < l.length;) if (null != (c = l[o = i >= 0 ? i-- : r++]) && 0 == (2 & c.__u) && e == c.key && f == c.type) return o; } return -1; } function z$1(n, l, u) { "-" == l[0] ? n.setProperty(l, null == u ? "" : u) : n[l] = null == u ? "" : "number" != typeof u || _$1.test(l) ? u : u + "px"; } function N(n, l, u, t, i) { var r, o; n: if ("style" == l) if ("string" == typeof u) n.style.cssText = u; else { if ("string" == typeof t && (n.style.cssText = t = ""), t) for (l in t) u && l in u || z$1(n.style, l, ""); if (u) for (l in u) t && u[l] == t[l] || z$1(n.style, l, u[l]); } else if ("o" == l[0] && "n" == l[1]) r = l != (l = l.replace(s$1, "$1")), o = l.toLowerCase(), l = o in n || "onFocusOut" == l || "onFocusIn" == l ? o.slice(2) : l.slice(2), n.l || (n.l = {}), n.l[l + r] = u, u ? t ? u[a$1] = t[a$1] : (u[a$1] = h$1, n.addEventListener(l, r ? v$1 : p$1, r)) : n.removeEventListener(l, r ? v$1 : p$1, r); else { if ("http://www.w3.org/2000/svg" == i) l = l.replace(/xlink(H|:h)/, "h").replace(/sName$/, "s"); else if ("width" != l && "height" != l && "href" != l && "list" != l && "form" != l && "tabIndex" != l && "download" != l && "rowSpan" != l && "colSpan" != l && "role" != l && "popover" != l && l in n) try { n[l] = null == u ? "" : u; break n; } catch (n) {} "function" == typeof u || (null == u || !1 === u && "-" != l[4] ? n.removeAttribute(l) : n.setAttribute(l, "popover" == l && 1 == u ? "" : u)); } } function V(n) { return function(u) { if (this.l) { var t = this.l[u.type + n]; if (null == u[c$1]) u[c$1] = h$1++; else if (u[c$1] < t[a$1]) return; return t(l$1.event ? l$1.event(u) : u); } }; } function q$1(n, u, t, i, r, o, e, f, c, a) { var s, h, p, v, y, d, _, k, x, M, $, I, P, A, H, T, j = u.type; if (void 0 !== u.constructor) return null; 128 & t.__u && (c = !!(32 & t.__u), o = [f = u.__e = t.__e]), (s = l$1.__b) && s(u); n: if ("function" == typeof j) { h = e.length; try { if (x = u.props, M = j.prototype && j.prototype.render, $ = (s = j.contextType) && i[s.__c], I = s ? $ ? $.props.value : s.__ : i, t.__c ? k = (p = u.__c = t.__c).__ = p.__E : (M ? u.__c = p = new j(x, I) : (u.__c = p = new C$1(x, I), p.constructor = j, p.render = Q), $ && $.sub(p), p.state || (p.state = {}), p.__n = i, v = p.__d = !0, p.__h = [], p._sb = []), M && null == p.__s && (p.__s = p.state), M && null != j.getDerivedStateFromProps && (p.__s == p.state && (p.__s = m$1({}, p.__s)), m$1(p.__s, j.getDerivedStateFromProps(x, p.__s))), y = p.props, d = p.state, p.__v = u, v) M && null == j.getDerivedStateFromProps && null != p.componentWillMount && p.componentWillMount(), M && null != p.componentDidMount && p.__h.push(p.componentDidMount); else { if (M && null == j.getDerivedStateFromProps && x !== y && null != p.componentWillReceiveProps && p.componentWillReceiveProps(x, I), u.__v == t.__v || !p.__e && null != p.shouldComponentUpdate && !1 === p.shouldComponentUpdate(x, p.__s, I)) { u.__v != t.__v && (p.props = x, p.state = p.__s, p.__d = !1), u.__e = t.__e, u.__k = t.__k, u.__k.some(function(n) { n && (n.__ = u); }), w$1.push.apply(p.__h, p._sb), p._sb = [], p.__h.length && e.push(p); break n; } null != p.componentWillUpdate && p.componentWillUpdate(x, p.__s, I), M && null != p.componentDidUpdate && p.__h.push(function() { p.componentDidUpdate(y, d, _); }); } if (p.context = I, p.props = x, p.__P = n, p.__e = !1, P = l$1.__r, A = 0, M) p.state = p.__s, p.__d = !1, P && P(u), s = p.render(p.props, p.state, p.context), w$1.push.apply(p.__h, p._sb), p._sb = []; else do p.__d = !1, P && P(u), s = p.render(p.props, p.state, p.context), p.state = p.__s; while (p.__d && ++A < 25); p.state = p.__s, null != p.getChildContext && (i = m$1(m$1({}, i), p.getChildContext())), M && !v && null != p.getSnapshotBeforeUpdate && (_ = p.getSnapshotBeforeUpdate(y, d)), H = null != s && s.type === S && null == s.key ? E(s.props.children) : s, f = L(n, g$1(H) ? H : [H], u, t, i, r, o, e, f, c, a), p.base = u.__e, u.__u &= -161, p.__h.length && e.push(p), k && (p.__E = p.__ = null); } catch (n) { if (e.length = h, u.__v = null, c || null != o) { if (n.then) { for (u.__u |= c ? 160 : 128; f && 8 == f.nodeType && f.nextSibling;) f = f.nextSibling; null != o && (o[o.indexOf(f)] = null), u.__e = f; } else if (null != o) for (T = o.length; T--;) b(o[T]); } else u.__e = t.__e; u.__k ??= t.__k || [], n.then || B$1(u), l$1.__e(n, u, t); } } else null == o && u.__v == t.__v ? (u.__k = t.__k, u.__e = t.__e) : f = u.__e = G(t.__e, u, t, i, r, o, e, c, a); return (s = l$1.diffed) && s(u), 128 & u.__u ? void 0 : f; } function B$1(n) { n && (n.__c && (n.__c.__e = !0), n.__k && n.__k.some(B$1)); } function D$1(n, u, t) { for (var i = 0; i < t.length; i++) J(t[i], t[++i], t[++i]); l$1.__c && l$1.__c(u, n), n.some(function(u) { try { n = u.__h, u.__h = [], n.some(function(n) { n.call(u); }); } catch (n) { l$1.__e(n, u.__v); } }); } function E(n) { return "object" != typeof n || null == n || n.__b > 0 ? n : g$1(n) ? n.map(E) : void 0 !== n.constructor ? null : m$1({}, n); } function G(u, t, i, r, o, e, f, c, a) { var s, h, p, v, y, w, _, m = i.props || d$1, k = t.props, x = t.type; if ("svg" == x ? o = "http://www.w3.org/2000/svg" : "math" == x ? o = "http://www.w3.org/1998/Math/MathML" : o || (o = "http://www.w3.org/1999/xhtml"), null != e) { for (s = 0; s < e.length; s++) if ((y = e[s]) && "setAttribute" in y == !!x && (x ? y.localName == x : 3 == y.nodeType)) { u = y, e[s] = null; break; } } if (null == u) { if (null == x) return document.createTextNode(k); u = document.createElementNS(o, x, k.is && k), c && (l$1.__m && l$1.__m(t, e), c = !1), e = null; } if (null == x) m === k || c && u.data == k || (u.data = k); else { if (e = "textarea" == x && null != k.defaultValue ? null : e && n.call(u.childNodes), !c && null != e) for (m = {}, s = 0; s < u.attributes.length; s++) m[(y = u.attributes[s]).name] = y.value; for (s in m) y = m[s], "dangerouslySetInnerHTML" == s ? p = y : "children" == s || s in k || "value" == s && "defaultValue" in k || "checked" == s && "defaultChecked" in k || N(u, s, null, y, o); for (s in k) y = k[s], "children" == s ? v = y : "dangerouslySetInnerHTML" == s ? h = y : "value" == s ? w = y : "checked" == s ? _ = y : c && "function" != typeof y || m[s] === y || N(u, s, y, m[s], o); if (h) c || p && (h.__html == p.__html || h.__html == u.innerHTML) || (u.innerHTML = h.__html), t.__k = []; else if (p && (u.innerHTML = ""), L("template" == t.type ? u.content : u, g$1(v) ? v : [v], t, i, r, "foreignObject" == x ? "http://www.w3.org/1999/xhtml" : o, e, f, e ? e[0] : i.__k && $$1(i, 0), c, a), null != e) for (s = e.length; s--;) b(e[s]); c && "textarea" != x || (s = "value", "progress" == x && null == w ? u.removeAttribute("value") : null != w && (w !== u[s] || "progress" == x && !w || "option" == x && w != m[s]) && N(u, s, w, m[s], o), s = "checked", null != _ && _ != u[s] && N(u, s, _, m[s], o)); } return u; } function J(n, u, t) { try { if ("function" == typeof n) { var i = "function" == typeof n.__u; i && n.__u(), i && null == u || (n.__u = n(u)); } else n.current = u; } catch (n) { l$1.__e(n, t); } } function K(n, u, t) { var i, r; if (l$1.unmount && l$1.unmount(n), (i = n.ref) && (i.current && i.current != n.__e || J(i, null, u)), null != (i = n.__c)) { if (i.componentWillUnmount) try { i.componentWillUnmount(); } catch (n) { l$1.__e(n, u); } i.base = i.__P = i.__n = null; } if (i = n.__k) for (r = 0; r < i.length; r++) i[r] && K(i[r], u, t || "function" != typeof n.type); t || b(n.__e), n.__c = n.__ = n.__e = void 0; } function Q(n, l, u) { return this.constructor(n, u); } function R(u, t, i) { var r, o, e, f; t == document && (t = document.documentElement), l$1.__ && l$1.__(u, t), o = (r = "function" == typeof i) ? null : i && i.__k || t.__k, e = [], f = [], q$1(t, u = (!r && i || t).__k = k$1(S, null, [u]), o || d$1, d$1, t.namespaceURI, !r && i ? [i] : o ? null : t.firstChild ? n.call(t.childNodes) : null, e, !r && i ? i : o ? o.__e : t.firstChild, r, f), D$1(e, u, f), u.props.children = null; } function X(n) { function l(n) { var u, t; return this.getChildContext || (u = new Set(), (t = {})[l.__c] = this, this.getChildContext = function() { return t; }, this.componentWillUnmount = function() { u = null; }, this.shouldComponentUpdate = function(n) { this.props.value != n.value && u.forEach(function(n) { n.__e = !0, A$1(n); }); }, this.sub = function(n) { u.add(n); var l = n.componentWillUnmount; n.componentWillUnmount = function() { u && u.delete(n), l && l.call(n); }; }), n.children; } return l.__c = "__cC" + y$1++, l.__ = n, l.Provider = l.__l = (l.Consumer = function(n, l) { return n.children(l); }).contextType = l, l; } n = w$1.slice, l$1 = { __e: function(n, l, u, t) { for (var i, r, o; l = l.__;) if ((i = l.__c) && !i.__) try { if ((r = i.constructor) && null != r.getDerivedStateFromError && (i.setState(r.getDerivedStateFromError(n)), o = i.__d), null != i.componentDidCatch && (i.componentDidCatch(n, t || {}), o = i.__d), o) return i.__E = i; } catch (l) { n = l; } throw n; } }, u$2 = 0, C$1.prototype.setState = function(n, l) { var u = null != this.__s && this.__s != this.state ? this.__s : this.__s = m$1({}, this.state); "function" == typeof n && (n = n(m$1({}, u), this.props)), n && m$1(u, n), null != n && this.__v && (l && this._sb.push(l), A$1(this)); }, C$1.prototype.forceUpdate = function(n) { this.__v && (this.__e = !0, n && this.__h.push(n), A$1(this)); }, C$1.prototype.render = S, i$2 = [], o$2 = "function" == typeof Promise ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout, e$1 = function(n, l) { return n.__v.__b - l.__v.__b; }, H.__r = 0, f$2 = Math.random().toString(8), c$1 = "__d" + f$2, a$1 = "__a" + f$2, s$1 = /(PointerCapture)$|Capture$/i, h$1 = 0, p$1 = V(!1), v$1 = V(!0), y$1 = 0; var activateEnhancedDocument = (doc) => { doc.body.classList.add("atv-enhanced"); }; var installEnhancedRoot = (doc, renderRoot) => { const root = doc.createElement("div"); root.id = "atv-douban-root"; try { renderRoot(root); doc.body.insertBefore(root, doc.body.firstChild); } catch (error) { root.remove(); console.warn("[ATV-Douban] 页面挂载失败:", error); return false; } activateEnhancedDocument(doc); return true; }; var t, r, u$1, i$1, o$1 = 0, f$1 = [], c = l$1, e = c.__b, a = c.__r, v = c.diffed, l = c.__c, m = c.unmount, p = c.__; function s(n, t) { c.__h && c.__h(r, n, o$1 || t), o$1 = 0; var u = r.__H || (r.__H = { __: [], __h: [] }); return n >= u.__.length && u.__.push({}), u.__[n]; } function d(n) { return o$1 = 1, y(D, n); } function y(n, u, i) { var o = s(t++, 2); if (o.t = n, !o.__c && (o.__ = [i ? i(u) : D(void 0, u), function(n) { var t = o.__N ? o.__N[0] : o.__[0], r = o.t(t, n); t !== r && (o.__N = [r, o.__[1]], o.__c.setState({})); }], o.__c = r, !r.__f)) { var f = function(n, t, r) { if (!o.__c.__H) return !0; var u = !1, i = o.__c.props !== n; if (o.__c.__H.__.some(function(n) { if (n.__N) { u = !0; var t = n.__[0]; n.__ = n.__N, n.__N = void 0, t !== n.__[0] && (i = !0); } }), c) { var f = c.call(this, n, t, r); return u ? f || i : f; } return !u || i; }; r.__f = !0; var c = r.shouldComponentUpdate, e = r.componentWillUpdate; r.componentWillUpdate = function(n, t, r) { if (this.__e) { var u = c; c = void 0, f(n, t, r), c = u; } e && e.call(this, n, t, r); }, r.shouldComponentUpdate = f; } return o.__N || o.__; } function h(n, u) { var i = s(t++, 3); !c.__s && C(i.__H, u) && (i.__ = n, i.u = u, r.__H.__h.push(i)); } function _(n, u) { var i = s(t++, 4); !c.__s && C(i.__H, u) && (i.__ = n, i.u = u, r.__h.push(i)); } function A(n) { return o$1 = 5, T(function() { return { current: n }; }, []); } function T(n, r) { var u = s(t++, 7); return C(u.__H, r) && (u.__ = n(), u.__H = r, u.__h = n), u.__; } function q(n, t) { return o$1 = 8, T(function() { return n; }, t); } function x(n) { var u = r.context[n.__c], i = s(t++, 9); return i.c = n, u ? (i.__ ?? (i.__ = !0, u.sub(r)), u.props.value) : n.__; } function g() { var n = s(t++, 11); if (!n.__) { for (var u = r.__v; null !== u && !u.__m && null !== u.__;) u = u.__; var i = u.__m || (u.__m = [0, 0]); n.__ = "P" + i[0] + "-" + i[1]++; } return n.__; } function j() { for (var n; n = f$1.shift();) { var t = n.__H; if (n.__P && t) try { t.__h.some(z), t.__h.some(B), t.__h = []; } catch (r) { t.__h = [], c.__e(r, n.__v); } } } c.__b = function(n) { r = null, e && e(n); }, c.__ = function(n, t) { n && t.__k && t.__k.__m && (n.__m = t.__k.__m), p && p(n, t); }, c.__r = function(n) { a && a(n), t = 0; var i = (r = n.__c).__H; i && (u$1 === r ? (i.__h = [], r.__h = [], i.__.some(function(n) { n.__N && (n.__ = n.__N), n.u = n.__N = void 0; })) : (i.__h.some(z), i.__h.some(B), i.__h = [], t = 0)), u$1 = r; }, c.diffed = function(n) { v && v(n); var t = n.__c; t && t.__H && (t.__H.__h.length && (1 !== f$1.push(t) && i$1 === c.requestAnimationFrame || ((i$1 = c.requestAnimationFrame) || w)(j)), t.__H.__.some(function(n) { n.u && (n.__H = n.u, n.u = void 0); })), u$1 = r = null; }, c.__c = function(n, t) { t.some(function(n) { try { n.__h.some(z), n.__h = n.__h.filter(function(n) { return !n.__ || B(n); }); } catch (r) { t.some(function(n) { n.__h && (n.__h = []); }), t = [], c.__e(r, n.__v); } }), l && l(n, t); }, c.unmount = function(n) { m && m(n); var t, r = n.__c; r && r.__H && (r.__H.__.some(function(n) { try { z(n); } catch (n) { t = n; } }), r.__H = void 0, t && c.__e(t, r.__v)); }; var k = "function" == typeof requestAnimationFrame; function w(n) { var t, r = function() { clearTimeout(u), k && cancelAnimationFrame(t), setTimeout(n); }, u = setTimeout(r, 35); k && (t = requestAnimationFrame(r)); } function z(n) { var t = r, u = n.__c; "function" == typeof u && (n.__c = void 0, u()), r = t; } function B(n) { var t = r; n.__c = n.__(), r = t; } function C(n, t) { return !n || n.length !== t.length || t.some(function(t, r) { return t !== n[r]; }); } function D(n, t) { return "function" == typeof t ? t(n) : t; } var $ = (selector, ctx) => (ctx ?? document).querySelector(selector); var $$ = (selector, ctx) => [...(ctx ?? document).querySelectorAll(selector)]; var safeText = (el) => el ? (el.textContent ?? "").trim() : ""; var personageIdFromPath = (pathname) => pathname.match(/^\/personage\/(?\d+)\/?$/u)?.groups?.id ?? null; var imageUrl = (element) => { if (!element) return null; if (element.localName === "img") { const image = element; return image.currentSrc || image.src || null; } return (element.getAttribute("style") ?? "").match(/url\(["']?(?[^"')]+)["']?\)/u)?.groups?.url ?? null; }; var largePhotoUrl = (src) => { if (src.includes("/view/photo/photo/")) return src.replace("/view/photo/photo/", "/view/photo/l/"); return src; }; var extractFacts = (target) => $$(".subject-property li", target).flatMap((item) => { const label = safeText(item.querySelector(".label")); const value = safeText(item.querySelector(".value")); return label && value ? [{ label, value }] : []; }); var extractBiography = (doc) => { const biography = $$(".subject-intro p", doc).map(safeText).filter((paragraph) => paragraph && paragraph !== "暂无"); return biography.length > 0 ? biography : null; }; var extractCollaborators = (doc) => { const source = doc.querySelector(".partners-mod-mod"); if (!source) return null; const collaborators = $$(".partners-mod-item", source).flatMap((item, nativeIndex) => { const profileLink = item.querySelector(".partners-mod-info a[title][href]"); const sharedWorksLink = item.querySelector(".partners-mod-info a[href*=\"partners#\"]"); const name = safeText(profileLink); const sharedWorkCount = Number(safeText(sharedWorksLink)); if (!(profileLink && name && Number.isFinite(sharedWorkCount))) return []; return [{ avatar: imageUrl(item.querySelector("img")), href: profileLink.href, name, nativeIndex, sharedWorkCount, sharedWorksHref: sharedWorksLink?.href ?? null }]; }).toSorted((left, right) => right.sharedWorkCount - left.sharedWorkCount || left.nativeIndex - right.nativeIndex).map(({ nativeIndex: _nativeIndex, ...collaborator }) => collaborator); const allCollaboratorsHref = source.querySelector("h2 a[href]")?.href ?? null; const totalCountMatch = source.querySelector("h2 a[href]")?.textContent?.match(/\d+/u)?.[0]; return { allCollaboratorsHref, ...totalCountMatch ? { totalCount: Number(totalCountMatch) } : {}, collaborators }; }; var hrefOf = (element) => element?.localName === "a" ? element.href : null; var extractAwards$1 = (doc) => { const source = doc.querySelector(".subject-awards"); if (!source) return null; const awards = $$("li", source).flatMap((item) => { const [yearElement, ceremonyElement, awardElement, workElement] = [...item.children]; const year = safeText(yearElement); const ceremony = safeText(ceremonyElement); const award = safeText(awardElement); if (!(year && ceremony && award)) return []; return [{ award, ceremony, ceremonyHref: hrefOf(ceremonyElement), work: safeText(workElement) || null, workHref: hrefOf(workElement), year }]; }); const allAwardsLink = source.querySelector("h2 a[href]"); const allAwardsHref = allAwardsLink?.href ?? null; const awardsCountMatch = allAwardsLink?.textContent?.match(/\d+/u)?.[0]; return { allAwardsHref, ...awardsCountMatch ? { totalCount: Number(awardsCountMatch) } : {}, awards }; }; var extractGallery = (doc, personageId) => { const galleryPath = `/personage/${personageId}/photos`; const source = doc.querySelector(".subject-picture"); if (!source) return null; const allImagesLink = $$("a[href]", source).find((link) => new URL(link.getAttribute("href") ?? "", doc.baseURI).pathname.replace(/\/$/u, "") === galleryPath); const images = $$("li.picture", source).flatMap((image) => { const src = imageUrl(image); if (!src) return []; return [{ alt: image.getAttribute("aria-label")?.trim() ?? "", largeSrc: largePhotoUrl(src), src }]; }); return { allImagesHref: allImagesLink?.href ?? null, images }; }; var extractWorkRail = (doc, matchesHeading) => { const source = $$(".subject-creations", doc).find((section) => matchesHeading(safeText(section.querySelector("h2")))); if (!source) return null; let lastYear = null; const works = $$("li.creation", source).flatMap((item) => { const posterLink = item.querySelector("a.img_wrap[href]"); const title = posterLink?.title.trim() ?? ""; if (!posterLink || !title) return []; const yearElement = item.querySelector(".timeline-year") ?? item.querySelector(".pl"); const rawYear = yearElement ? safeText(yearElement).trim() : ""; const currentYear = /^\d{4}$/u.test(rawYear) ? rawYear : lastYear; if (/^\d{4}$/u.test(rawYear)) lastYear = rawYear; return [{ href: posterLink.href, poster: imageUrl(posterLink.querySelector("img")) ?? "", rating: safeText(item.querySelector(".rating-val")) || null, title, year: currentYear }]; }); const allWorksLink = $$("a[href]", source).find((link) => new URL(link.href).pathname.includes("/creations")); const allWorksHref = allWorksLink?.href; const totalCountMatch = allWorksLink?.textContent?.match(/\d+/u)?.[0]; return { allWorksHref: allWorksHref ?? null, ...totalCountMatch ? { totalCount: Number(totalCountMatch) } : {}, works }; }; var extractPersonageProfile = (doc) => { const target = doc.querySelector(".subject-target"); const name = safeText(target?.querySelector(".subject-name")); const id = personageIdFromPath(doc.defaultView?.location.pathname ?? ""); if (!target || !name || !id) return null; return { awards: extractAwards$1(doc), biography: extractBiography(doc), collaborators: extractCollaborators(doc), facts: extractFacts(target), gallery: extractGallery(doc, id), id, name, portrait: imageUrl(target.querySelector(".avatar img, .subject-avatar img") ?? target.querySelector(".avatar, .subject-avatar")), recentWorks: extractWorkRail(doc, (heading) => heading.includes("最近")), representativeWorks: extractWorkRail(doc, (heading) => heading.includes("收藏人数最多")) }; }, f = 0; Array.isArray; function u(e, t, n, o, i, u) { t || (t = {}); var a, c, p = t; if ("ref" in p) for (c in p = {}, t) "ref" == c ? a = t[c] : p[c] = t[c]; var l = { type: e, props: p, key: n, ref: a, __k: null, __: null, __b: 0, __e: null, __c: null, constructor: void 0, __v: --f, __i: -1, __u: 0, __source: i, __self: u }; if ("function" == typeof e && (a = e.defaultProps)) for (c in a) void 0 === p[c] && (p[c] = a[c]); return l$1.vnode && l$1.vnode(l), l; } var IconStarFull = (props) => u("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", ...props, children: u("path", { fill: "currentColor", d: "M8 1.2l2.06 4.18 4.61.67-3.34 3.25.79 4.6L8 11.74l-4.12 2.16.79-4.6L1.33 6.05l4.61-.67L8 1.2z" }) }); var IconStarHalf = (props) => u("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", ...props, children: [u("defs", { children: u("linearGradient", { id: "atvHalfStar", children: [u("stop", { offset: "50%", "stop-color": "currentColor" }), u("stop", { offset: "50%", "stop-color": "currentColor", "stop-opacity": "0.22" })] }) }), u("path", { fill: "url(#atvHalfStar)", d: "M8 1.2l2.06 4.18 4.61.67-3.34 3.25.79 4.6L8 11.74l-4.12 2.16.79-4.6L1.33 6.05l4.61-.67L8 1.2z" })] }); var IconStarEmpty = (props) => u("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", ...props, children: u("path", { fill: "currentColor", "fill-opacity": "0.22", d: "M8 1.2l2.06 4.18 4.61.67-3.34 3.25.79 4.6L8 11.74l-4.12 2.16.79-4.6L1.33 6.05l4.61-.67L8 1.2z" }) }); var IconPlay = (props) => u("svg", { viewBox: "0 0 14 14", width: "14", height: "14", "aria-hidden": "true", ...props, children: u("path", { fill: "currentColor", d: "M3 1.6v10.8c0 .8.86 1.27 1.5.83l8.1-5.4a1 1 0 0 0 0-1.66L4.5.77C3.86.33 3 .8 3 1.6z" }) }); var IconCheck = (props) => u("svg", { viewBox: "0 0 14 14", width: "14", height: "14", "aria-hidden": "true", ...props, children: u("path", { fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", d: "M2.5 7.5l3 3 6-7" }) }); var IconChevron = (props) => u("svg", { viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", ...props, children: u("path", { fill: "none", stroke: "currentColor", "stroke-width": "1.6", "stroke-linecap": "round", "stroke-linejoin": "round", d: "M2.5 4l3.5 4 3.5-4" }) }); var IconArrow = (props) => u("svg", { viewBox: "0 0 16 16", width: "15", height: "15", "aria-hidden": "true", ...props, children: u("path", { fill: "none", stroke: "currentColor", "stroke-width": "1.8", "stroke-linecap": "round", "stroke-linejoin": "round", d: "M3 8h9m0 0l-3.5-3.5M12 8l-3.5 3.5" }) }); var IconThumb = (props) => u("svg", { viewBox: "0 0 16 16", width: "13", height: "13", "aria-hidden": "true", ...props, children: u("path", { fill: "currentColor", d: "M6.3 6.6L9 1.3c.7-.1 1.4.5 1.4 1.3v2.6h3c.9 0 1.5.8 1.3 1.6l-1.2 5.1c-.1.6-.7 1-1.3 1H6.3V6.6zM4.7 6.7v7H2.5c-.6 0-1-.4-1-1v-5c0-.6.4-1 1-1h2.2z" }) }); var IconVoteTriangle = (props) => u("svg", { viewBox: "0 0 12 12", width: "12", height: "12", "aria-hidden": "true", ...props, children: u("path", { fill: "currentColor", d: "M6 2.2 10.4 9H1.6L6 2.2z" }) }); var IconExpand = (props) => u("svg", { viewBox: "0 0 14 14", width: "12", height: "12", "aria-hidden": "true", ...props, children: u("path", { fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round", d: "M3 5.5l4 4 4-4" }) }); var IconTomato = (props) => u("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", ...props, children: u("circle", { cx: "8", cy: "8", r: "5.5", fill: "currentColor" }) }); var IconPopcorn = (props) => u("svg", { viewBox: "0 0 16 16", width: "16", height: "16", "aria-hidden": "true", ...props, children: u("path", { fill: "currentColor", d: "M8 2l6 6-6 6-6-6z" }) }); var IconClose = ({ size = 22, ...props }) => u("svg", { viewBox: "0 0 24 24", width: size, height: size, fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", ...props, children: u("path", { d: "M6 6l12 12M18 6l-12 12" }) }); var IconFilmPlaceholder = (props) => u("svg", { viewBox: "0 0 64 96", width: "100%", height: "100%", preserveAspectRatio: "xMidYMid slice", "aria-hidden": "true", ...props, children: [ u("defs", { children: u("linearGradient", { id: "atvFilmGrad", x1: "0", y1: "0", x2: "1", y2: "1", children: [u("stop", { offset: "0%", "stop-color": "#2c2c2e" }), u("stop", { offset: "100%", "stop-color": "#1c1c1e" })] }) }), u("rect", { width: "64", height: "96", fill: "url(#atvFilmGrad)" }), u("g", { fill: "rgba(255,255,255,0.12)", children: [ u("rect", { x: "4", y: "6", width: "6", height: "6", rx: "1" }), u("rect", { x: "4", y: "18", width: "6", height: "6", rx: "1" }), u("rect", { x: "4", y: "30", width: "6", height: "6", rx: "1" }), u("rect", { x: "4", y: "42", width: "6", height: "6", rx: "1" }), u("rect", { x: "4", y: "54", width: "6", height: "6", rx: "1" }), u("rect", { x: "4", y: "66", width: "6", height: "6", rx: "1" }), u("rect", { x: "4", y: "78", width: "6", height: "6", rx: "1" }), u("rect", { x: "54", y: "6", width: "6", height: "6", rx: "1" }), u("rect", { x: "54", y: "18", width: "6", height: "6", rx: "1" }), u("rect", { x: "54", y: "30", width: "6", height: "6", rx: "1" }), u("rect", { x: "54", y: "42", width: "6", height: "6", rx: "1" }), u("rect", { x: "54", y: "54", width: "6", height: "6", rx: "1" }), u("rect", { x: "54", y: "66", width: "6", height: "6", rx: "1" }), u("rect", { x: "54", y: "78", width: "6", height: "6", rx: "1" }) ] }), u("path", { d: "M26 38l14 10-14 10z", fill: "rgba(255,255,255,0.28)" }) ] }); var LogoDouban = (props) => u("svg", { viewBox: "0 0 24 24", width: "24", height: "24", "aria-label": "豆瓣", fill: "#2D963D", ...props, children: u("path", { d: "M.51 3.06h22.98V.755H.51V3.06Zm20.976 2.537v9.608h-2.137l-1.669 5.76H24v2.28H0v-2.28h6.32l-1.67-5.76H2.515V5.597h18.972Zm-5.066 9.608H7.58l1.67 5.76h5.501l1.67-5.76ZM18.367 7.9H5.634v5.025h12.733V7.9Z" }) }); var LogoImdb = (props) => u("svg", { viewBox: "0 0 24 24", width: "24", height: "24", "aria-label": "IMDb", fill: "#F5C518", ...props, children: u("path", { d: "M22.3781 0H1.6218C.7411.0583.0587.7437.0018 1.5953l-.001 20.783c.0585.8761.7125 1.543 1.5559 1.6191A.337.337 0 0 0 1.6016 24h20.7971a.4579.4579 0 0 0 .0437-.002c.8727-.0768 1.5568-.8271 1.5568-1.7085V1.7098c0-.8914-.696-1.6416-1.584-1.7078A.3294.3294 0 0 0 22.3781 0zm0 .496a1.2144 1.2144 0 0 1 1.1252 1.2139v20.5797c0 .6377-.4875 1.1602-1.1045 1.2145H1.6016c-.5967-.0543-1.0645-.5297-1.1053-1.1258V1.6284C.5371 1.0185 1.0184.5364 1.6217.496h20.7564zM4.7954 8.2603v7.3636H2.8899V8.2603h1.9055zm6.5367 0v7.3636H9.6707v-4.9704l-.6711 4.9704H7.813l-.6986-4.8618-.0066 4.8618h-1.668V8.2603h2.468c.0748.4476.1492.9694.2307 1.5734l.2712 1.8713.4407-3.4447h2.4817zm2.9772 1.3289c.0742.0404.122.108.1417.2034.0279.0953.0345.3118.0345.6442v2.8548c0 .4881-.0345.7867-.0955.8954-.0609.1152-.2304.1695-.5018.1695V9.5211c.204 0 .3457.0205.4211.0681zm-.0211 6.0347c.4543 0 .8006-.0265 1.0245-.0742.2304-.0477.4204-.1357.5694-.2648.1556-.1218.2642-.298.3251-.5219.0611-.2238.1021-.6648.1021-1.3224v-2.5832c0-.6986-.0271-1.1668-.0742-1.4039-.041-.237-.1431-.4543-.3126-.6437-.1695-.1973-.4198-.3324-.7456-.421-.3191-.0808-.8542-.1285-1.7694-.1285h-1.4244v7.3636h2.3051zm5.14-1.7827c0 .3523-.0199.5762-.0544.6708-.033.0947-.1894.1424-.3046.1424-.1086 0-.19-.0477-.2238-.1351-.041-.0887-.0609-.2986-.0609-.6238v-1.9469c0-.3324.0199-.5423.0543-.6237.0338-.0808.1086-.122.2171-.122.1153 0 .2709.0412.3114.1425.041.0947.0609.2986.0609.6032v1.8926zm-2.4747-5.5809v7.3636h1.7157l.1152-.4675c.1556.1894.3251.3324.5152.4271.1828.0881.4608.1357.678.1357.3047 0 .5629-.0748.7802-.237.2165-.1562.3589-.3462.4198-.5628.0543-.2173.0887-.543.0887-.9841v-2.0675c0-.4409-.0139-.7324-.0344-.8681-.0199-.1357-.0742-.2781-.1695-.4204-.1021-.1425-.2437-.251-.4272-.3325-.1834-.0742-.3999-.1152-.6576-.1152-.2172 0-.4952.0477-.6846.1285-.1835.0887-.353.2238-.5086.4007V8.2603h-1.8309z" }) }); var LogoMetacritic = (props) => u("svg", { viewBox: "0 0 24 24", width: "24", height: "24", "aria-label": "Metacritic", fill: "#FFD500", ...props, children: u("path", { d: "M11.99 0A12 12 0 1 0 24 12v-.014A12 12 0 0 0 11.99 0Zm-.055 2.564a9.399 9.399 0 0 1 9.407 9.389v.01a9.399 9.399 0 1 1-9.408-9.399Zm-1.61 17.198 2.046-2.046-3.94-3.94c-.165-.166-.345-.373-.442-.608-.221-.47-.318-1.203.221-1.742.664-.664 1.548-.387 2.406.47l3.788 3.788 2.046-2.046-3.954-3.954a2.48 2.48 0 0 1-.456-.622c-.263-.539-.25-1.216.235-1.7.677-.678 1.562-.429 2.544.553l3.677 3.677 2.046-2.046-3.982-3.982c-2.018-2.018-3.912-1.949-5.212-.65-.498.499-.802 1.024-.954 1.618a4.026 4.026 0 0 0-.055 1.686l-.027.028c-.996-.414-2.13-.166-3 .705-1.162 1.161-1.12 2.392-.982 3.11l-.042.043-1.009-.816-1.77 1.77a64.1 64.1 0 0 1 2.213 2.1z" }) }); var LogoRT = (props) => u("svg", { viewBox: "0 0 24 24", width: "24", height: "24", "aria-label": "Rotten Tomatoes", fill: "#FA320A", ...props, children: u("path", { d: "M5.866 0L4.335 1.262l2.082 1.8c-2.629-.989-4.842 1.4-5.012 2.338 1.384-.323 2.24-.422 3.344-.335-7.042 4.634-4.978 13.148-1.434 16.094 5.784 4.612 13.77 3.202 17.91-1.316C27.26 13.363 22.993.65 10.86 2.766c.107-1.17.633-1.503 1.243-1.602-.89-1.493-3.67-.734-4.556 1.374C7.52 2.602 5.866 0 5.866 0zM4.422 7.217H6.9c2.673 0 2.898.012 3.55.202 1.06.307 1.868.973 2.313 1.904.05.106.092.206.13.305l7.623.008.027 2.912-2.745-.024v7.549l-2.982-.016v-7.522l-2.127.016a2.92 2.92 0 0 1-1.056 1.134c-.287.176-.3.19-.254.264.127.2 2.125 3.642 2.125 3.659l-3.39.019-2.013-3.376c-.034-.047-.122-.068-.344-.084l-.297-.02.037 3.48-3.075-.038zm3.016 2.288l.024.338c.014.186.024.729.024 1.206v.867l.582-.025c.32-.013.695-.049.833-.078.694-.146 1.048-.478 1.087-1.018.027-.378-.063-.636-.303-.87-.318-.309-.761-.416-1.733-.418Z" }) }); var PlayIcon = (props) => u("svg", { viewBox: "0 0 24 24", width: "24", height: "24", fill: "none", stroke: "white", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", ...props, children: u("polygon", { points: "6 3 20 12 6 21 6 3", fill: "white", stroke: "none" }) }); var ModalCloseButton = ({ ariaLabel, className = "atv-modal-close", onClick, size = 22 }) => { return u("button", { "aria-label": ariaLabel, class: className.split(/\s+/u).includes("atv-modal-close") ? className : `atv-modal-close ${className}`, onClick, type: "button", children: u(IconClose, { size }) }); }; var ModalSessionContext = X(0); var ModalSession = ({ children, request }) => { const [session, setSession] = d({ request, sequence: 0 }); _(() => { setSession((current) => current.request === request ? current : { request, sequence: current.sequence + 1 }); }, [request]); return u(ModalSessionContext.Provider, { value: session.sequence, children }); }; var useModalSession = () => x(ModalSessionContext); var ModalSessionContent = ({ children }) => u(S, { children }, useModalSession()); function addUniqueItem(arr, item) { if (arr.indexOf(item) === -1) arr.push(item); } function removeItem(arr, item) { const index = arr.indexOf(item); if (index > -1) arr.splice(index, 1); } var clamp = (min, max, v) => { if (v > max) return max; if (v < min) return min; return v; }; var MotionGlobalConfig = {}; var isNumericalString = (v) => /^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(v); var isObject = (value) => typeof value === "object" && value !== null; var isZeroValueString = (v) => /^0[^.\s]+$/u.test(v); function memo(callback) { let result; return () => { if (result === void 0) result = callback(); return result; }; } var noop$4 = (any) => any; var pipe = (...transformers) => transformers.reduce((a, b) => (v) => b(a(v))); var progress = (from, to, value) => { const range = to - from; return range ? (value - from) / range : 1; }; var SubscriptionManager = class { constructor() { this.subscriptions = []; } add(handler) { addUniqueItem(this.subscriptions, handler); return () => removeItem(this.subscriptions, handler); } notify(a, b, c) { const numSubscriptions = this.subscriptions.length; if (!numSubscriptions) return; if (numSubscriptions === 1) this.subscriptions[0](a, b, c); else for (let i = 0; i < numSubscriptions; i++) { const handler = this.subscriptions[i]; handler && handler(a, b, c); } } getSize() { return this.subscriptions.length; } clear() { this.subscriptions.length = 0; } }; var secondsToMilliseconds = (seconds) => seconds * 1e3; var millisecondsToSeconds = (milliseconds) => milliseconds / 1e3; var velocityPerSecond = (velocity, frameDuration) => frameDuration ? velocity * (1e3 / frameDuration) : 0; var wrap = (min, max, v) => { const rangeSize = max - min; return ((v - min) % rangeSize + rangeSize) % rangeSize + min; }; var calcBezier = (t, a1, a2) => (((1 - 3 * a2 + 3 * a1) * t + (3 * a2 - 6 * a1)) * t + 3 * a1) * t; var subdivisionPrecision = 1e-7; var subdivisionMaxIterations = 12; function binarySubdivide(x, lowerBound, upperBound, mX1, mX2) { let currentX; let currentT; let i = 0; do { currentT = lowerBound + (upperBound - lowerBound) / 2; currentX = calcBezier(currentT, mX1, mX2) - x; if (currentX > 0) upperBound = currentT; else lowerBound = currentT; } while (Math.abs(currentX) > subdivisionPrecision && ++i < subdivisionMaxIterations); return currentT; } function cubicBezier(mX1, mY1, mX2, mY2) { if (mX1 === mY1 && mX2 === mY2) return noop$4; const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2); return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2); } var mirrorEasing = (easing) => (p) => p <= .5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2; var reverseEasing = (easing) => (p) => 1 - easing(1 - p); var backOut = cubicBezier(.33, 1.53, .69, .99); var backIn = reverseEasing(backOut); var backInOut = mirrorEasing(backIn); var anticipate = (p) => p >= 1 ? 1 : (p *= 2) < 1 ? .5 * backIn(p) : .5 * (2 - Math.pow(2, -10 * (p - 1))); var circIn = (p) => 1 - Math.sin(Math.acos(p)); var circOut = reverseEasing(circIn); var circInOut = mirrorEasing(circIn); var easeIn = cubicBezier(.42, 0, 1, 1); var easeOut = cubicBezier(0, 0, .58, 1); var easeInOut = cubicBezier(.42, 0, .58, 1); var isEasingArray = (ease) => { return Array.isArray(ease) && typeof ease[0] !== "number"; }; function getEasingForSegment(easing, i) { return isEasingArray(easing) ? easing[wrap(0, easing.length, i)] : easing; } var isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number"; var easingLookup = { linear: noop$4, easeIn, easeInOut, easeOut, circIn, circInOut, circOut, backIn, backInOut, backOut, anticipate }; var isValidEasing = (easing) => { return typeof easing === "string"; }; var easingDefinitionToFunction = (definition) => { if (isBezierDefinition(definition)) { definition.length; const [x1, y1, x2, y2] = definition; return cubicBezier(x1, y1, x2, y2); } else if (isValidEasing(definition)) { easingLookup[definition], `${definition}`; return easingLookup[definition]; } return definition; }; var stepsOrder = [ "setup", "read", "resolveKeyframes", "preUpdate", "update", "preRender", "render", "postRender" ]; function createRenderStep(runNextFrame) { let thisFrame = new Set(); let nextFrame = new Set(); let isProcessing = false; let flushNextFrame = false; const toKeepAlive = new WeakSet(); let latestFrameData = { delta: 0, timestamp: 0, isProcessing: false }; function triggerCallback(callback) { if (toKeepAlive.has(callback)) { step.schedule(callback); runNextFrame(); } callback(latestFrameData); } const step = { schedule: (callback, keepAlive = false, immediate = false) => { const queue = immediate && isProcessing ? thisFrame : nextFrame; if (keepAlive) toKeepAlive.add(callback); queue.add(callback); return callback; }, cancel: (callback) => { nextFrame.delete(callback); toKeepAlive.delete(callback); }, process: (frameData) => { latestFrameData = frameData; if (isProcessing) { flushNextFrame = true; return; } isProcessing = true; const prevFrame = thisFrame; thisFrame = nextFrame; nextFrame = prevFrame; thisFrame.forEach(triggerCallback); thisFrame.clear(); isProcessing = false; if (flushNextFrame) { flushNextFrame = false; step.process(frameData); } } }; return step; } var maxElapsed = 40; function createRenderBatcher(scheduleNextBatch, allowKeepAlive) { let runNextFrame = false; let useDefaultElapsed = true; const state = { delta: 0, timestamp: 0, isProcessing: false }; const flagRunNextFrame = () => runNextFrame = true; const steps = stepsOrder.reduce((acc, key) => { acc[key] = createRenderStep(flagRunNextFrame); return acc; }, {}); const { setup, read, resolveKeyframes, preUpdate, update, preRender, render, postRender } = steps; const processBatch = () => { const useManualTiming = MotionGlobalConfig.useManualTiming; const timestamp = useManualTiming ? state.timestamp : performance.now(); runNextFrame = false; if (!useManualTiming) state.delta = useDefaultElapsed ? 1e3 / 60 : Math.max(Math.min(timestamp - state.timestamp, maxElapsed), 1); state.timestamp = timestamp; state.isProcessing = true; setup.process(state); read.process(state); resolveKeyframes.process(state); preUpdate.process(state); update.process(state); preRender.process(state); render.process(state); postRender.process(state); state.isProcessing = false; if (runNextFrame && allowKeepAlive) { useDefaultElapsed = false; scheduleNextBatch(processBatch); } }; const wake = () => { runNextFrame = true; useDefaultElapsed = true; if (!state.isProcessing) scheduleNextBatch(processBatch); }; const schedule = stepsOrder.reduce((acc, key) => { const step = steps[key]; acc[key] = (process, keepAlive = false, immediate = false) => { if (!runNextFrame) wake(); return step.schedule(process, keepAlive, immediate); }; return acc; }, {}); const cancel = (process) => { for (let i = 0; i < stepsOrder.length; i++) steps[stepsOrder[i]].cancel(process); }; return { schedule, cancel, state, steps }; } var { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps } = createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop$4, true); var now; function clearTime() { now = void 0; } var time = { now: () => { if (now === void 0) time.set(frameData.isProcessing || MotionGlobalConfig.useManualTiming ? frameData.timestamp : performance.now()); return now; }, set: (newTime) => { now = newTime; queueMicrotask(clearTime); } }; var checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token); var isCSSVariableName = checkStringStartsWith("--"); var startsAsVariableToken = checkStringStartsWith("var(--"); var isCSSVariableToken = (value) => { if (!startsAsVariableToken(value)) return false; return singleCssVariableRegex.test(value.split("/*")[0].trim()); }; var singleCssVariableRegex = /var\(--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)$/iu; function containsCSSVariable(value) { if (typeof value !== "string") return false; return value.split("/*")[0].includes("var(--"); } var number = { test: (v) => typeof v === "number", parse: parseFloat, transform: (v) => v }; var alpha = { ...number, transform: (v) => clamp(0, 1, v) }; var scale = { ...number, default: 1 }; var sanitize = (v) => Math.round(v * 1e5) / 1e5; var floatRegex = /-?(?:\d+(?:\.\d+)?|\.\d+)/gu; function isNullish(v) { return v == null; } var singleColorRegex = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))$/iu; var isColorString = (type, testProp) => (v) => { return Boolean(typeof v === "string" && singleColorRegex.test(v) && v.startsWith(type) || testProp && !isNullish(v) && Object.prototype.hasOwnProperty.call(v, testProp)); }; var splitColor = (aName, bName, cName) => (v) => { if (typeof v !== "string") return v; const [a, b, c, alpha] = v.match(floatRegex); return { [aName]: parseFloat(a), [bName]: parseFloat(b), [cName]: parseFloat(c), alpha: alpha !== void 0 ? parseFloat(alpha) : 1 }; }; var clampRgbUnit = (v) => clamp(0, 255, v); var rgbUnit = { ...number, transform: (v) => Math.round(clampRgbUnit(v)) }; var rgba = { test: isColorString("rgb", "red"), parse: splitColor("red", "green", "blue"), transform: ({ red, green, blue, alpha: alpha$1 = 1 }) => "rgba(" + rgbUnit.transform(red) + ", " + rgbUnit.transform(green) + ", " + rgbUnit.transform(blue) + ", " + sanitize(alpha.transform(alpha$1)) + ")" }; function parseHex(v) { let r = ""; let g = ""; let b = ""; let a = ""; if (v.length > 5) { r = v.substring(1, 3); g = v.substring(3, 5); b = v.substring(5, 7); a = v.substring(7, 9); } else { r = v.substring(1, 2); g = v.substring(2, 3); b = v.substring(3, 4); a = v.substring(4, 5); r += r; g += g; b += b; a += a; } return { red: parseInt(r, 16), green: parseInt(g, 16), blue: parseInt(b, 16), alpha: a ? parseInt(a, 16) / 255 : 1 }; } var hex = { test: isColorString("#"), parse: parseHex, transform: rgba.transform }; var createUnitType = (unit) => ({ test: (v) => typeof v === "string" && v.endsWith(unit) && v.split(" ").length === 1, parse: parseFloat, transform: (v) => `${v}${unit}` }); var degrees = createUnitType("deg"); var percent = createUnitType("%"); var px = createUnitType("px"); var vh = createUnitType("vh"); var vw = createUnitType("vw"); var progressPercentage = (() => ({ ...percent, parse: (v) => percent.parse(v) / 100, transform: (v) => percent.transform(v * 100) }))(); var hsla = { test: isColorString("hsl", "hue"), parse: splitColor("hue", "saturation", "lightness"), transform: ({ hue, saturation, lightness, alpha: alpha$1 = 1 }) => { return "hsla(" + Math.round(hue) + ", " + percent.transform(sanitize(saturation)) + ", " + percent.transform(sanitize(lightness)) + ", " + sanitize(alpha.transform(alpha$1)) + ")"; } }; var color = { test: (v) => rgba.test(v) || hex.test(v) || hsla.test(v), parse: (v) => { if (rgba.test(v)) return rgba.parse(v); else if (hsla.test(v)) return hsla.parse(v); else return hex.parse(v); }, transform: (v) => { return typeof v === "string" ? v : v.hasOwnProperty("red") ? rgba.transform(v) : hsla.transform(v); }, getAnimatableNone: (v) => { const parsed = color.parse(v); parsed.alpha = 0; return color.transform(parsed); } }; var colorRegex = /(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\))/giu; function test(v) { return isNaN(v) && typeof v === "string" && (v.match(floatRegex)?.length || 0) + (v.match(colorRegex)?.length || 0) > 0; } var NUMBER_TOKEN = "number"; var COLOR_TOKEN = "color"; var VAR_TOKEN = "var"; var VAR_FUNCTION_TOKEN = "var("; var SPLIT_TOKEN = "${}"; var complexRegex = /var\s*\(\s*--(?:[\w-]+\s*|[\w-]+\s*,(?:\s*[^)(\s]|\s*\((?:[^)(]|\([^)(]*\))*\))+\s*)\)|#[\da-f]{3,8}|(?:rgb|hsl)a?\((?:-?[\d.]+%?[,\s]+){2}-?[\d.]+%?\s*(?:[,/]\s*)?(?:\b\d+(?:\.\d+)?|\.\d+)?%?\)|-?(?:\d+(?:\.\d+)?|\.\d+)/giu; function analyseComplexValue(value) { const originalValue = value.toString(); const values = []; const indexes = { color: [], number: [], var: [] }; const types = []; let i = 0; return { values, split: originalValue.replace(complexRegex, (parsedValue) => { if (color.test(parsedValue)) { indexes.color.push(i); types.push(COLOR_TOKEN); values.push(color.parse(parsedValue)); } else if (parsedValue.startsWith(VAR_FUNCTION_TOKEN)) { indexes.var.push(i); types.push(VAR_TOKEN); values.push(parsedValue); } else { indexes.number.push(i); types.push(NUMBER_TOKEN); values.push(parseFloat(parsedValue)); } ++i; return SPLIT_TOKEN; }).split(SPLIT_TOKEN), indexes, types }; } function parseComplexValue(v) { return analyseComplexValue(v).values; } function buildTransformer({ split, types }) { const numSections = split.length; return (v) => { let output = ""; for (let i = 0; i < numSections; i++) { output += split[i]; if (v[i] !== void 0) { const type = types[i]; if (type === NUMBER_TOKEN) output += sanitize(v[i]); else if (type === COLOR_TOKEN) output += color.transform(v[i]); else output += v[i]; } } return output; }; } function createTransformer(source) { return buildTransformer(analyseComplexValue(source)); } var convertNumbersToZero = (v) => typeof v === "number" ? 0 : color.test(v) ? color.getAnimatableNone(v) : v; var convertToZero = (value, splitBefore) => { if (typeof value === "number") return splitBefore?.trim().endsWith("/") ? value : 0; return convertNumbersToZero(value); }; function getAnimatableNone$1(v) { const info = analyseComplexValue(v); return buildTransformer(info)(info.values.map((value, i) => convertToZero(value, info.split[i]))); } var complex = { test, parse: parseComplexValue, createTransformer, getAnimatableNone: getAnimatableNone$1 }; function hueToRgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; } function hslaToRgba({ hue, saturation, lightness, alpha }) { hue /= 360; saturation /= 100; lightness /= 100; let red = 0; let green = 0; let blue = 0; if (!saturation) red = green = blue = lightness; else { const q = lightness < .5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation; const p = 2 * lightness - q; red = hueToRgb(p, q, hue + 1 / 3); green = hueToRgb(p, q, hue); blue = hueToRgb(p, q, hue - 1 / 3); } return { red: Math.round(red * 255), green: Math.round(green * 255), blue: Math.round(blue * 255), alpha }; } function mixImmediate(a, b) { return (p) => p > 0 ? b : a; } var mixNumber$1 = (from, to, progress) => { return from + (to - from) * progress; }; var mixLinearColor = (from, to, v) => { const fromExpo = from * from; const expo = v * (to * to - fromExpo) + fromExpo; return expo < 0 ? 0 : Math.sqrt(expo); }; var colorTypes = [ hex, rgba, hsla ]; var getColorType = (v) => colorTypes.find((type) => type.test(v)); function asRGBA(color) { const type = getColorType(color); `${color}`; if (!Boolean(type)) return false; let model = type.parse(color); if (type === hsla) model = hslaToRgba(model); return model; } var mixColor = (from, to) => { const fromRGBA = asRGBA(from); const toRGBA = asRGBA(to); if (!fromRGBA || !toRGBA) return mixImmediate(from, to); const blended = { ...fromRGBA }; return (v) => { blended.red = mixLinearColor(fromRGBA.red, toRGBA.red, v); blended.green = mixLinearColor(fromRGBA.green, toRGBA.green, v); blended.blue = mixLinearColor(fromRGBA.blue, toRGBA.blue, v); blended.alpha = mixNumber$1(fromRGBA.alpha, toRGBA.alpha, v); return rgba.transform(blended); }; }; var invisibleValues = new Set(["none", "hidden"]); function mixVisibility(origin, target) { if (invisibleValues.has(origin)) return (p) => p <= 0 ? origin : target; else return (p) => p >= 1 ? target : origin; } function mixNumber(a, b) { return (p) => mixNumber$1(a, b, p); } function getMixer(a) { if (typeof a === "number") return mixNumber; else if (typeof a === "string") return isCSSVariableToken(a) ? mixImmediate : color.test(a) ? mixColor : mixComplex; else if (Array.isArray(a)) return mixArray; else if (typeof a === "object") return color.test(a) ? mixColor : mixObject; return mixImmediate; } function mixArray(a, b) { const output = [...a]; const numValues = output.length; const blendValue = a.map((v, i) => getMixer(v)(v, b[i])); return (p) => { for (let i = 0; i < numValues; i++) output[i] = blendValue[i](p); return output; }; } function mixObject(a, b) { const output = { ...a, ...b }; const blendValue = {}; for (const key in output) if (a[key] !== void 0 && b[key] !== void 0) blendValue[key] = getMixer(a[key])(a[key], b[key]); return (v) => { for (const key in blendValue) output[key] = blendValue[key](v); return output; }; } function matchOrder(origin, target) { const orderedOrigin = []; const pointers = { color: 0, var: 0, number: 0 }; for (let i = 0; i < target.values.length; i++) { const type = target.types[i]; const originIndex = origin.indexes[type][pointers[type]]; orderedOrigin[i] = origin.values[originIndex] ?? 0; pointers[type]++; } return orderedOrigin; } var mixComplex = (origin, target) => { const template = complex.createTransformer(target); const originStats = analyseComplexValue(origin); const targetStats = analyseComplexValue(target); if (originStats.indexes.var.length === targetStats.indexes.var.length && originStats.indexes.color.length === targetStats.indexes.color.length && originStats.indexes.number.length >= targetStats.indexes.number.length) { if (invisibleValues.has(origin) && !targetStats.values.length || invisibleValues.has(target) && !originStats.values.length) return mixVisibility(origin, target); return pipe(mixArray(matchOrder(originStats, targetStats), targetStats.values), template); } else { `${origin}${target}`; return mixImmediate(origin, target); } }; function mix(from, to, p) { if (typeof from === "number" && typeof to === "number" && typeof p === "number") return mixNumber$1(from, to, p); return getMixer(from)(from, to); } var frameloopDriver = (update) => { const passTimestamp = ({ timestamp }) => update(timestamp); return { start: (keepAlive = true) => frame.update(passTimestamp, keepAlive), stop: () => cancelFrame(passTimestamp), now: () => frameData.isProcessing ? frameData.timestamp : time.now() }; }; var generateLinearEasing = (easing, duration, resolution = 10) => { let points = ""; const numPoints = Math.max(Math.round(duration / resolution), 2); for (let i = 0; i < numPoints; i++) points += Math.round(easing(i / (numPoints - 1)) * 1e4) / 1e4 + ", "; return `linear(${points.substring(0, points.length - 2)})`; }; var maxGeneratorDuration = 2e4; function calcGeneratorDuration(generator) { let duration = 0; const timeStep = 50; let state = generator.next(duration); while (!state.done && duration < 2e4) { duration += timeStep; state = generator.next(duration); } return duration >= 2e4 ? Infinity : duration; } function createGeneratorEasing(options, scale = 100, createGenerator) { const generator = createGenerator({ ...options, keyframes: [0, scale] }); const duration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration); return { type: "keyframes", ease: (progress) => { return generator.next(duration * progress).value / scale; }, duration: millisecondsToSeconds(duration) }; } var springDefaults = { stiffness: 100, damping: 10, mass: 1, velocity: 0, duration: 800, bounce: .3, visualDuration: .3, restSpeed: { granular: .01, default: 2 }, restDelta: { granular: .005, default: .5 }, minDuration: .01, maxDuration: 10, minDamping: .05, maxDamping: 1 }; function calcAngularFreq(undampedFreq, dampingRatio) { return undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio); } var rootIterations = 12; function approximateRoot(envelope, derivative, initialGuess) { let result = initialGuess; for (let i = 1; i < rootIterations; i++) result = result - envelope(result) / derivative(result); return result; } var safeMin = .001; function findSpring({ duration = springDefaults.duration, bounce = springDefaults.bounce, velocity = springDefaults.velocity, mass = springDefaults.mass }) { let envelope; let derivative; springDefaults.maxDuration; let dampingRatio = 1 - bounce; dampingRatio = clamp(springDefaults.minDamping, springDefaults.maxDamping, dampingRatio); duration = clamp(springDefaults.minDuration, springDefaults.maxDuration, millisecondsToSeconds(duration)); if (dampingRatio < 1) { envelope = (undampedFreq) => { const exponentialDecay = undampedFreq * dampingRatio; const delta = exponentialDecay * duration; const a = exponentialDecay - velocity; const b = calcAngularFreq(undampedFreq, dampingRatio); const c = Math.exp(-delta); return safeMin - a / b * c; }; derivative = (undampedFreq) => { const delta = undampedFreq * dampingRatio * duration; const d = delta * velocity + velocity; const e = Math.pow(dampingRatio, 2) * Math.pow(undampedFreq, 2) * duration; const f = Math.exp(-delta); const g = calcAngularFreq(Math.pow(undampedFreq, 2), dampingRatio); return (-envelope(undampedFreq) + safeMin > 0 ? -1 : 1) * ((d - e) * f) / g; }; } else { envelope = (undampedFreq) => { return -.001 + Math.exp(-undampedFreq * duration) * ((undampedFreq - velocity) * duration + 1); }; derivative = (undampedFreq) => { return Math.exp(-undampedFreq * duration) * ((velocity - undampedFreq) * (duration * duration)); }; } const initialGuess = 5 / duration; const undampedFreq = approximateRoot(envelope, derivative, initialGuess); duration = secondsToMilliseconds(duration); if (isNaN(undampedFreq)) return { stiffness: springDefaults.stiffness, damping: springDefaults.damping, duration }; else { const stiffness = Math.pow(undampedFreq, 2) * mass; return { stiffness, damping: dampingRatio * 2 * Math.sqrt(mass * stiffness), duration }; } } var durationKeys = ["duration", "bounce"]; var physicsKeys = [ "stiffness", "damping", "mass" ]; function isSpringType(options, keys) { return keys.some((key) => options[key] !== void 0); } function getSpringOptions(options) { let springOptions = { velocity: springDefaults.velocity, stiffness: springDefaults.stiffness, damping: springDefaults.damping, mass: springDefaults.mass, isResolvedFromDuration: false, ...options }; if (!isSpringType(options, physicsKeys) && isSpringType(options, durationKeys)) { springOptions.velocity = 0; if (options.visualDuration) { const visualDuration = options.visualDuration; const root = 2 * Math.PI / (visualDuration * 1.2); const stiffness = root * root; const damping = 2 * clamp(.05, 1, 1 - (options.bounce || 0)) * Math.sqrt(stiffness); springOptions = { ...springOptions, mass: springDefaults.mass, stiffness, damping }; } else { const derived = findSpring({ ...options, velocity: 0 }); springOptions = { ...springOptions, ...derived, mass: springDefaults.mass }; springOptions.isResolvedFromDuration = true; } } return springOptions; } function spring(optionsOrVisualDuration = springDefaults.visualDuration, bounce = springDefaults.bounce) { const options = typeof optionsOrVisualDuration !== "object" ? { visualDuration: optionsOrVisualDuration, keyframes: [0, 1], bounce } : optionsOrVisualDuration; let { restSpeed, restDelta } = options; const origin = options.keyframes[0]; const target = options.keyframes[options.keyframes.length - 1]; const state = { done: false, value: origin }; const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration } = getSpringOptions({ ...options, velocity: -millisecondsToSeconds(options.velocity || 0) }); const initialVelocity = velocity || 0; const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass)); const initialDelta = target - origin; const undampedAngularFreq = millisecondsToSeconds(Math.sqrt(stiffness / mass)); const isGranularScale = Math.abs(initialDelta) < 5; restSpeed || (restSpeed = isGranularScale ? springDefaults.restSpeed.granular : springDefaults.restSpeed.default); restDelta || (restDelta = isGranularScale ? springDefaults.restDelta.granular : springDefaults.restDelta.default); let resolveSpring; let resolveVelocity; let angularFreq; let A; let sinCoeff; let cosCoeff; if (dampingRatio < 1) { angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio); A = (initialVelocity + dampingRatio * undampedAngularFreq * initialDelta) / angularFreq; resolveSpring = (t) => { const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t); return target - envelope * (A * Math.sin(angularFreq * t) + initialDelta * Math.cos(angularFreq * t)); }; sinCoeff = dampingRatio * undampedAngularFreq * A + initialDelta * angularFreq; cosCoeff = dampingRatio * undampedAngularFreq * initialDelta - A * angularFreq; resolveVelocity = (t) => { return Math.exp(-dampingRatio * undampedAngularFreq * t) * (sinCoeff * Math.sin(angularFreq * t) + cosCoeff * Math.cos(angularFreq * t)); }; } else if (dampingRatio === 1) { resolveSpring = (t) => target - Math.exp(-undampedAngularFreq * t) * (initialDelta + (initialVelocity + undampedAngularFreq * initialDelta) * t); const C = initialVelocity + undampedAngularFreq * initialDelta; resolveVelocity = (t) => Math.exp(-undampedAngularFreq * t) * (undampedAngularFreq * C * t - initialVelocity); } else { const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1); resolveSpring = (t) => { const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t); const freqForT = Math.min(dampedAngularFreq * t, 300); return target - envelope * ((initialVelocity + dampingRatio * undampedAngularFreq * initialDelta) * Math.sinh(freqForT) + dampedAngularFreq * initialDelta * Math.cosh(freqForT)) / dampedAngularFreq; }; const P = (initialVelocity + dampingRatio * undampedAngularFreq * initialDelta) / dampedAngularFreq; const sinhCoeff = dampingRatio * undampedAngularFreq * P - initialDelta * dampedAngularFreq; const coshCoeff = dampingRatio * undampedAngularFreq * initialDelta - P * dampedAngularFreq; resolveVelocity = (t) => { const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t); const freqForT = Math.min(dampedAngularFreq * t, 300); return envelope * (sinhCoeff * Math.sinh(freqForT) + coshCoeff * Math.cosh(freqForT)); }; } const generator = { calculatedDuration: isResolvedFromDuration ? duration || null : null, velocity: (t) => secondsToMilliseconds(resolveVelocity(t)), next: (t) => { if (!isResolvedFromDuration && dampingRatio < 1) { const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t); const sin = Math.sin(angularFreq * t); const cos = Math.cos(angularFreq * t); const current = target - envelope * (A * sin + initialDelta * cos); const currentVelocity = secondsToMilliseconds(envelope * (sinCoeff * sin + cosCoeff * cos)); state.done = Math.abs(currentVelocity) <= restSpeed && Math.abs(target - current) <= restDelta; state.value = state.done ? target : current; return state; } const current = resolveSpring(t); if (!isResolvedFromDuration) { const currentVelocity = secondsToMilliseconds(resolveVelocity(t)); state.done = Math.abs(currentVelocity) <= restSpeed && Math.abs(target - current) <= restDelta; } else state.done = t >= duration; state.value = state.done ? target : current; return state; }, toString: () => { const calculatedDuration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration); const easing = generateLinearEasing((progress) => generator.next(calculatedDuration * progress).value, calculatedDuration, 30); return calculatedDuration + "ms " + easing; }, toTransition: () => {} }; return generator; } spring.applyToOptions = (options) => { const generatorOptions = createGeneratorEasing(options, 100, spring); options.ease = generatorOptions.ease; options.duration = secondsToMilliseconds(generatorOptions.duration); options.type = "keyframes"; return options; }; var velocitySampleDuration = 5; function getGeneratorVelocity(resolveValue, t, current) { const prevT = Math.max(t - velocitySampleDuration, 0); return velocityPerSecond(current - resolveValue(prevT), t - prevT); } function inertia({ keyframes, velocity = 0, power = .8, timeConstant = 325, bounceDamping = 10, bounceStiffness = 500, modifyTarget, min, max, restDelta = .5, restSpeed }) { const origin = keyframes[0]; const state = { done: false, value: origin }; const isOutOfBounds = (v) => min !== void 0 && v < min || max !== void 0 && v > max; const nearestBoundary = (v) => { if (min === void 0) return max; if (max === void 0) return min; return Math.abs(min - v) < Math.abs(max - v) ? min : max; }; let amplitude = power * velocity; const ideal = origin + amplitude; const target = modifyTarget === void 0 ? ideal : modifyTarget(ideal); if (target !== ideal) amplitude = target - origin; const calcDelta = (t) => -amplitude * Math.exp(-t / timeConstant); const calcLatest = (t) => target + calcDelta(t); const applyFriction = (t) => { const delta = calcDelta(t); const latest = calcLatest(t); state.done = Math.abs(delta) <= restDelta; state.value = state.done ? target : latest; }; let timeReachedBoundary; let spring$1; const checkCatchBoundary = (t) => { if (!isOutOfBounds(state.value)) return; timeReachedBoundary = t; spring$1 = spring({ keyframes: [state.value, nearestBoundary(state.value)], velocity: getGeneratorVelocity(calcLatest, t, state.value), damping: bounceDamping, stiffness: bounceStiffness, restDelta, restSpeed }); }; checkCatchBoundary(0); return { calculatedDuration: null, next: (t) => { let hasUpdatedFrame = false; if (!spring$1 && timeReachedBoundary === void 0) { hasUpdatedFrame = true; applyFriction(t); checkCatchBoundary(t); } if (timeReachedBoundary !== void 0 && t >= timeReachedBoundary) return spring$1.next(t - timeReachedBoundary); else { !hasUpdatedFrame && applyFriction(t); return state; } } }; } function createMixers(output, ease, customMixer) { const mixers = []; const mixerFactory = customMixer || MotionGlobalConfig.mix || mix; const numMixers = output.length - 1; for (let i = 0; i < numMixers; i++) { let mixer = mixerFactory(output[i], output[i + 1]); if (ease) mixer = pipe(Array.isArray(ease) ? ease[i] || noop$4 : ease, mixer); mixers.push(mixer); } return mixers; } function interpolate(input, output, { clamp: isClamp = true, ease, mixer } = {}) { const inputLength = input.length; output.length; if (inputLength === 1) return () => output[0]; if (inputLength === 2 && output[0] === output[1]) return () => output[1]; const isZeroDeltaRange = input[0] === input[1]; if (input[0] > input[inputLength - 1]) { input = [...input].reverse(); output = [...output].reverse(); } const mixers = createMixers(output, ease, mixer); const numMixers = mixers.length; const interpolator = (v) => { if (isZeroDeltaRange && v < input[0]) return output[0]; let i = 0; if (numMixers > 1) { for (; i < input.length - 2; i++) if (v < input[i + 1]) break; } const progressInRange = progress(input[i], input[i + 1], v); return mixers[i](progressInRange); }; return isClamp ? (v) => interpolator(clamp(input[0], input[inputLength - 1], v)) : interpolator; } function fillOffset(offset, remaining) { const min = offset[offset.length - 1]; for (let i = 1; i <= remaining; i++) { const offsetProgress = progress(0, remaining, i); offset.push(mixNumber$1(min, 1, offsetProgress)); } } function defaultOffset(arr) { const offset = [0]; fillOffset(offset, arr.length - 1); return offset; } function convertOffsetToTimes(offset, duration) { return offset.map((o) => o * duration); } function defaultEasing(values, easing) { return values.map(() => easing || easeInOut).splice(0, values.length - 1); } function keyframes({ duration = 300, keyframes: keyframeValues, times, ease = "easeInOut" }) { const easingFunctions = isEasingArray(ease) ? ease.map(easingDefinitionToFunction) : easingDefinitionToFunction(ease); const state = { done: false, value: keyframeValues[0] }; const mapTimeToKeyframe = interpolate(convertOffsetToTimes(times && times.length === keyframeValues.length ? times : defaultOffset(keyframeValues), duration), keyframeValues, { ease: Array.isArray(easingFunctions) ? easingFunctions : defaultEasing(keyframeValues, easingFunctions) }); return { calculatedDuration: duration, next: (t) => { state.value = mapTimeToKeyframe(t); state.done = t >= duration; return state; } }; } var isNotNull = (value) => value !== null; function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe, speed = 1) { const resolvedKeyframes = keyframes.filter(isNotNull); const index = speed < 0 || repeat && repeatType !== "loop" && repeat % 2 === 1 ? 0 : resolvedKeyframes.length - 1; return !index || finalKeyframe === void 0 ? resolvedKeyframes[index] : finalKeyframe; } var transitionTypeMap = { decay: inertia, inertia, tween: keyframes, keyframes, spring }; function replaceTransitionType(transition) { if (typeof transition.type === "string") transition.type = transitionTypeMap[transition.type]; } var WithPromise = class { constructor() { this.updateFinished(); } get finished() { return this._finished; } updateFinished() { this._finished = new Promise((resolve) => { this.resolve = resolve; }); } notifyFinished() { this.resolve(); } then(onResolve, onReject) { return this.finished.then(onResolve, onReject); } }; var percentToProgress = (percent) => percent / 100; var JSAnimation = class extends WithPromise { constructor(options) { super(); this.state = "idle"; this.startTime = null; this.isStopped = false; this.currentTime = 0; this.holdTime = null; this.playbackSpeed = 1; this.delayState = { done: false, value: void 0 }; this.stop = () => { const { motionValue } = this.options; if (motionValue && motionValue.updatedAt !== time.now()) this.tick(time.now()); this.isStopped = true; if (this.state === "idle") return; this.teardown(); this.options.onStop?.(); }; this.options = options; this.initAnimation(); this.play(); if (options.autoplay === false) this.pause(); } initAnimation() { const { options } = this; replaceTransitionType(options); const { type = keyframes, repeat = 0, repeatDelay = 0, repeatType, velocity = 0 } = options; let { keyframes: keyframes$1 } = options; const generatorFactory = type || keyframes; if (generatorFactory !== keyframes && typeof keyframes$1[0] !== "number") { this.mixKeyframes = pipe(percentToProgress, mix(keyframes$1[0], keyframes$1[1])); keyframes$1 = [0, 100]; } const generator = generatorFactory({ ...options, keyframes: keyframes$1 }); if (repeatType === "mirror") this.mirroredGenerator = generatorFactory({ ...options, keyframes: [...keyframes$1].reverse(), velocity: -velocity }); if (generator.calculatedDuration === null) generator.calculatedDuration = calcGeneratorDuration(generator); const { calculatedDuration } = generator; this.calculatedDuration = calculatedDuration; this.resolvedDuration = calculatedDuration + repeatDelay; this.totalDuration = this.resolvedDuration * (repeat + 1) - repeatDelay; this.generator = generator; } updateTime(timestamp) { const animationTime = Math.round(timestamp - this.startTime) * this.playbackSpeed; if (this.holdTime !== null) this.currentTime = this.holdTime; else this.currentTime = animationTime; } tick(timestamp, sample = false) { const { generator, totalDuration, mixKeyframes, mirroredGenerator, resolvedDuration, calculatedDuration } = this; if (this.startTime === null) return generator.next(0); const { delay = 0, keyframes, repeat, repeatType, repeatDelay, type, onUpdate, finalKeyframe } = this.options; if (this.speed > 0) this.startTime = Math.min(this.startTime, timestamp); else if (this.speed < 0) this.startTime = Math.min(timestamp - totalDuration / this.speed, this.startTime); if (sample) this.currentTime = timestamp; else this.updateTime(timestamp); const timeWithoutDelay = this.currentTime - delay * (this.playbackSpeed >= 0 ? 1 : -1); const isInDelayPhase = this.playbackSpeed >= 0 ? timeWithoutDelay < 0 : timeWithoutDelay > totalDuration; this.currentTime = Math.max(timeWithoutDelay, 0); if (this.state === "finished" && this.holdTime === null) this.currentTime = totalDuration; let elapsed = this.currentTime; let frameGenerator = generator; if (repeat) { const progress = Math.min(this.currentTime, totalDuration) / resolvedDuration; let currentIteration = Math.floor(progress); let iterationProgress = progress % 1; if (!iterationProgress && progress >= 1) iterationProgress = 1; iterationProgress === 1 && currentIteration--; currentIteration = Math.min(currentIteration, repeat + 1); if (Boolean(currentIteration % 2)) { if (repeatType === "reverse") { iterationProgress = 1 - iterationProgress; if (repeatDelay) iterationProgress -= repeatDelay / resolvedDuration; } else if (repeatType === "mirror") frameGenerator = mirroredGenerator; } elapsed = clamp(0, 1, iterationProgress) * resolvedDuration; } let state; if (isInDelayPhase) { this.delayState.value = keyframes[0]; state = this.delayState; } else state = frameGenerator.next(elapsed); if (mixKeyframes && !isInDelayPhase) state.value = mixKeyframes(state.value); let { done } = state; if (!isInDelayPhase && calculatedDuration !== null) done = this.playbackSpeed >= 0 ? this.currentTime >= totalDuration : this.currentTime <= 0; const isAnimationFinished = this.holdTime === null && (this.state === "finished" || this.state === "running" && done); if (isAnimationFinished && type !== inertia) state.value = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed); if (onUpdate) onUpdate(state.value); if (isAnimationFinished) this.finish(); return state; } then(resolve, reject) { return this.finished.then(resolve, reject); } get duration() { return millisecondsToSeconds(this.calculatedDuration); } get iterationDuration() { const { delay = 0 } = this.options || {}; return this.duration + millisecondsToSeconds(delay); } get time() { return millisecondsToSeconds(this.currentTime); } set time(newTime) { newTime = secondsToMilliseconds(newTime); this.currentTime = newTime; if (this.startTime === null || this.holdTime !== null || this.playbackSpeed === 0) this.holdTime = newTime; else if (this.driver) this.startTime = this.driver.now() - newTime / this.playbackSpeed; if (this.driver) this.driver.start(false); else { this.startTime = 0; this.state = "paused"; this.holdTime = newTime; this.tick(newTime); } } getGeneratorVelocity() { const t = this.currentTime; if (t <= 0) return this.options.velocity || 0; if (this.generator.velocity) return this.generator.velocity(t); const current = this.generator.next(t).value; return getGeneratorVelocity((s) => this.generator.next(s).value, t, current); } get speed() { return this.playbackSpeed; } set speed(newSpeed) { const hasChanged = this.playbackSpeed !== newSpeed; if (hasChanged && this.driver) this.updateTime(time.now()); this.playbackSpeed = newSpeed; if (hasChanged && this.driver) this.time = millisecondsToSeconds(this.currentTime); } play() { if (this.isStopped) return; const { driver = frameloopDriver, startTime } = this.options; if (!this.driver) this.driver = driver((timestamp) => this.tick(timestamp)); this.options.onPlay?.(); const now = this.driver.now(); if (this.state === "finished") { this.updateFinished(); this.startTime = now; } else if (this.holdTime !== null) this.startTime = now - this.holdTime; else if (!this.startTime) this.startTime = startTime ?? now; if (this.state === "finished" && this.speed < 0) this.startTime += this.calculatedDuration; this.holdTime = null; this.state = "running"; this.driver.start(); } pause() { this.state = "paused"; this.updateTime(time.now()); this.holdTime = this.currentTime; } complete() { if (this.state !== "running") this.play(); this.state = "finished"; this.holdTime = null; } finish() { this.notifyFinished(); this.teardown(); this.state = "finished"; this.options.onComplete?.(); } cancel() { this.holdTime = null; this.startTime = 0; this.tick(0); this.teardown(); this.options.onCancel?.(); } teardown() { this.state = "idle"; this.stopDriver(); this.startTime = this.holdTime = null; } stopDriver() { if (!this.driver) return; this.driver.stop(); this.driver = void 0; } sample(sampleTime) { this.startTime = 0; return this.tick(sampleTime, true); } attachTimeline(timeline) { if (this.options.allowFlatten) { this.options.type = "keyframes"; this.options.ease = "linear"; this.initAnimation(); } this.driver?.stop(); return timeline.observe(this); } }; function fillWildcards(keyframes) { for (let i = 1; i < keyframes.length; i++) keyframes[i] ?? (keyframes[i] = keyframes[i - 1]); } var radToDeg = (rad) => rad * 180 / Math.PI; var rotate = (v) => { return rebaseAngle(radToDeg(Math.atan2(v[1], v[0]))); }; var matrix2dParsers = { x: 4, y: 5, translateX: 4, translateY: 5, scaleX: 0, scaleY: 3, scale: (v) => (Math.abs(v[0]) + Math.abs(v[3])) / 2, rotate, rotateZ: rotate, skewX: (v) => radToDeg(Math.atan(v[1])), skewY: (v) => radToDeg(Math.atan(v[2])), skew: (v) => (Math.abs(v[1]) + Math.abs(v[2])) / 2 }; var rebaseAngle = (angle) => { angle = angle % 360; if (angle < 0) angle += 360; return angle; }; var rotateZ = rotate; var scaleX = (v) => Math.sqrt(v[0] * v[0] + v[1] * v[1]); var scaleY = (v) => Math.sqrt(v[4] * v[4] + v[5] * v[5]); var matrix3dParsers = { x: 12, y: 13, z: 14, translateX: 12, translateY: 13, translateZ: 14, scaleX, scaleY, scale: (v) => (scaleX(v) + scaleY(v)) / 2, rotateX: (v) => rebaseAngle(radToDeg(Math.atan2(v[6], v[5]))), rotateY: (v) => rebaseAngle(radToDeg(Math.atan2(-v[2], v[0]))), rotateZ, rotate: rotateZ, skewX: (v) => radToDeg(Math.atan(v[4])), skewY: (v) => radToDeg(Math.atan(v[1])), skew: (v) => (Math.abs(v[1]) + Math.abs(v[4])) / 2 }; function defaultTransformValue(name) { return name.includes("scale") ? 1 : 0; } function parseValueFromTransform(transform, name) { if (!transform || transform === "none") return defaultTransformValue(name); const matrix3dMatch = transform.match(/^matrix3d\(([-\d.e\s,]+)\)$/u); let parsers; let match; if (matrix3dMatch) { parsers = matrix3dParsers; match = matrix3dMatch; } else { const matrix2dMatch = transform.match(/^matrix\(([-\d.e\s,]+)\)$/u); parsers = matrix2dParsers; match = matrix2dMatch; } if (!match) return defaultTransformValue(name); const valueParser = parsers[name]; const values = match[1].split(",").map(convertTransformToNumber); return typeof valueParser === "function" ? valueParser(values) : values[valueParser]; } var readTransformValue = (instance, name) => { const { transform = "none" } = getComputedStyle(instance); return parseValueFromTransform(transform, name); }; function convertTransformToNumber(value) { return parseFloat(value.trim()); } var transformPropOrder = [ "transformPerspective", "x", "y", "z", "translateX", "translateY", "translateZ", "scale", "scaleX", "scaleY", "rotate", "rotateX", "rotateY", "rotateZ", "skew", "skewX", "skewY" ]; var transformProps = (() => new Set([...transformPropOrder, "pathRotation"]))(); var isNumOrPxType = (v) => v === number || v === px; var transformKeys = new Set([ "x", "y", "z" ]); var nonTranslationalTransformKeys = transformPropOrder.filter((key) => !transformKeys.has(key)); function removeNonTranslationalTransform(visualElement) { const removedTransforms = []; nonTranslationalTransformKeys.forEach((key) => { const value = visualElement.getValue(key); if (value !== void 0) { removedTransforms.push([key, value.get()]); value.set(key.startsWith("scale") ? 1 : 0); } }); return removedTransforms; } var positionalValues = { width: ({ x }, { paddingLeft = "0", paddingRight = "0", boxSizing }) => { const width = x.max - x.min; return boxSizing === "border-box" ? width : width - parseFloat(paddingLeft) - parseFloat(paddingRight); }, height: ({ y }, { paddingTop = "0", paddingBottom = "0", boxSizing }) => { const height = y.max - y.min; return boxSizing === "border-box" ? height : height - parseFloat(paddingTop) - parseFloat(paddingBottom); }, top: (_bbox, { top }) => parseFloat(top), left: (_bbox, { left }) => parseFloat(left), bottom: ({ y }, { top }) => parseFloat(top) + (y.max - y.min), right: ({ x }, { left }) => parseFloat(left) + (x.max - x.min), x: (_bbox, { transform }) => parseValueFromTransform(transform, "x"), y: (_bbox, { transform }) => parseValueFromTransform(transform, "y") }; positionalValues.translateX = positionalValues.x; positionalValues.translateY = positionalValues.y; var toResolve = new Set(); var isScheduled = false; var anyNeedsMeasurement = false; var isForced = false; function measureAllKeyframes() { if (anyNeedsMeasurement) { const resolversToMeasure = Array.from(toResolve).filter((resolver) => resolver.needsMeasurement); const elementsToMeasure = new Set(resolversToMeasure.map((resolver) => resolver.element)); const transformsToRestore = new Map(); elementsToMeasure.forEach((element) => { const removedTransforms = removeNonTranslationalTransform(element); if (!removedTransforms.length) return; transformsToRestore.set(element, removedTransforms); element.render(); }); resolversToMeasure.forEach((resolver) => resolver.measureInitialState()); elementsToMeasure.forEach((element) => { element.render(); const restore = transformsToRestore.get(element); if (restore) restore.forEach(([key, value]) => { element.getValue(key)?.set(value); }); }); resolversToMeasure.forEach((resolver) => resolver.measureEndState()); resolversToMeasure.forEach((resolver) => { if (resolver.suspendedScrollY !== void 0) window.scrollTo(0, resolver.suspendedScrollY); }); } anyNeedsMeasurement = false; isScheduled = false; toResolve.forEach((resolver) => resolver.complete(isForced)); toResolve.clear(); } function readAllKeyframes() { toResolve.forEach((resolver) => { resolver.readKeyframes(); if (resolver.needsMeasurement) anyNeedsMeasurement = true; }); } function flushKeyframeResolvers() { isForced = true; readAllKeyframes(); measureAllKeyframes(); isForced = false; } var KeyframeResolver = class { constructor(unresolvedKeyframes, onComplete, name, motionValue, element, isAsync = false) { this.state = "pending"; this.isAsync = false; this.needsMeasurement = false; this.unresolvedKeyframes = [...unresolvedKeyframes]; this.onComplete = onComplete; this.name = name; this.motionValue = motionValue; this.element = element; this.isAsync = isAsync; } scheduleResolve() { this.state = "scheduled"; if (this.isAsync) { toResolve.add(this); if (!isScheduled) { isScheduled = true; frame.read(readAllKeyframes); frame.resolveKeyframes(measureAllKeyframes); } } else { this.readKeyframes(); this.complete(); } } readKeyframes() { const { unresolvedKeyframes, name, element, motionValue } = this; if (unresolvedKeyframes[0] === null) { const currentValue = motionValue?.get(); const finalKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1]; if (currentValue !== void 0) unresolvedKeyframes[0] = currentValue; else if (element && name) { const valueAsRead = element.readValue(name, finalKeyframe); if (valueAsRead !== void 0 && valueAsRead !== null) unresolvedKeyframes[0] = valueAsRead; } if (unresolvedKeyframes[0] === void 0) unresolvedKeyframes[0] = finalKeyframe; if (motionValue && currentValue === void 0) motionValue.set(unresolvedKeyframes[0]); } fillWildcards(unresolvedKeyframes); } setFinalKeyframe() {} measureInitialState() {} renderEndStyles() {} measureEndState() {} complete(isForcedComplete = false) { this.state = "complete"; this.onComplete(this.unresolvedKeyframes, this.finalKeyframe, isForcedComplete); toResolve.delete(this); } cancel() { if (this.state === "scheduled") { toResolve.delete(this); this.state = "pending"; } } resume() { if (this.state === "pending") this.scheduleResolve(); } }; var isCSSVar = (name) => name.startsWith("--"); function setStyle(element, name, value) { isCSSVar(name) ? element.style.setProperty(name, value) : element.style[name] = value; } var supportsFlags = {}; function memoSupports(callback, supportsFlag) { const memoized = memo(callback); return () => supportsFlags[supportsFlag] ?? memoized(); } var supportsScrollTimeline = memoSupports(() => window.ScrollTimeline !== void 0, "scrollTimeline"); var supportsLinearEasing = memoSupports(() => { try { document.createElement("div").animate({ opacity: 0 }, { easing: "linear(0, 1)" }); } catch (e) { return false; } return true; }, "linearEasing"); var cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`; var supportedWaapiEasing = { linear: "linear", ease: "ease", easeIn: "ease-in", easeOut: "ease-out", easeInOut: "ease-in-out", circIn: cubicBezierAsString([ 0, .65, .55, 1 ]), circOut: cubicBezierAsString([ .55, 0, 1, .45 ]), backIn: cubicBezierAsString([ .31, .01, .66, -.59 ]), backOut: cubicBezierAsString([ .33, 1.53, .69, .99 ]) }; function mapEasingToNativeEasing(easing, duration) { if (!easing) return; else if (typeof easing === "function") return supportsLinearEasing() ? generateLinearEasing(easing, duration) : "ease-out"; else if (isBezierDefinition(easing)) return cubicBezierAsString(easing); else if (Array.isArray(easing)) return easing.map((segmentEasing) => mapEasingToNativeEasing(segmentEasing, duration) || supportedWaapiEasing.easeOut); else return supportedWaapiEasing[easing]; } function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease = "easeOut", times } = {}, pseudoElement = void 0) { const keyframeOptions = { [valueName]: keyframes }; if (times) keyframeOptions.offset = times; const easing = mapEasingToNativeEasing(ease, duration); if (Array.isArray(easing)) keyframeOptions.easing = easing; const options = { delay, duration, easing: !Array.isArray(easing) ? easing : "linear", fill: "both", iterations: repeat + 1, direction: repeatType === "reverse" ? "alternate" : "normal" }; if (pseudoElement) options.pseudoElement = pseudoElement; return element.animate(keyframeOptions, options); } function isGenerator(type) { return typeof type === "function" && "applyToOptions" in type; } function applyGeneratorOptions({ type, ...options }) { if (isGenerator(type) && supportsLinearEasing()) return type.applyToOptions(options); else { options.duration ?? (options.duration = 300); options.ease ?? (options.ease = "easeOut"); } return options; } var NativeAnimation = class extends WithPromise { constructor(options) { super(); this.finishedTime = null; this.isStopped = false; this.manualStartTime = null; if (!options) return; const { element, name, keyframes, pseudoElement, allowFlatten = false, finalKeyframe, onComplete } = options; this.isPseudoElement = Boolean(pseudoElement); this.allowFlatten = allowFlatten; this.options = options; options.type; const transition = applyGeneratorOptions(options); this.animation = startWaapiAnimation(element, name, keyframes, transition, pseudoElement); if (transition.autoplay === false) this.animation.pause(); this.animation.onfinish = () => { this.finishedTime = this.time; if (!pseudoElement) { const keyframe = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed); if (this.updateMotionValue) this.updateMotionValue(keyframe); setStyle(element, name, keyframe); this.animation.cancel(); } onComplete?.(); this.notifyFinished(); }; } play() { if (this.isStopped) return; this.manualStartTime = null; this.animation.play(); if (this.state === "finished") this.updateFinished(); } pause() { this.animation.pause(); } complete() { this.animation.finish?.(); } cancel() { try { this.animation.cancel(); } catch (e) {} } stop() { if (this.isStopped) return; this.isStopped = true; const { state } = this; if (state === "idle" || state === "finished") return; if (this.updateMotionValue) this.updateMotionValue(); else this.commitStyles(); if (!this.isPseudoElement) this.cancel(); } commitStyles() { const element = this.options?.element; if (!this.isPseudoElement && element?.isConnected) this.animation.commitStyles?.(); } get duration() { const duration = this.animation.effect?.getComputedTiming?.().duration || 0; return millisecondsToSeconds(Number(duration)); } get iterationDuration() { const { delay = 0 } = this.options || {}; return this.duration + millisecondsToSeconds(delay); } get time() { return millisecondsToSeconds(Number(this.animation.currentTime) || 0); } set time(newTime) { const wasFinished = this.finishedTime !== null; this.manualStartTime = null; this.finishedTime = null; this.animation.currentTime = secondsToMilliseconds(newTime); if (wasFinished) this.animation.pause(); } get speed() { return this.animation.playbackRate; } set speed(newSpeed) { if (newSpeed < 0) this.finishedTime = null; this.animation.playbackRate = newSpeed; } get state() { return this.finishedTime !== null ? "finished" : this.animation.playState; } get startTime() { return this.manualStartTime ?? Number(this.animation.startTime); } set startTime(newStartTime) { this.manualStartTime = this.animation.startTime = newStartTime; } attachTimeline({ timeline, rangeStart, rangeEnd, observe }) { if (this.allowFlatten) this.animation.effect?.updateTiming({ easing: "linear" }); this.animation.onfinish = null; if (timeline && supportsScrollTimeline()) { this.animation.timeline = timeline; if (rangeStart) this.animation.rangeStart = rangeStart; if (rangeEnd) this.animation.rangeEnd = rangeEnd; return noop$4; } else return observe(this); } }; var unsupportedEasingFunctions = { anticipate, backInOut, circInOut }; function isUnsupportedEase(key) { return key in unsupportedEasingFunctions; } function replaceStringEasing(transition) { if (typeof transition.ease === "string" && isUnsupportedEase(transition.ease)) transition.ease = unsupportedEasingFunctions[transition.ease]; } var sampleDelta = 10; var NativeAnimationExtended = class extends NativeAnimation { constructor(options) { replaceStringEasing(options); replaceTransitionType(options); super(options); if (options.startTime !== void 0 && options.autoplay !== false) this.startTime = options.startTime; this.options = options; } updateMotionValue(value) { const { motionValue, onUpdate, onComplete, element, ...options } = this.options; if (!motionValue) return; if (value !== void 0) { motionValue.set(value); return; } const sampleAnimation = new JSAnimation({ ...options, autoplay: false }); const sampleTime = Math.max(sampleDelta, time.now() - this.startTime); const delta = clamp(0, sampleDelta, sampleTime - sampleDelta); const current = sampleAnimation.sample(sampleTime).value; const { name } = this.options; if (element && name) setStyle(element, name, current); motionValue.setWithVelocity(sampleAnimation.sample(Math.max(0, sampleTime - delta)).value, current, delta); sampleAnimation.stop(); } }; var isAnimatable = (value, name) => { if (name === "zIndex") return false; if (typeof value === "number" || Array.isArray(value)) return true; if (typeof value === "string" && (complex.test(value) || value === "0") && !value.startsWith("url(")) return true; return false; }; function hasKeyframesChanged(keyframes) { const current = keyframes[0]; if (keyframes.length === 1) return true; for (let i = 0; i < keyframes.length; i++) if (keyframes[i] !== current) return true; } function canAnimate(keyframes, name, type, velocity) { const originKeyframe = keyframes[0]; if (originKeyframe === null) return false; if (name === "display" || name === "visibility") return true; const targetKeyframe = keyframes[keyframes.length - 1]; const isOriginAnimatable = isAnimatable(originKeyframe, name); const isTargetAnimatable = isAnimatable(targetKeyframe, name); `${name}${originKeyframe}${targetKeyframe}${isOriginAnimatable ? targetKeyframe : originKeyframe}`; if (!isOriginAnimatable || !isTargetAnimatable) return false; return hasKeyframesChanged(keyframes) || (type === "spring" || isGenerator(type)) && velocity; } function makeAnimationInstant(options) { options.duration = 0; options.type = "keyframes"; } var acceleratedValues = new Set([ "opacity", "clipPath", "filter", "transform" ]); var browserColorFunctions = /^(?:oklch|oklab|lab|lch|color|color-mix|light-dark)\(/; function hasBrowserOnlyColors(keyframes) { for (let i = 0; i < keyframes.length; i++) if (typeof keyframes[i] === "string" && browserColorFunctions.test(keyframes[i])) return true; return false; } var colorProperties = new Set([ "color", "backgroundColor", "outlineColor", "fill", "stroke", "borderColor", "borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor" ]); var supportsWaapi = memo(() => Object.hasOwnProperty.call(Element.prototype, "animate")); function supportsBrowserAnimation(options) { const { motionValue, name, repeatDelay, repeatType, damping, type, keyframes } = options; if (!(motionValue?.owner?.current instanceof HTMLElement)) return false; const { onUpdate, transformTemplate } = motionValue.owner.getProps(); return supportsWaapi() && name && (acceleratedValues.has(name) || colorProperties.has(name) && hasBrowserOnlyColors(keyframes)) && (name !== "transform" || !transformTemplate) && !onUpdate && !repeatDelay && repeatType !== "mirror" && damping !== 0 && type !== "inertia"; } var MAX_RESOLVE_DELAY = 40; var AsyncMotionValueAnimation = class extends WithPromise { constructor({ autoplay = true, delay = 0, type = "keyframes", repeat = 0, repeatDelay = 0, repeatType = "loop", keyframes, name, motionValue, element, ...options }) { super(); this.stop = () => { if (this._animation) { this._animation.stop(); this.stopTimeline?.(); } this.keyframeResolver?.cancel(); }; this.createdAt = time.now(); const optionsWithDefaults = { autoplay, delay, type, repeat, repeatDelay, repeatType, name, motionValue, element, ...options }; const KeyframeResolver$1 = element?.KeyframeResolver || KeyframeResolver; this.keyframeResolver = new KeyframeResolver$1(keyframes, (resolvedKeyframes, finalKeyframe, forced) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe, optionsWithDefaults, !forced), name, motionValue, element); this.keyframeResolver?.scheduleResolve(); } onKeyframesResolved(keyframes, finalKeyframe, options, sync) { this.keyframeResolver = void 0; const { name, type, velocity, delay, isHandoff, onUpdate } = options; this.resolvedAt = time.now(); let canAnimateValue = true; if (!canAnimate(keyframes, name, type, velocity)) { canAnimateValue = false; if (MotionGlobalConfig.instantAnimations || !delay) onUpdate?.(getFinalKeyframe(keyframes, options, finalKeyframe)); keyframes[0] = keyframes[keyframes.length - 1]; makeAnimationInstant(options); options.repeat = 0; } const resolvedOptions = { startTime: sync ? !this.resolvedAt ? this.createdAt : this.resolvedAt - this.createdAt > MAX_RESOLVE_DELAY ? this.resolvedAt : this.createdAt : void 0, finalKeyframe, ...options, keyframes }; const useWaapi = canAnimateValue && !isHandoff && supportsBrowserAnimation(resolvedOptions); const element = resolvedOptions.motionValue?.owner?.current; let animation; if (useWaapi) try { animation = new NativeAnimationExtended({ ...resolvedOptions, element }); } catch { animation = new JSAnimation(resolvedOptions); } else animation = new JSAnimation(resolvedOptions); animation.finished.then(() => { this.notifyFinished(); }).catch(noop$4); if (this.pendingTimeline) { this.stopTimeline = animation.attachTimeline(this.pendingTimeline); this.pendingTimeline = void 0; } this._animation = animation; } get finished() { if (!this._animation) return this._finished; else return this.animation.finished; } then(onResolve, _onReject) { return this.finished.finally(onResolve).then(() => {}); } get animation() { if (!this._animation) { this.keyframeResolver?.resume(); flushKeyframeResolvers(); } return this._animation; } get duration() { return this.animation.duration; } get iterationDuration() { return this.animation.iterationDuration; } get time() { return this.animation.time; } set time(newTime) { this.animation.time = newTime; } get speed() { return this.animation.speed; } get state() { return this.animation.state; } set speed(newSpeed) { this.animation.speed = newSpeed; } get startTime() { return this.animation.startTime; } attachTimeline(timeline) { if (this._animation) this.stopTimeline = this.animation.attachTimeline(timeline); else this.pendingTimeline = timeline; return () => this.stop(); } play() { this.animation.play(); } pause() { this.animation.pause(); } complete() { this.animation.complete(); } cancel() { if (this._animation) this.animation.cancel(); this.keyframeResolver?.cancel(); } }; var GroupAnimation = class { constructor(animations) { this.stop = () => this.runAll("stop"); this.animations = animations.filter(Boolean); } get finished() { return Promise.all(this.animations.map((animation) => animation.finished)); } getAll(propName) { return this.animations[0][propName]; } setAll(propName, newValue) { for (let i = 0; i < this.animations.length; i++) this.animations[i][propName] = newValue; } attachTimeline(timeline) { const subscriptions = this.animations.map((animation) => animation.attachTimeline(timeline)); return () => { subscriptions.forEach((cancel, i) => { cancel && cancel(); this.animations[i].stop(); }); }; } get time() { return this.getAll("time"); } set time(time) { this.setAll("time", time); } get speed() { return this.getAll("speed"); } set speed(speed) { this.setAll("speed", speed); } get state() { return this.getAll("state"); } get startTime() { return this.getAll("startTime"); } get duration() { return getMax(this.animations, "duration"); } get iterationDuration() { return getMax(this.animations, "iterationDuration"); } runAll(methodName) { this.animations.forEach((controls) => controls[methodName]()); } play() { this.runAll("play"); } pause() { this.runAll("pause"); } cancel() { this.runAll("cancel"); } complete() { this.runAll("complete"); } }; function getMax(animations, propName) { let max = 0; for (let i = 0; i < animations.length; i++) { const value = animations[i][propName]; if (value !== null && value > max) max = value; } return max; } var GroupAnimationWithThen = class extends GroupAnimation { then(onResolve, _onReject) { return this.finished.finally(onResolve).then(() => {}); } }; var MAX_VELOCITY_DELTA = 30; var isFloat = (value) => { return !isNaN(parseFloat(value)); }; var collectMotionValues = { current: void 0 }; var MotionValue = class { constructor(init, options = {}) { this.canTrackVelocity = null; this.events = {}; this.updateAndNotify = (v) => { const currentTime = time.now(); if (this.updatedAt !== currentTime) this.setPrevFrameValue(); this.prev = this.current; this.setCurrent(v); if (this.current !== this.prev) { this.events.change?.notify(this.current); if (this.dependents) for (const dependent of this.dependents) dependent.dirty(); } }; this.hasAnimated = false; this.setCurrent(init); this.owner = options.owner; } setCurrent(current) { this.current = current; this.updatedAt = time.now(); if (this.canTrackVelocity === null && current !== void 0) this.canTrackVelocity = isFloat(this.current); } setPrevFrameValue(prevFrameValue = this.current) { this.prevFrameValue = prevFrameValue; this.prevUpdatedAt = this.updatedAt; } onChange(subscription) { return this.on("change", subscription); } on(eventName, callback) { if (!this.events[eventName]) this.events[eventName] = new SubscriptionManager(); const unsubscribe = this.events[eventName].add(callback); if (eventName === "change") return () => { unsubscribe(); frame.read(() => { if (!this.events.change.getSize()) this.stop(); }); }; return unsubscribe; } clearListeners() { for (const eventManagers in this.events) this.events[eventManagers].clear(); } attach(passiveEffect, stopPassiveEffect) { this.passiveEffect = passiveEffect; this.stopPassiveEffect = stopPassiveEffect; } set(v) { if (!this.passiveEffect) this.updateAndNotify(v); else this.passiveEffect(v, this.updateAndNotify); } setWithVelocity(prev, current, delta) { this.set(current); this.prev = void 0; this.prevFrameValue = prev; this.prevUpdatedAt = this.updatedAt - delta; } jump(v, endAnimation = true) { this.updateAndNotify(v); this.prev = v; this.prevUpdatedAt = this.prevFrameValue = void 0; endAnimation && this.stop(); if (this.stopPassiveEffect) this.stopPassiveEffect(); } dirty() { this.events.change?.notify(this.current); } addDependent(dependent) { if (!this.dependents) this.dependents = new Set(); this.dependents.add(dependent); } removeDependent(dependent) { if (this.dependents) this.dependents.delete(dependent); } get() { if (collectMotionValues.current) collectMotionValues.current.push(this); return this.current; } getPrevious() { return this.prev; } getVelocity() { const currentTime = time.now(); if (!this.canTrackVelocity || this.prevFrameValue === void 0 || currentTime - this.updatedAt > MAX_VELOCITY_DELTA) return 0; const delta = Math.min(this.updatedAt - this.prevUpdatedAt, MAX_VELOCITY_DELTA); return velocityPerSecond(parseFloat(this.current) - parseFloat(this.prevFrameValue), delta); } start(startAnimation) { this.stop(); return new Promise((resolve) => { this.hasAnimated = true; this.animation = startAnimation(resolve); if (this.events.animationStart) this.events.animationStart.notify(); }).then(() => { if (this.events.animationComplete) this.events.animationComplete.notify(); this.clearAnimation(); }); } stop() { if (this.animation) { this.animation.stop(); if (this.events.animationCancel) this.events.animationCancel.notify(); } this.clearAnimation(); } isAnimating() { return !!this.animation; } clearAnimation() { delete this.animation; } destroy() { this.dependents?.clear(); this.events.destroy?.notify(); this.clearListeners(); this.stop(); if (this.stopPassiveEffect) this.stopPassiveEffect(); } }; function motionValue(init, options) { return new MotionValue(init, options); } function resolveTransition(transition, parentTransition) { if (transition?.inherit && parentTransition) { const { inherit: _, ...rest } = transition; return { ...parentTransition, ...rest }; } return transition; } function getValueTransition$1(transition, key) { const valueTransition = transition?.[key] ?? transition?.["default"] ?? transition; if (valueTransition !== transition) return resolveTransition(valueTransition, transition); return valueTransition; } var underDampedSpring = { type: "spring", stiffness: 500, damping: 25, restSpeed: 10 }; var criticallyDampedSpring = (target) => ({ type: "spring", stiffness: 550, damping: target === 0 ? 2 * Math.sqrt(550) : 30, restSpeed: 10 }); var keyframesTransition = { type: "keyframes", duration: .8 }; var ease = { type: "keyframes", ease: [ .25, .1, .35, 1 ], duration: .3 }; var getDefaultTransition = (valueKey, { keyframes }) => { if (keyframes.length > 2) return keyframesTransition; else if (transformProps.has(valueKey)) return valueKey.startsWith("scale") ? criticallyDampedSpring(keyframes[1]) : underDampedSpring; return ease; }; var orchestrationKeys = new Set([ "when", "delay", "delayChildren", "staggerChildren", "staggerDirection", "repeat", "repeatType", "repeatDelay", "from", "elapsed" ]); function isTransitionDefined(transition) { for (const key in transition) if (!orchestrationKeys.has(key)) return true; return false; } var animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => { const valueTransition = getValueTransition$1(transition, name) || {}; const delay = valueTransition.delay || transition.delay || 0; let { elapsed = 0 } = transition; elapsed = elapsed - secondsToMilliseconds(delay); const options = { keyframes: Array.isArray(target) ? target : [null, target], ease: "easeOut", velocity: value.getVelocity(), ...valueTransition, delay: -elapsed, onUpdate: (v) => { value.set(v); valueTransition.onUpdate && valueTransition.onUpdate(v); }, onComplete: () => { onComplete(); valueTransition.onComplete && valueTransition.onComplete(); }, name, motionValue: value, element: isHandoff ? void 0 : element }; if (!isTransitionDefined(valueTransition)) Object.assign(options, getDefaultTransition(name, options)); options.duration && (options.duration = secondsToMilliseconds(options.duration)); options.repeatDelay && (options.repeatDelay = secondsToMilliseconds(options.repeatDelay)); if (options.from !== void 0) options.keyframes[0] = options.from; let shouldSkip = false; if (options.type === false || options.duration === 0 && !options.repeatDelay) { makeAnimationInstant(options); if (options.delay === 0) shouldSkip = true; } if (MotionGlobalConfig.instantAnimations || MotionGlobalConfig.skipAnimations || element?.shouldSkipAnimations || valueTransition.skipAnimations) { shouldSkip = true; makeAnimationInstant(options); options.delay = 0; } options.allowFlatten = !valueTransition.type && !valueTransition.ease; if (shouldSkip && !isHandoff && value.get() !== void 0) { const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition); if (finalKeyframe !== void 0) { frame.update(() => { options.onUpdate(finalKeyframe); options.onComplete(); }); return; } } return valueTransition.isSync ? new JSAnimation(options) : new AsyncMotionValueAnimation(options); }; var splitCSSVariableRegex = /^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u; function parseCSSVariable(current) { const match = splitCSSVariableRegex.exec(current); if (!match) return [,]; const [, token1, token2, fallback] = match; return [`--${token1 ?? token2}`, fallback]; } function getVariableValue(current, element, depth = 1) { `${current}`; const [token, fallback] = parseCSSVariable(current); if (!token) return; const resolved = window.getComputedStyle(element).getPropertyValue(token); if (resolved) { const trimmed = resolved.trim(); return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed; } return isCSSVariableToken(fallback) ? getVariableValue(fallback, element, depth + 1) : fallback; } function getValueState(visualElement) { const state = [{}, {}]; visualElement?.values.forEach((value, key) => { state[0][key] = value.get(); state[1][key] = value.getVelocity(); }); return state; } function resolveVariantFromProps(props, definition, custom, visualElement) { if (typeof definition === "function") { const [current, velocity] = getValueState(visualElement); definition = definition(custom !== void 0 ? custom : props.custom, current, velocity); } if (typeof definition === "string") definition = props.variants && props.variants[definition]; if (typeof definition === "function") { const [current, velocity] = getValueState(visualElement); definition = definition(custom !== void 0 ? custom : props.custom, current, velocity); } return definition; } function resolveVariant(visualElement, definition, custom) { const props = visualElement.getProps(); return resolveVariantFromProps(props, definition, custom !== void 0 ? custom : props.custom, visualElement); } var positionalKeys = new Set([ "width", "height", "top", "left", "right", "bottom", ...transformPropOrder ]); var isKeyframesTarget = (v) => { return Array.isArray(v); }; function setMotionValue(visualElement, key, value) { if (visualElement.hasValue(key)) visualElement.getValue(key).set(value); else visualElement.addValue(key, motionValue(value)); } function resolveFinalValueInKeyframes(v) { return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v; } function setTarget(visualElement, definition) { let { transitionEnd = {}, transition = {}, ...target } = resolveVariant(visualElement, definition) || {}; target = { ...target, ...transitionEnd }; for (const key in target) setMotionValue(visualElement, key, resolveFinalValueInKeyframes(target[key])); } var isMotionValue = (value) => Boolean(value && value.getVelocity); function isWillChangeMotionValue(value) { return Boolean(isMotionValue(value) && value.add); } function addValueToWillChange(visualElement, key) { const willChange = visualElement.getValue("willChange"); if (isWillChangeMotionValue(willChange)) return willChange.add(key); else if (!willChange && MotionGlobalConfig.WillChange) { const newWillChange = new MotionGlobalConfig.WillChange("auto"); visualElement.addValue("willChange", newWillChange); newWillChange.add(key); } } function camelToDash(str) { return str.replace(/([A-Z])/g, (match) => `-${match.toLowerCase()}`); } var optimizedAppearDataAttribute = "data-" + camelToDash("framerAppearId"); function getOptimisedAppearId(visualElement) { return visualElement.props[optimizedAppearDataAttribute]; } function shouldBlockAnimation({ protectedKeys, needsAnimating }, key) { const shouldBlock = protectedKeys.hasOwnProperty(key) && needsAnimating[key] !== true; needsAnimating[key] = false; return shouldBlock; } function animateTarget(visualElement, targetAndTransition, { delay = 0, transitionOverride, type } = {}) { let { transition, transitionEnd, ...target } = targetAndTransition; const defaultTransition = visualElement.getDefaultTransition(); transition = transition ? resolveTransition(transition, defaultTransition) : defaultTransition; const reduceMotion = transition?.reduceMotion; const skipAnimations = transition?.skipAnimations; if (transitionOverride) transition = transitionOverride; const animations = []; const animationTypeState = type && visualElement.animationState && visualElement.animationState.getState()[type]; const path = transition?.path; if (path) path.animateVisualElement(visualElement, target, transition, delay, animations); for (const key in target) { const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null); const valueTarget = target[key]; if (valueTarget === void 0 || animationTypeState && shouldBlockAnimation(animationTypeState, key)) continue; const valueTransition = { delay, ...getValueTransition$1(transition || {}, key) }; if (skipAnimations) valueTransition.skipAnimations = true; const currentValue = value.get(); if (currentValue !== void 0 && !value.isAnimating() && !Array.isArray(valueTarget) && valueTarget === currentValue && !valueTransition.velocity) { frame.update(() => value.set(valueTarget)); continue; } let isHandoff = false; if (window.MotionHandoffAnimation) { const appearId = getOptimisedAppearId(visualElement); if (appearId) { const startTime = window.MotionHandoffAnimation(appearId, key, frame); if (startTime !== null) { valueTransition.startTime = startTime; isHandoff = true; } } } addValueToWillChange(visualElement, key); const shouldReduceMotion = reduceMotion ?? visualElement.shouldReduceMotion; value.start(animateMotionValue(key, value, valueTarget, shouldReduceMotion && positionalKeys.has(key) ? { type: false } : valueTransition, visualElement, isHandoff)); const animation = value.animation; if (animation) animations.push(animation); } if (transitionEnd) { const applyTransitionEnd = () => frame.update(() => { transitionEnd && setTarget(visualElement, transitionEnd); }); if (animations.length) Promise.all(animations).then(applyTransitionEnd); else applyTransitionEnd(); } return animations; } var auto = { test: (v) => v === "auto", parse: (v) => v }; var testValueType = (v) => (type) => type.test(v); var dimensionValueTypes = [ number, px, percent, degrees, vw, vh, auto ]; var findDimensionValueType = (v) => dimensionValueTypes.find(testValueType(v)); function isNone(value) { if (typeof value === "number") return value === 0; else if (value !== null) return value === "none" || value === "0" || isZeroValueString(value); else return true; } var maxDefaults = new Set([ "brightness", "contrast", "saturate", "opacity" ]); function applyDefaultFilter(v) { const [name, value] = v.slice(0, -1).split("("); if (name === "drop-shadow") return v; const [number] = value.match(floatRegex) || []; if (!number) return v; const unit = value.replace(number, ""); let defaultValue = maxDefaults.has(name) ? 1 : 0; if (number !== value) defaultValue *= 100; return name + "(" + defaultValue + unit + ")"; } var functionRegex = /\b([a-z-]*)\(.*?\)/gu; var filter = { ...complex, getAnimatableNone: (v) => { const functions = v.match(functionRegex); return functions ? functions.map(applyDefaultFilter).join(" ") : v; } }; var mask = { ...complex, getAnimatableNone: (v) => { const parsed = complex.parse(v); return complex.createTransformer(v)(parsed.map((v) => typeof v === "number" ? 0 : typeof v === "object" ? { ...v, alpha: 1 } : v)); } }; var int = { ...number, transform: Math.round }; var numberValueTypes = { borderWidth: px, borderTopWidth: px, borderRightWidth: px, borderBottomWidth: px, borderLeftWidth: px, borderRadius: px, borderTopLeftRadius: px, borderTopRightRadius: px, borderBottomRightRadius: px, borderBottomLeftRadius: px, width: px, maxWidth: px, height: px, maxHeight: px, top: px, right: px, bottom: px, left: px, inset: px, insetBlock: px, insetBlockStart: px, insetBlockEnd: px, insetInline: px, insetInlineStart: px, insetInlineEnd: px, padding: px, paddingTop: px, paddingRight: px, paddingBottom: px, paddingLeft: px, paddingBlock: px, paddingBlockStart: px, paddingBlockEnd: px, paddingInline: px, paddingInlineStart: px, paddingInlineEnd: px, margin: px, marginTop: px, marginRight: px, marginBottom: px, marginLeft: px, marginBlock: px, marginBlockStart: px, marginBlockEnd: px, marginInline: px, marginInlineStart: px, marginInlineEnd: px, fontSize: px, backgroundPositionX: px, backgroundPositionY: px, rotate: degrees, pathRotation: degrees, rotateX: degrees, rotateY: degrees, rotateZ: degrees, scale, scaleX: scale, scaleY: scale, scaleZ: scale, skew: degrees, skewX: degrees, skewY: degrees, distance: px, translateX: px, translateY: px, translateZ: px, x: px, y: px, z: px, perspective: px, transformPerspective: px, opacity: alpha, originX: progressPercentage, originY: progressPercentage, originZ: px, zIndex: int, fillOpacity: alpha, strokeOpacity: alpha, numOctaves: int }; var defaultValueTypes = { ...numberValueTypes, color, backgroundColor: color, outlineColor: color, fill: color, stroke: color, borderColor: color, borderTopColor: color, borderRightColor: color, borderBottomColor: color, borderLeftColor: color, filter, WebkitFilter: filter, mask, WebkitMask: mask }; var getDefaultValueType = (key) => defaultValueTypes[key]; var customTypes = new Set([filter, mask]); function getAnimatableNone(key, value) { let defaultValueType = getDefaultValueType(key); if (!customTypes.has(defaultValueType)) defaultValueType = complex; return defaultValueType.getAnimatableNone ? defaultValueType.getAnimatableNone(value) : void 0; } var invalidTemplates = new Set([ "auto", "none", "0" ]); function makeNoneKeyframesAnimatable(unresolvedKeyframes, noneKeyframeIndexes, name) { let i = 0; let animatableTemplate = void 0; while (i < unresolvedKeyframes.length && !animatableTemplate) { const keyframe = unresolvedKeyframes[i]; if (typeof keyframe === "string" && !invalidTemplates.has(keyframe) && analyseComplexValue(keyframe).values.length) animatableTemplate = unresolvedKeyframes[i]; i++; } if (animatableTemplate && name) for (const noneIndex of noneKeyframeIndexes) unresolvedKeyframes[noneIndex] = getAnimatableNone(name, animatableTemplate); } var DOMKeyframesResolver = class extends KeyframeResolver { constructor(unresolvedKeyframes, onComplete, name, motionValue, element) { super(unresolvedKeyframes, onComplete, name, motionValue, element, true); } readKeyframes() { const { unresolvedKeyframes, element, name } = this; if (!element || !element.current) return; super.readKeyframes(); for (let i = 0; i < unresolvedKeyframes.length; i++) { let keyframe = unresolvedKeyframes[i]; if (typeof keyframe === "string") { keyframe = keyframe.trim(); if (isCSSVariableToken(keyframe)) { const resolved = getVariableValue(keyframe, element.current); if (resolved !== void 0) unresolvedKeyframes[i] = resolved; if (i === unresolvedKeyframes.length - 1) this.finalKeyframe = keyframe; } } } this.resolveNoneKeyframes(); if (!positionalKeys.has(name) || unresolvedKeyframes.length !== 2) return; const [origin, target] = unresolvedKeyframes; const originType = findDimensionValueType(origin); const targetType = findDimensionValueType(target); if (containsCSSVariable(origin) !== containsCSSVariable(target) && positionalValues[name]) { this.needsMeasurement = true; return; } if (originType === targetType) return; if (isNumOrPxType(originType) && isNumOrPxType(targetType)) for (let i = 0; i < unresolvedKeyframes.length; i++) { const value = unresolvedKeyframes[i]; if (typeof value === "string") unresolvedKeyframes[i] = parseFloat(value); } else if (positionalValues[name]) this.needsMeasurement = true; } resolveNoneKeyframes() { const { unresolvedKeyframes, name } = this; const noneKeyframeIndexes = []; for (let i = 0; i < unresolvedKeyframes.length; i++) if (unresolvedKeyframes[i] === null || isNone(unresolvedKeyframes[i])) noneKeyframeIndexes.push(i); if (noneKeyframeIndexes.length) makeNoneKeyframesAnimatable(unresolvedKeyframes, noneKeyframeIndexes, name); } measureInitialState() { const { element, unresolvedKeyframes, name } = this; if (!element || !element.current) return; if (name === "height") this.suspendedScrollY = window.pageYOffset; this.measuredOrigin = positionalValues[name](element.measureViewportBox(), window.getComputedStyle(element.current)); unresolvedKeyframes[0] = this.measuredOrigin; const measureKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1]; if (measureKeyframe !== void 0) element.getValue(name, measureKeyframe).jump(measureKeyframe, false); } measureEndState() { const { element, name, unresolvedKeyframes } = this; if (!element || !element.current) return; const value = element.getValue(name); value && value.jump(this.measuredOrigin, false); const finalKeyframeIndex = unresolvedKeyframes.length - 1; const finalKeyframe = unresolvedKeyframes[finalKeyframeIndex]; unresolvedKeyframes[finalKeyframeIndex] = positionalValues[name](element.measureViewportBox(), window.getComputedStyle(element.current)); if (finalKeyframe !== null && this.finalKeyframe === void 0) this.finalKeyframe = finalKeyframe; if (this.removedTransforms?.length) this.removedTransforms.forEach(([unsetTransformName, unsetTransformValue]) => { element.getValue(unsetTransformName).set(unsetTransformValue); }); this.resolveNoneKeyframes(); } }; var cornerRadiusProps = [ "borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius" ]; function resolveElements(elementOrSelector, scope, selectorCache) { if (elementOrSelector == null) return []; if (elementOrSelector instanceof EventTarget) return [elementOrSelector]; else if (typeof elementOrSelector === "string") { let root = document; if (scope) root = scope.current; const elements = selectorCache?.[elementOrSelector] ?? root.querySelectorAll(elementOrSelector); return elements ? Array.from(elements) : []; } return Array.from(elementOrSelector).filter((element) => element != null); } var getValueAsType = (value, type) => { return type && typeof value === "number" ? type.transform(value) : value; }; var { schedule: microtask, cancel: cancelMicrotask } = createRenderBatcher(queueMicrotask, false); function isSVGElement(element) { return isObject(element) && "ownerSVGElement" in element; } function isSVGSVGElement(element) { return isSVGElement(element) && element.tagName === "svg"; } var valueTypes = [ ...dimensionValueTypes, color, complex ]; var findValueType = (v) => valueTypes.find(testValueType(v)); var createAxis = () => ({ min: 0, max: 0 }); var createBox = () => ({ x: createAxis(), y: createAxis() }); var visualElementStore = new WeakMap(); function isAnimationControls(v) { return v !== null && typeof v === "object" && typeof v.start === "function"; } function isVariantLabel(v) { return typeof v === "string" || Array.isArray(v); } var variantProps = ["initial", ...[ "animate", "whileInView", "whileFocus", "whileHover", "whileTap", "whileDrag", "exit" ]]; function isControllingVariants(props) { return isAnimationControls(props.animate) || variantProps.some((name) => isVariantLabel(props[name])); } function isVariantNode(props) { return Boolean(isControllingVariants(props) || props.variants); } function updateMotionValuesFromProps(element, next, prev) { for (const key in next) { const nextValue = next[key]; const prevValue = prev[key]; if (isMotionValue(nextValue)) element.addValue(key, nextValue); else if (isMotionValue(prevValue)) element.addValue(key, motionValue(nextValue, { owner: element })); else if (prevValue !== nextValue) if (element.hasValue(key)) { const existingValue = element.getValue(key); if (existingValue.liveStyle === true) existingValue.jump(nextValue); else if (!existingValue.hasAnimated) existingValue.set(nextValue); } else { const latestValue = element.getStaticValue(key); element.addValue(key, motionValue(latestValue !== void 0 ? latestValue : nextValue, { owner: element })); } } for (const key in prev) if (next[key] === void 0) element.removeValue(key); return next; } var prefersReducedMotion$1 = { current: null }; var hasReducedMotionListener = { current: false }; var isBrowser = typeof window !== "undefined"; function initPrefersReducedMotion() { hasReducedMotionListener.current = true; if (!isBrowser) return; if (window.matchMedia) { const motionMediaQuery = window.matchMedia("(prefers-reduced-motion)"); const setReducedMotionPreferences = () => prefersReducedMotion$1.current = motionMediaQuery.matches; motionMediaQuery.addEventListener("change", setReducedMotionPreferences); setReducedMotionPreferences(); } else prefersReducedMotion$1.current = false; } var propEventHandlers = [ "AnimationStart", "AnimationComplete", "Update", "BeforeLayoutMeasure", "LayoutMeasure", "LayoutAnimationStart", "LayoutAnimationComplete" ]; var featureDefinitions = {}; var VisualElement = class { scrapeMotionValuesFromProps(_props, _prevProps, _visualElement) { return {}; } constructor({ parent, props, presenceContext, reducedMotionConfig, skipAnimations, blockInitialAnimation, visualState }, options = {}) { this.current = null; this.children = new Set(); this.isVariantNode = false; this.isControllingVariants = false; this.shouldReduceMotion = null; this.shouldSkipAnimations = false; this.values = new Map(); this.KeyframeResolver = KeyframeResolver; this.features = {}; this.valueSubscriptions = new Map(); this.prevMotionValues = {}; this.hasBeenMounted = false; this.events = {}; this.propEventSubscriptions = {}; this.notifyUpdate = () => this.notify("Update", this.latestValues); this.render = () => { if (!this.current) return; this.triggerBuild(); this.renderInstance(this.current, this.renderState, this.props.style, this.projection); }; this.renderScheduledAt = 0; this.scheduleRender = () => { const now = time.now(); if (this.renderScheduledAt < now) { this.renderScheduledAt = now; frame.render(this.render, false, true); } }; const { latestValues, renderState } = visualState; this.latestValues = latestValues; this.baseTarget = { ...latestValues }; this.initialValues = props.initial ? { ...latestValues } : {}; this.renderState = renderState; this.parent = parent; this.props = props; this.presenceContext = presenceContext; this.depth = parent ? parent.depth + 1 : 0; this.reducedMotionConfig = reducedMotionConfig; this.skipAnimationsConfig = skipAnimations; this.options = options; this.blockInitialAnimation = Boolean(blockInitialAnimation); this.isControllingVariants = isControllingVariants(props); this.isVariantNode = isVariantNode(props); if (this.isVariantNode) this.variantChildren = new Set(); this.manuallyAnimateOnMount = Boolean(parent && parent.current); const { willChange, ...initialMotionValues } = this.scrapeMotionValuesFromProps(props, {}, this); for (const key in initialMotionValues) { const value = initialMotionValues[key]; if (latestValues[key] !== void 0 && isMotionValue(value)) value.set(latestValues[key]); } } mount(instance) { if (this.hasBeenMounted) for (const key in this.initialValues) { this.values.get(key)?.jump(this.initialValues[key]); this.latestValues[key] = this.initialValues[key]; } this.current = instance; visualElementStore.set(instance, this); if (this.projection && !this.projection.instance) this.projection.mount(instance); if (this.parent && this.isVariantNode && !this.isControllingVariants) this.removeFromVariantTree = this.parent.addVariantChild(this); this.values.forEach((value, key) => this.bindToMotionValue(key, value)); if (this.reducedMotionConfig === "never") this.shouldReduceMotion = false; else if (this.reducedMotionConfig === "always") this.shouldReduceMotion = true; else { if (!hasReducedMotionListener.current) initPrefersReducedMotion(); this.shouldReduceMotion = prefersReducedMotion$1.current; } this.shouldSkipAnimations = this.skipAnimationsConfig ?? false; this.parent?.addChild(this); this.update(this.props, this.presenceContext); this.hasBeenMounted = true; } unmount() { this.projection && this.projection.unmount(); cancelFrame(this.notifyUpdate); cancelFrame(this.render); this.valueSubscriptions.forEach((remove) => remove()); this.valueSubscriptions.clear(); this.removeFromVariantTree && this.removeFromVariantTree(); this.parent?.removeChild(this); for (const key in this.events) this.events[key].clear(); for (const key in this.features) { const feature = this.features[key]; if (feature) { feature.unmount(); feature.isMounted = false; } } this.current = null; } addChild(child) { this.children.add(child); this.enteringChildren ?? (this.enteringChildren = new Set()); this.enteringChildren.add(child); } removeChild(child) { this.children.delete(child); this.enteringChildren && this.enteringChildren.delete(child); } bindToMotionValue(key, value) { if (this.valueSubscriptions.has(key)) this.valueSubscriptions.get(key)(); if (value.accelerate && acceleratedValues.has(key) && this.current instanceof HTMLElement) { const { factory, keyframes, times, ease, duration } = value.accelerate; const animation = new NativeAnimation({ element: this.current, name: key, keyframes, times, ease, duration: secondsToMilliseconds(duration) }); const cleanup = factory(animation); this.valueSubscriptions.set(key, () => { cleanup(); animation.cancel(); }); return; } const valueIsTransform = transformProps.has(key); if (valueIsTransform && this.onBindTransform) this.onBindTransform(); const removeOnChange = value.on("change", (latestValue) => { this.latestValues[key] = latestValue; this.props.onUpdate && frame.preRender(this.notifyUpdate); if (valueIsTransform && this.projection) this.projection.isTransformDirty = true; this.scheduleRender(); }); let removeSyncCheck; if (typeof window !== "undefined" && window.MotionCheckAppearSync) removeSyncCheck = window.MotionCheckAppearSync(this, key, value); this.valueSubscriptions.set(key, () => { removeOnChange(); if (removeSyncCheck) removeSyncCheck(); }); } sortNodePosition(other) { if (!this.current || !this.sortInstanceNodePosition || this.type !== other.type) return 0; return this.sortInstanceNodePosition(this.current, other.current); } updateFeatures() { let key = "animation"; for (key in featureDefinitions) { const featureDefinition = featureDefinitions[key]; if (!featureDefinition) continue; const { isEnabled, Feature: FeatureConstructor } = featureDefinition; if (!this.features[key] && FeatureConstructor && isEnabled(this.props)) this.features[key] = new FeatureConstructor(this); if (this.features[key]) { const feature = this.features[key]; if (feature.isMounted) feature.update(); else { feature.mount(); feature.isMounted = true; } } } } triggerBuild() { this.build(this.renderState, this.latestValues, this.props); } measureViewportBox() { return this.current ? this.measureInstanceViewportBox(this.current, this.props) : createBox(); } getStaticValue(key) { return this.latestValues[key]; } setStaticValue(key, value) { this.latestValues[key] = value; } update(props, presenceContext) { if (props.transformTemplate || this.props.transformTemplate) this.scheduleRender(); this.prevProps = this.props; this.props = props; this.prevPresenceContext = this.presenceContext; this.presenceContext = presenceContext; for (let i = 0; i < propEventHandlers.length; i++) { const key = propEventHandlers[i]; if (this.propEventSubscriptions[key]) { this.propEventSubscriptions[key](); delete this.propEventSubscriptions[key]; } const listener = props["on" + key]; if (listener) this.propEventSubscriptions[key] = this.on(key, listener); } this.prevMotionValues = updateMotionValuesFromProps(this, this.scrapeMotionValuesFromProps(props, this.prevProps || {}, this), this.prevMotionValues); if (this.handleChildMotionValue) this.handleChildMotionValue(); } getProps() { return this.props; } getVariant(name) { return this.props.variants ? this.props.variants[name] : void 0; } getDefaultTransition() { return this.props.transition; } getTransformPagePoint() { return this.props.transformPagePoint; } getClosestVariantNode() { return this.isVariantNode ? this : this.parent ? this.parent.getClosestVariantNode() : void 0; } addVariantChild(child) { const closestVariantNode = this.getClosestVariantNode(); if (closestVariantNode) { closestVariantNode.variantChildren && closestVariantNode.variantChildren.add(child); return () => closestVariantNode.variantChildren.delete(child); } } addValue(key, value) { const existingValue = this.values.get(key); if (value !== existingValue) { if (existingValue) this.removeValue(key); this.bindToMotionValue(key, value); this.values.set(key, value); this.latestValues[key] = value.get(); } } removeValue(key) { this.values.delete(key); const unsubscribe = this.valueSubscriptions.get(key); if (unsubscribe) { unsubscribe(); this.valueSubscriptions.delete(key); } delete this.latestValues[key]; this.removeValueFromRenderState(key, this.renderState); } hasValue(key) { return this.values.has(key); } getValue(key, defaultValue) { if (this.props.values && this.props.values[key]) return this.props.values[key]; let value = this.values.get(key); if (value === void 0 && defaultValue !== void 0) { value = motionValue(defaultValue === null ? void 0 : defaultValue, { owner: this }); this.addValue(key, value); } return value; } readValue(key, target) { let value = this.latestValues[key] !== void 0 || !this.current ? this.latestValues[key] : this.getBaseTargetFromProps(this.props, key) ?? this.readValueFromInstance(this.current, key, this.options); if (value !== void 0 && value !== null) { if (typeof value === "string" && (isNumericalString(value) || isZeroValueString(value))) value = parseFloat(value); else if (!findValueType(value) && complex.test(target)) value = getAnimatableNone(key, target); this.setBaseTarget(key, isMotionValue(value) ? value.get() : value); } return isMotionValue(value) ? value.get() : value; } setBaseTarget(key, value) { this.baseTarget[key] = value; } getBaseTarget(key) { const { initial } = this.props; let valueFromInitial; if (typeof initial === "string" || typeof initial === "object") { const variant = resolveVariantFromProps(this.props, initial, this.presenceContext?.custom); if (variant) valueFromInitial = variant[key]; } if (initial && valueFromInitial !== void 0) return valueFromInitial; const target = this.getBaseTargetFromProps(this.props, key); if (target !== void 0 && !isMotionValue(target)) return target; return this.initialValues[key] !== void 0 && valueFromInitial === void 0 ? void 0 : this.baseTarget[key]; } on(eventName, callback) { if (!this.events[eventName]) this.events[eventName] = new SubscriptionManager(); return this.events[eventName].add(callback); } notify(eventName, ...args) { if (this.events[eventName]) this.events[eventName].notify(...args); } scheduleRenderMicrotask() { microtask.render(this.render); } }; var DOMVisualElement = class extends VisualElement { constructor() { super(...arguments); this.KeyframeResolver = DOMKeyframesResolver; } sortInstanceNodePosition(a, b) { return a.compareDocumentPosition(b) & 2 ? 1 : -1; } getBaseTargetFromProps(props, key) { const style = props.style; return style ? style[key] : void 0; } removeValueFromRenderState(key, { vars, style }) { delete vars[key]; delete style[key]; } handleChildMotionValue() { if (this.childSubscription) { this.childSubscription(); delete this.childSubscription; } const { children } = this.props; if (isMotionValue(children)) this.childSubscription = children.on("change", (latest) => { if (this.current) this.current.textContent = `${latest}`; }); } }; function convertBoundingBoxToBox({ top, left, right, bottom }) { return { x: { min: left, max: right }, y: { min: top, max: bottom } }; } function transformBoxPoints(point, transformPoint) { if (!transformPoint) return point; const topLeft = transformPoint({ x: point.left, y: point.top }); const bottomRight = transformPoint({ x: point.right, y: point.bottom }); return { top: topLeft.y, left: topLeft.x, bottom: bottomRight.y, right: bottomRight.x }; } function measureViewportBox(instance, transformPoint) { return convertBoundingBoxToBox(transformBoxPoints(instance.getBoundingClientRect(), transformPoint)); } var translateAlias = { x: "translateX", y: "translateY", z: "translateZ", transformPerspective: "perspective" }; var numTransforms = transformPropOrder.length; function buildTransform(latestValues, transform, transformTemplate) { let transformString = ""; let transformIsDefault = true; for (let i = 0; i < numTransforms; i++) { const key = transformPropOrder[i]; const value = latestValues[key]; if (value === void 0) continue; let valueIsDefault = true; if (typeof value === "number") valueIsDefault = value === (key.startsWith("scale") ? 1 : 0); else { const parsed = parseFloat(value); valueIsDefault = key.startsWith("scale") ? parsed === 1 : parsed === 0; } if (!valueIsDefault || transformTemplate) { const valueAsType = getValueAsType(value, numberValueTypes[key]); if (!valueIsDefault) { transformIsDefault = false; const transformName = translateAlias[key] || key; transformString += `${transformName}(${valueAsType}) `; } if (transformTemplate) transform[key] = valueAsType; } } const pathRotation = latestValues.pathRotation; if (pathRotation) { transformIsDefault = false; transformString += `rotate(${getValueAsType(pathRotation, numberValueTypes.pathRotation)}) `; } transformString = transformString.trim(); if (transformTemplate) transformString = transformTemplate(transform, transformIsDefault ? "" : transformString); else if (transformIsDefault) transformString = "none"; return transformString; } function buildHTMLStyles(state, latestValues, transformTemplate) { const { style, vars, transformOrigin } = state; let hasTransform = false; let hasTransformOrigin = false; for (const key in latestValues) { const value = latestValues[key]; if (transformProps.has(key)) { hasTransform = true; continue; } else if (isCSSVariableName(key)) { vars[key] = value; continue; } else { const valueAsType = getValueAsType(value, numberValueTypes[key]); if (key.startsWith("origin")) { hasTransformOrigin = true; transformOrigin[key] = valueAsType; } else style[key] = valueAsType; } } if (!latestValues.transform) { if (hasTransform || transformTemplate) style.transform = buildTransform(latestValues, state.transform, transformTemplate); else if (style.transform) style.transform = "none"; } if (hasTransformOrigin) { const { originX = "50%", originY = "50%", originZ = 0 } = transformOrigin; style.transformOrigin = `${originX} ${originY} ${originZ}`; } } function renderHTML(element, { style, vars }, styleProp, projection) { const elementStyle = element.style; let key; for (key in style) elementStyle[key] = style[key]; projection?.applyProjectionStyles(elementStyle, styleProp); for (key in vars) elementStyle.setProperty(key, vars[key]); } function pixelsToPercent(pixels, axis) { if (axis.max === axis.min) return 0; return pixels / (axis.max - axis.min) * 100; } var correctBorderRadius = { correct: (latest, node) => { if (!node.target) return latest; if (typeof latest === "string") if (px.test(latest)) latest = parseFloat(latest); else return latest; return `${pixelsToPercent(latest, node.target.x)}% ${pixelsToPercent(latest, node.target.y)}%`; } }; var correctBoxShadow = { correct: (latest, { treeScale, projectionDelta }) => { const original = latest; const shadow = complex.parse(latest); if (shadow.length > 5) return original; const template = complex.createTransformer(latest); const offset = typeof shadow[0] !== "number" ? 1 : 0; const xScale = projectionDelta.x.scale * treeScale.x; const yScale = projectionDelta.y.scale * treeScale.y; shadow[0 + offset] /= xScale; shadow[1 + offset] /= yScale; const averageScale = mixNumber$1(xScale, yScale, .5); if (typeof shadow[2 + offset] === "number") shadow[2 + offset] /= averageScale; if (typeof shadow[3 + offset] === "number") shadow[3 + offset] /= averageScale; return template(shadow); } }; var scaleCorrectors = { borderRadius: { ...correctBorderRadius, applyTo: [...cornerRadiusProps] }, borderTopLeftRadius: correctBorderRadius, borderTopRightRadius: correctBorderRadius, borderBottomLeftRadius: correctBorderRadius, borderBottomRightRadius: correctBorderRadius, boxShadow: correctBoxShadow }; function isForcedMotionValue(key, { layout, layoutId }) { return transformProps.has(key) || key.startsWith("origin") || (layout || layoutId !== void 0) && (!!scaleCorrectors[key] || key === "opacity"); } function scrapeMotionValuesFromProps$1(props, prevProps, visualElement) { const style = props.style; const prevStyle = prevProps?.style; const newValues = {}; if (!style) return newValues; for (const key in style) if (isMotionValue(style[key]) || prevStyle && isMotionValue(prevStyle[key]) || isForcedMotionValue(key, props) || visualElement?.getValue(key)?.liveStyle !== void 0) newValues[key] = style[key]; return newValues; } function getComputedStyle$1(element) { return window.getComputedStyle(element); } var HTMLVisualElement = class extends DOMVisualElement { constructor() { super(...arguments); this.type = "html"; this.renderInstance = renderHTML; } readValueFromInstance(instance, key) { if (transformProps.has(key)) return this.projection?.isProjecting ? defaultTransformValue(key) : readTransformValue(instance, key); else { const computedStyle = getComputedStyle$1(instance); const value = (isCSSVariableName(key) ? computedStyle.getPropertyValue(key) : computedStyle[key]) || 0; return typeof value === "string" ? value.trim() : value; } } measureInstanceViewportBox(instance, { transformPagePoint }) { return measureViewportBox(instance, transformPagePoint); } build(renderState, latestValues, props) { buildHTMLStyles(renderState, latestValues, props.transformTemplate); } scrapeMotionValuesFromProps(props, prevProps, visualElement) { return scrapeMotionValuesFromProps$1(props, prevProps, visualElement); } }; function isObjectKey(key, object) { return key in object; } var ObjectVisualElement = class extends VisualElement { constructor() { super(...arguments); this.type = "object"; } readValueFromInstance(instance, key) { if (isObjectKey(key, instance)) { const value = instance[key]; if (typeof value === "string" || typeof value === "number") return value; } } getBaseTargetFromProps() {} removeValueFromRenderState(key, renderState) { delete renderState.output[key]; } measureInstanceViewportBox() { return createBox(); } build(renderState, latestValues) { Object.assign(renderState.output, latestValues); } renderInstance(instance, { output }) { Object.assign(instance, output); } sortInstanceNodePosition() { return 0; } }; var dashKeys = { offset: "stroke-dashoffset", array: "stroke-dasharray" }; var camelKeys = { offset: "strokeDashoffset", array: "strokeDasharray" }; function buildSVGPath(attrs, length, spacing = 1, offset = 0, useDashCase = true) { attrs.pathLength = 1; const keys = useDashCase ? dashKeys : camelKeys; attrs[keys.offset] = `${-offset}`; attrs[keys.array] = `${length} ${spacing}`; } var cssMotionPathProperties = [ "offsetDistance", "offsetPath", "offsetRotate", "offsetAnchor" ]; function buildSVGAttrs(state, { attrX, attrY, attrScale, pathLength, pathSpacing = 1, pathOffset = 0, ...latest }, isSVGTag, transformTemplate, styleProp) { buildHTMLStyles(state, latest, transformTemplate); if (isSVGTag) { if (state.style.viewBox) state.attrs.viewBox = state.style.viewBox; return; } state.attrs = state.style; state.style = {}; const { attrs, style } = state; if (attrs.transform) { style.transform = attrs.transform; delete attrs.transform; } if (style.transform || attrs.transformOrigin) { style.transformOrigin = attrs.transformOrigin ?? "50% 50%"; delete attrs.transformOrigin; } if (style.transform) { style.transformBox = styleProp?.transformBox ?? "fill-box"; delete attrs.transformBox; } for (const key of cssMotionPathProperties) if (attrs[key] !== void 0) { style[key] = attrs[key]; delete attrs[key]; } if (attrX !== void 0) attrs.x = attrX; if (attrY !== void 0) attrs.y = attrY; if (attrScale !== void 0) attrs.scale = attrScale; if (pathLength !== void 0) buildSVGPath(attrs, pathLength, pathSpacing, pathOffset, false); } var camelCaseAttributes = new Set([ "baseFrequency", "diffuseConstant", "kernelMatrix", "kernelUnitLength", "keySplines", "keyTimes", "limitingConeAngle", "markerHeight", "markerWidth", "numOctaves", "targetX", "targetY", "surfaceScale", "specularConstant", "specularExponent", "stdDeviation", "tableValues", "viewBox", "gradientTransform", "pathLength", "startOffset", "textLength", "lengthAdjust" ]); var isSVGTag = (tag) => typeof tag === "string" && tag.toLowerCase() === "svg"; function renderSVG(element, renderState, _styleProp, projection) { renderHTML(element, renderState, void 0, projection); for (const key in renderState.attrs) element.setAttribute(!camelCaseAttributes.has(key) ? camelToDash(key) : key, renderState.attrs[key]); } function scrapeMotionValuesFromProps(props, prevProps, visualElement) { const newValues = scrapeMotionValuesFromProps$1(props, prevProps, visualElement); for (const key in props) if (isMotionValue(props[key]) || isMotionValue(prevProps[key])) { const targetKey = transformPropOrder.indexOf(key) !== -1 ? "attr" + key.charAt(0).toUpperCase() + key.substring(1) : key; newValues[targetKey] = props[key]; } return newValues; } var SVGVisualElement = class extends DOMVisualElement { constructor() { super(...arguments); this.type = "svg"; this.isSVGTag = false; this.measureInstanceViewportBox = createBox; } getBaseTargetFromProps(props, key) { return props[key]; } readValueFromInstance(instance, key) { if (transformProps.has(key)) { const defaultType = getDefaultValueType(key); return defaultType ? defaultType.default || 0 : 0; } key = !camelCaseAttributes.has(key) ? camelToDash(key) : key; return instance.getAttribute(key); } scrapeMotionValuesFromProps(props, prevProps, visualElement) { return scrapeMotionValuesFromProps(props, prevProps, visualElement); } build(renderState, latestValues, props) { buildSVGAttrs(renderState, latestValues, this.isSVGTag, props.transformTemplate, props.style); } renderInstance(instance, renderState, styleProp, projection) { renderSVG(instance, renderState, styleProp, projection); } mount(instance) { this.isSVGTag = isSVGTag(instance.tagName); super.mount(instance); } }; function animateSingleValue(value, keyframes, options) { const motionValue$1 = isMotionValue(value) ? value : motionValue(value); motionValue$1.start(animateMotionValue("", motionValue$1, keyframes, options)); return motionValue$1.animation; } function isDOMKeyframes(keyframes) { return typeof keyframes === "object" && !Array.isArray(keyframes); } function resolveSubjects(subject, keyframes, scope, selectorCache) { if (subject == null) return []; if (typeof subject === "string" && isDOMKeyframes(keyframes)) return resolveElements(subject, scope, selectorCache); else if (subject instanceof NodeList) return Array.from(subject); else if (Array.isArray(subject)) return subject.filter((s) => s != null); else return [subject]; } function calculateRepeatDuration(duration, repeat, repeatDelay) { return duration * (repeat + 1) + repeatDelay * repeat; } function calcNextTime(current, next, prev, labels) { if (typeof next === "number") return next; else if (next.startsWith("-") || next.startsWith("+")) return Math.max(0, current + parseFloat(next)); else if (next === "<") return prev; else if (next.startsWith("<")) return Math.max(0, prev + parseFloat(next.slice(1))); else return labels.get(next) ?? current; } function eraseKeyframes(sequence, startTime, endTime) { for (let i = 0; i < sequence.length; i++) { const keyframe = sequence[i]; if (keyframe.at > startTime && keyframe.at < endTime) { removeItem(sequence, keyframe); i--; } } } function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) { eraseKeyframes(sequence, startTime, endTime); for (let i = 0; i < keyframes.length; i++) sequence.push({ value: keyframes[i], at: mixNumber$1(startTime, endTime, offset[i]), easing: getEasingForSegment(easing, i) }); } function normalizeTimes(times, repeat, repeatDelayUnits = 0) { const totalUnits = repeat + 1 + repeat * repeatDelayUnits; for (let i = 0; i < times.length; i++) times[i] = times[i] / totalUnits; } function compareByTime(a, b) { if (a.at === b.at) { if (a.value === null) return 1; if (b.value === null) return -1; return 0; } else return a.at - b.at; } var defaultSegmentEasing = "easeInOut"; var MAX_REPEAT = 20; function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) { const defaultDuration = defaultTransition.duration || .3; const animationDefinitions = new Map(); const sequences = new Map(); const elementCache = {}; const timeLabels = new Map(); let prevTime = 0; let currentTime = 0; let totalDuration = 0; for (let i = 0; i < sequence.length; i++) { const segment = sequence[i]; if (typeof segment === "string") { timeLabels.set(segment, currentTime); continue; } else if (!Array.isArray(segment)) { timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels)); continue; } let [subject, keyframes, transition = {}] = segment; if (transition.at !== void 0) currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels); let maxDuration = 0; const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => { const valueKeyframesAsList = keyframesAsList(valueKeyframes); const { delay = 0, times = defaultOffset(valueKeyframesAsList), type = defaultTransition.type || "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition; let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition; const calculatedDelay = typeof delay === "function" ? delay(elementIndex, numSubjects) : delay; const numKeyframes = valueKeyframesAsList.length; const createGenerator = isGenerator(type) ? type : generators?.[type || "keyframes"]; if (numKeyframes <= 2 && createGenerator) { let absoluteDelta = 100; if (numKeyframes === 2 && isNumberKeyframesArray(valueKeyframesAsList)) { const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0]; absoluteDelta = Math.abs(delta); } const springTransition = { ...defaultTransition, ...remainingTransition }; if (duration !== void 0) springTransition.duration = secondsToMilliseconds(duration); const springEasing = createGeneratorEasing(springTransition, absoluteDelta, createGenerator); ease = springEasing.ease; duration = springEasing.duration; } duration ?? (duration = defaultDuration); const startTime = currentTime + calculatedDelay; if (times.length === 1 && times[0] === 0) times[1] = 1; const remainder = times.length - valueKeyframesAsList.length; remainder > 0 && fillOffset(times, remainder); valueKeyframesAsList.length === 1 && valueKeyframesAsList.unshift(null); if (repeat) `${repeat}${MAX_REPEAT}`; if (repeat && repeat < MAX_REPEAT) { const repeatDelayUnits = duration > 0 ? repeatDelay / duration : 0; duration = calculateRepeatDuration(duration, repeat, repeatDelay); const originalKeyframes = [...valueKeyframesAsList]; const originalTimes = [...times]; ease = Array.isArray(ease) ? [...ease] : [ease]; const originalEase = [...ease]; const isFlipping = repeatType === "reverse" || repeatType === "mirror"; let flippedKeyframes = originalKeyframes; let flippedEases = originalEase; if (isFlipping) { flippedKeyframes = [...originalKeyframes].reverse(); if (repeatType === "reverse") flippedEases = [...originalEase].reverse().map((e) => typeof e === "function" ? reverseEasing(e) : e); } for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) { const isFlipped = isFlipping && repeatIndex % 2 === 0; const iterKeyframes = isFlipped ? flippedKeyframes : originalKeyframes; const iterEase = isFlipped ? flippedEases : originalEase; const iterStartOffset = (repeatIndex + 1) * (1 + repeatDelayUnits); if (repeatDelayUnits > 0) { valueKeyframesAsList.push(valueKeyframesAsList[valueKeyframesAsList.length - 1]); times.push(iterStartOffset); ease.push("linear"); } valueKeyframesAsList.push(...iterKeyframes); for (let keyframeIndex = 0; keyframeIndex < iterKeyframes.length; keyframeIndex++) { times.push(originalTimes[keyframeIndex] + iterStartOffset); ease.push(keyframeIndex === 0 ? "linear" : getEasingForSegment(iterEase, keyframeIndex - 1)); } } normalizeTimes(times, repeat, repeatDelayUnits); } const targetTime = startTime + duration; addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime); maxDuration = Math.max(calculatedDelay + duration, maxDuration); totalDuration = Math.max(targetTime, totalDuration); }; if (isMotionValue(subject)) { const subjectSequence = getSubjectSequence(subject, sequences); resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence)); } else { const subjects = resolveSubjects(subject, keyframes, scope, elementCache); const numSubjects = subjects.length; for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) { keyframes = keyframes; transition = transition; const thisSubject = subjects[subjectIndex]; const subjectSequence = getSubjectSequence(thisSubject, sequences); for (const key in keyframes) resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects); } } prevTime = currentTime; currentTime += maxDuration; } sequences.forEach((valueSequences, element) => { for (const key in valueSequences) { const valueSequence = valueSequences[key]; valueSequence.sort(compareByTime); const keyframes = []; const valueOffset = []; const valueEasing = []; for (let i = 0; i < valueSequence.length; i++) { const { at, value, easing } = valueSequence[i]; keyframes.push(value); valueOffset.push(progress(0, totalDuration, at)); valueEasing.push(easing || "easeOut"); } if (valueOffset[0] !== 0) { valueOffset.unshift(0); keyframes.unshift(keyframes[0]); valueEasing.unshift(defaultSegmentEasing); } if (valueOffset[valueOffset.length - 1] !== 1) { valueOffset.push(1); keyframes.push(null); } if (!animationDefinitions.has(element)) animationDefinitions.set(element, { keyframes: {}, transition: {} }); const definition = animationDefinitions.get(element); definition.keyframes[key] = keyframes; const { type: _type, ...remainingDefaultTransition } = defaultTransition; definition.transition[key] = { ...remainingDefaultTransition, duration: totalDuration, ease: valueEasing, times: valueOffset, ...sequenceTransition }; } }); return animationDefinitions; } function getSubjectSequence(subject, sequences) { !sequences.has(subject) && sequences.set(subject, {}); return sequences.get(subject); } function getValueSequence(name, sequences) { if (!sequences[name]) sequences[name] = []; return sequences[name]; } function keyframesAsList(keyframes) { return Array.isArray(keyframes) ? keyframes : [keyframes]; } function getValueTransition(transition, key) { return transition && transition[key] ? { ...transition, ...transition[key] } : { ...transition }; } var isNumber = (keyframe) => typeof keyframe === "number"; var isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber); function createDOMVisualElement(element) { const options = { presenceContext: null, props: {}, visualState: { renderState: { transform: {}, transformOrigin: {}, style: {}, vars: {}, attrs: {} }, latestValues: {} } }; const node = isSVGElement(element) && !isSVGSVGElement(element) ? new SVGVisualElement(options) : new HTMLVisualElement(options); node.mount(element); visualElementStore.set(element, node); } function createObjectVisualElement(subject) { const node = new ObjectVisualElement({ presenceContext: null, props: {}, visualState: { renderState: { output: {} }, latestValues: {} } }); node.mount(subject); visualElementStore.set(subject, node); } function isSingleValue(subject, keyframes) { return isMotionValue(subject) || typeof subject === "number" || typeof subject === "string" && !isDOMKeyframes(keyframes); } function animateSubject(subject, keyframes, options, scope) { const animations = []; if (isSingleValue(subject, keyframes)) animations.push(animateSingleValue(subject, isDOMKeyframes(keyframes) ? keyframes.default || keyframes : keyframes, options ? options.default || options : options)); else { if (subject == null) return animations; const subjects = resolveSubjects(subject, keyframes, scope); const numSubjects = subjects.length; for (let i = 0; i < numSubjects; i++) { const thisSubject = subjects[i]; const createVisualElement = thisSubject instanceof Element ? createDOMVisualElement : createObjectVisualElement; if (!visualElementStore.has(thisSubject)) createVisualElement(thisSubject); const visualElement = visualElementStore.get(thisSubject); const transition = { ...options }; if ("delay" in transition && typeof transition.delay === "function") transition.delay = transition.delay(i, numSubjects); animations.push(...animateTarget(visualElement, { ...keyframes, transition }, {})); } } return animations; } function animateSequence(sequence, options, scope) { const animations = []; createAnimationsFromSequence(sequence.map((segment) => { if (Array.isArray(segment) && typeof segment[0] === "function") { const callback = segment[0]; const mv = motionValue(0); mv.on("change", callback); if (segment.length === 1) return [mv, [0, 1]]; else if (segment.length === 2) return [ mv, [0, 1], segment[1] ]; else return [ mv, segment[1], segment[2] ]; } return segment; }), options, scope, { spring }).forEach(({ keyframes, transition }, subject) => { animations.push(...animateSubject(subject, keyframes, transition)); }); return animations; } function isSequence(value) { return Array.isArray(value) && value.some(Array.isArray); } function createScopedAnimate(options = {}) { const { scope, reduceMotion, skipAnimations } = options; function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) { let animations = []; let animationOnComplete; const inherited = {}; if (reduceMotion !== void 0) inherited.reduceMotion = reduceMotion; if (skipAnimations !== void 0) inherited.skipAnimations = skipAnimations; if (isSequence(subjectOrSequence)) { const { onComplete, ...sequenceOptions } = optionsOrKeyframes || {}; if (typeof onComplete === "function") animationOnComplete = onComplete; animations = animateSequence(subjectOrSequence, { ...inherited, ...sequenceOptions }, scope); } else { const { onComplete, ...rest } = options || {}; if (typeof onComplete === "function") animationOnComplete = onComplete; animations = animateSubject(subjectOrSequence, optionsOrKeyframes, { ...inherited, ...rest }, scope); } const animation = new GroupAnimationWithThen(animations); if (animationOnComplete) animation.finished.then(animationOnComplete); if (scope) { scope.animations.push(animation); animation.finished.then(() => { removeItem(scope.animations, animation); }); } return animation; } return scopedAnimate; } var animate = createScopedAnimate(); var springConfigs = { carouselSnap: { damping: 18, stiffness: 200, type: "spring" }, contentEntrance: { damping: 28, stiffness: 300, type: "spring" }, modalBackdrop: { bounce: 0, duration: .4, type: "spring" }, modalSurface: { bounce: 0, duration: .35, type: "spring" }, ratingEntrance: { bounce: 0, duration: .3, type: "spring" }, reviewBodyEntrance: { bounce: 0, duration: .3, type: "spring" }, stickyNav: { bounce: 0, duration: .3, type: "spring" }, summaryEntrance: { bounce: 0, duration: .3, type: "spring" }, swipeDismissExit: { bounce: .2, duration: .4, type: "spring" }, swipeSettleBack: { damping: 15, stiffness: 180, type: "spring" } }; var animateWithReducedMotion = (element, options) => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return animate(element, options.reducedMotionProperties ?? options.properties, { duration: .2 }); return animate(element, options.properties, options.springConfig ?? springConfigs.contentEntrance); }; var playEntrance = (element, springConfig) => animateWithReducedMotion(element, { properties: { opacity: [0, 1], transform: ["translateY(4px)", "translateY(0)"] }, reducedMotionProperties: { opacity: [0, 1] }, springConfig }); var ENTERING_SURFACE_TRANSFORM = "scale(0.92) translateY(8px)"; var EXITING_SURFACE_TRANSFORM = "scale(0.92) translateY(100dvh)"; var reducedMotionQuery = "(prefers-reduced-motion: reduce)"; var prefersReducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false; var finishClosingAnimation = async (animationId, animations, animationIdRef, finishClose) => { try { await Promise.all(animations.map((animation) => animation.finished)); if (animationId === animationIdRef.current) finishClose(); } catch {} }; var ModalCloseContext = X(null); var useModalClose = () => { const close = x(ModalCloseContext); if (!close) throw new Error("useModalClose must be used inside a ModalShell"); return close; }; var focusableSelector = [ "button:not([disabled])", "iframe", "a[href]", "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])", "[tabindex]:not([tabindex='-1'])" ].join(", "); var focusableElements = (root) => [...root.querySelectorAll(focusableSelector)].filter((node) => !node.hasAttribute("disabled") && node.getAttribute("aria-hidden") !== "true"); var trapFocus = (event, root) => { if (event.key !== "Tab") return; const focusable = focusableElements(root); if (!focusable.length) { event.preventDefault(); root.focus(); return; } const [first] = focusable; if (!first) return; const last = focusable.at(-1); const active = document.activeElement; if (event.shiftKey && active === first) { event.preventDefault(); last?.focus(); return; } if (!event.shiftKey && active === last) { event.preventDefault(); first.focus(); } }; var useModalAccessibility = ({ onClose, overlayRef, surfaceRef }) => { h(() => { const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; const onKeydown = (event) => { if (event.key === "Escape") { onClose(); return; } const surface = surfaceRef.current; if (surface) trapFocus(event, surface); }; document.addEventListener("keydown", onKeydown); requestAnimationFrame(() => surfaceRef.current?.focus()); return () => { document.removeEventListener("keydown", onKeydown); document.body.style.overflow = previousOverflow; }; }, [onClose, surfaceRef]); h(() => { const overlay = overlayRef.current; if (!overlay) return; const onClick = (event) => { if (event.target === overlay) onClose(); }; overlay.addEventListener("click", onClick); return () => overlay.removeEventListener("click", onClick); }, [onClose, overlayRef]); }; var readTranslateY = (element) => { const computedTransform = getComputedStyle(element).transform; const transform = element.style.transform || computedTransform; const translateMatch = transform.match(/translateY\((?-?[\d.]+)px\)/u); if (translateMatch?.groups?.translateY) return Number(translateMatch.groups.translateY); const matrixMatch = transform.match(/^matrix\([^,]+,[^,]+,[^,]+,[^,]+,[^,]+,\s*(?-?[\d.]+)\)$/u); return matrixMatch?.groups?.translateY ? Number(matrixMatch.groups.translateY) : 0; }; var rubberBand = (distance) => -Math.min(distance * .35, 96); var useSwipeToDismiss = (options) => { const optionsRef = A(options); h(() => { optionsRef.current = options; }, [options]); const startYRef = A(0); const baseTranslationRef = A(0); const currentDeltaRef = A(0); const currentPositionRef = A(0); const activePointerIdRef = A(null); const pointerHistoryRef = A([]); const animationRef = A(null); const animationGenerationRef = A(0); const elementRef = A(null); const stopAnimation = q(() => { animationGenerationRef.current += 1; animationRef.current?.stop(); animationRef.current = null; }, []); const handlePointerDown = q((event) => { if (activePointerIdRef.current !== null) return; stopAnimation(); const element = event.currentTarget; elementRef.current = element; startYRef.current = event.clientY; baseTranslationRef.current = readTranslateY(element); currentDeltaRef.current = 0; currentPositionRef.current = baseTranslationRef.current; pointerHistoryRef.current = [{ time: performance.now(), y: event.clientY }]; activePointerIdRef.current = event.pointerId; element.setPointerCapture(event.pointerId); }, [stopAnimation]); const handlePointerMove = q((event) => { const element = elementRef.current; if (!element) return; if (event.pointerId !== activePointerIdRef.current) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; const position = baseTranslationRef.current + event.clientY - startYRef.current; currentPositionRef.current = position < 0 ? rubberBand(-position) : position; currentDeltaRef.current = Math.max(0, position); const history = pointerHistoryRef.current; history.push({ time: performance.now(), y: event.clientY }); if (history.length > 5) history.shift(); element.style.transform = `translateY(${currentPositionRef.current}px)`; }, []); const settlePointer = q((element) => { const delta = currentDeltaRef.current; const position = currentPositionRef.current; const history = pointerHistoryRef.current; currentDeltaRef.current = 0; currentPositionRef.current = 0; baseTranslationRef.current = 0; const { dismissThreshold = 80, velocityThreshold = 300, onDismiss } = optionsRef.current; let velocity = 0; if (history.length >= 2) { const [first] = history; const last = history.at(-1); if (first && last) { const dt = (last.time - first.time) / 1e3; if (dt > 0) velocity = (last.y - first.y) / dt; } } if (delta > dismissThreshold || velocity > velocityThreshold) { onDismiss({ position, velocity }); return; } if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) element.style.transform = ""; else { const animationGeneration = animationGenerationRef.current + 1; animationGenerationRef.current = animationGeneration; const anim = animate(element, { transform: "translateY(0)" }, springConfigs.swipeSettleBack); animationRef.current = anim; (async () => { try { await anim.finished; if (animationGenerationRef.current !== animationGeneration || animationRef.current !== anim) return; animationRef.current = null; element.style.transform = ""; } catch {} })(); } }, []); const handlePointerEnd = q((event) => { const element = elementRef.current; if (element && event.pointerId === activePointerIdRef.current) { activePointerIdRef.current = null; settlePointer(element); } }, [settlePointer]); return { ref: q((el) => { stopAnimation(); const prevEl = elementRef.current; if (prevEl) { prevEl.removeEventListener("pointerdown", handlePointerDown); prevEl.removeEventListener("pointermove", handlePointerMove); prevEl.removeEventListener("pointerup", handlePointerEnd); prevEl.removeEventListener("pointercancel", handlePointerEnd); } if (el) { el.addEventListener("pointerdown", handlePointerDown); el.addEventListener("pointermove", handlePointerMove); el.addEventListener("pointerup", handlePointerEnd); el.addEventListener("pointercancel", handlePointerEnd); elementRef.current = el; } else elementRef.current = null; }, [ handlePointerDown, handlePointerEnd, handlePointerMove, stopAnimation ]) }; }; var ModalShell = ({ ariaDescribedBy, ariaLabel, ariaLabelledBy, children, className, dismissable = false, id, onClose, surfaceClassName }) => { const overlayRef = A(null); const surfaceRef = A(null); const closeSourceRef = A("standard"); const swipeDismissRef = A(null); const realOnCloseRef = A(onClose); const [phase, setPhase] = d("open"); const [reducedMotion, setReducedMotion] = d(prefersReducedMotion); const didFinishCloseRef = A(false); const animationIdRef = A(0); const animationsRef = A([]); const session = useModalSession(); const previousSessionRef = A(session); h(() => { realOnCloseRef.current = onClose; }, [onClose]); h(() => { const mediaQuery = window.matchMedia?.(reducedMotionQuery); if (!mediaQuery) return; const updateReducedMotion = () => setReducedMotion(mediaQuery.matches); mediaQuery.addEventListener("change", updateReducedMotion); return () => mediaQuery.removeEventListener("change", updateReducedMotion); }, []); const finishClose = q(() => { if (didFinishCloseRef.current) return; didFinishCloseRef.current = true; realOnCloseRef.current(); }, []); const animateModal = q((state) => { const overlay = overlayRef.current; const surface = surfaceRef.current; if (!overlay || !surface) return; for (const animation of animationsRef.current) animation.stop(); const animationId = animationIdRef.current + 1; animationIdRef.current = animationId; const opening = state === "open"; const backdropAnimation = animateWithReducedMotion(overlay, { properties: { opacity: opening ? 1 : 0 }, reducedMotionProperties: { opacity: opening ? 1 : 0 }, springConfig: springConfigs.modalBackdrop }); const surfaceExitTransform = closeSourceRef.current === "swipe" ? "translateY(100dvh)" : EXITING_SURFACE_TRANSFORM; const surfaceSpring = closeSourceRef.current === "swipe" ? { ...springConfigs.swipeDismissExit, velocity: swipeDismissRef.current?.velocity ?? 0 } : springConfigs.modalSurface; if (!opening) { closeSourceRef.current = "standard"; swipeDismissRef.current = null; } const surfaceAnimation = animateWithReducedMotion(surface, { properties: { opacity: opening ? 1 : 0, transform: opening ? "scale(1) translateY(0)" : surfaceExitTransform }, reducedMotionProperties: { opacity: opening ? 1 : 0 }, springConfig: surfaceSpring }); animationsRef.current = [backdropAnimation, surfaceAnimation]; if (!opening) finishClosingAnimation(animationId, [backdropAnimation, surfaceAnimation], animationIdRef, finishClose); }, [finishClose]); _(() => { if (previousSessionRef.current === session) return; previousSessionRef.current = session; didFinishCloseRef.current = false; setPhase("open"); animateModal("open"); }, [animateModal, session]); const handleClose = q(() => { if (didFinishCloseRef.current) return; didFinishCloseRef.current = false; setPhase("closing"); animateModal("closed"); }, [animateModal]); const handleSwipeDismiss = q((details) => { closeSourceRef.current = "swipe"; swipeDismissRef.current = details; handleClose(); }, [handleClose]); const swipe = useSwipeToDismiss({ onDismiss: dismissable ? handleSwipeDismiss : (_details) => {} }); const surfaceRefCallback = q((el) => { surfaceRef.current = el; if (dismissable) swipe.ref(el); }, [dismissable, swipe]); h(() => { const frame = requestAnimationFrame(() => { if (!overlayRef.current) return; animateModal("open"); }); return () => { cancelAnimationFrame(frame); for (const animation of animationsRef.current) animation.stop(); }; }, [animateModal]); useModalAccessibility({ onClose: handleClose, overlayRef, surfaceRef }); return u(ModalCloseContext.Provider, { value: handleClose, children: u("dialog", { "aria-describedby": ariaDescribedBy, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-modal": "true", class: className, id, open: true, ref: overlayRef, style: { opacity: 0, pointerEvents: phase === "open" ? "auto" : "none" }, children: u("div", { class: surfaceClassName, onClick: (event) => event.stopPropagation(), ref: dismissable ? surfaceRefCallback : surfaceRef, role: "none", style: { opacity: 0, transform: reducedMotion ? "none" : ENTERING_SURFACE_TRANSFORM, ...dismissable ? { touchAction: "pan-x" } : {} }, tabindex: -1, children }) }) }); }; var MODAL_ID$1 = "atv-poster-modal"; var PosterModalContent = ({ alt, previewSrc, src }) => { const close = useModalClose(); const hasPreview = Boolean(previewSrc && previewSrc !== src); const [imageState, setImageState] = d(hasPreview ? "loading" : "loaded"); return u("div", { class: `atv-image-modal-frame is-${imageState}`, children: [ u(ModalCloseButton, { ariaLabel: "关闭图片预览", className: "atv-image-modal-close", onClick: close }), hasPreview ? u("img", { alt: "", "aria-hidden": "true", class: "atv-modal-preview", src: previewSrc }) : null, u("img", { alt: alt || "", class: "atv-modal-img", onError: () => { if (hasPreview) setImageState("preview"); }, onLoad: () => setImageState("loaded"), src }), imageState === "loading" ? u("output", { "aria-live": "polite", class: "atv-image-modal-loading", children: [u("span", { "aria-hidden": "true", class: "atv-spinner" }), u("span", { children: "加载图片中…" })] }) : null ] }); }; var PosterModal = ({ alt, onClose, previewSrc, src }) => u(ModalShell, { ariaLabel: "海报预览", className: "atv-modal-overlay", id: MODAL_ID$1, onClose, surfaceClassName: "atv-modal-surface", children: u(PosterModalContent, { alt, ...previewSrc ? { previewSrc } : {}, src }, src) }); var useModalRequest = () => { const [active, setActive] = d(null); return { active, handleClose: q(() => setActive(null), []), handleOpen: q((value) => { setActive({ value }); }, []) }; }; var REVEAL_THRESHOLD = 0; var useSectionReveal = (ref) => { h(() => { const element = ref.current; if (!element) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches || typeof IntersectionObserver === "undefined") { element.classList.add("is-revealed"); return; } const observer = new IntersectionObserver((entries) => { for (const entry of entries) if (entry.isIntersecting) { element.classList.add("is-revealed"); observer.unobserve(element); } }, { threshold: [REVEAL_THRESHOLD] }); observer.observe(element); return () => { observer.disconnect(); }; }, [ref]); }; var Section = ({ children, id, moreLink, title }) => { const ref = A(null); useSectionReveal(ref); return u("section", { class: "atv-section atv-section-reveal", "data-atv-section-reveal": true, id, ref, children: [moreLink ? u("div", { class: "atv-section-h-row", children: [u("h2", { class: "atv-section-h", children: title }), u("a", { class: "atv-section-more", href: moreLink.href, rel: "noopener", target: "_blank", children: moreLink.text })] }) : u("h2", { class: "atv-section-h", children: title }), children] }); }; var AwardLink = ({ children, href }) => href ? u("a", { href, rel: "noreferrer", target: "_blank", children }) : u("span", { children }); var PersonageAwardsSection = ({ awards }) => { if (!awards) return null; return u(Section, { id: "atv-personage-awards", ...awards.allAwardsHref ? { moreLink: { href: awards.allAwardsHref, text: "查看全部 →" } } : {}, title: "荣誉", children: awards.awards.length ? u("ul", { class: "atv-personage-awards-list", children: awards.awards.map((award) => u("li", { children: [u("time", { class: "atv-personage-award-year", children: award.year }), u("div", { class: "atv-personage-award-detail", children: [ u("span", { class: "atv-personage-award-ceremony", children: u(AwardLink, { href: award.ceremonyHref, children: award.ceremony }) }), u("p", { class: "atv-personage-award-name", children: award.award }), award.work ? u("p", { class: "atv-personage-award-work", children: u(AwardLink, { href: award.workHref, children: award.work }) }) : null ] })] }, `${award.year}-${award.ceremony}-${award.award}`)) }) : u("p", { class: "atv-personage-awards-empty", children: "暂无公开获奖记录" }) }); }; var formatAspectRatio = (ratio) => { const fraction = ratio.toFixed(4); if (fraction.endsWith("6667")) return "2 / 3"; if (fraction.endsWith("7500")) return "3 / 4"; if (fraction.endsWith("3333")) return "4 / 3"; if (fraction.endsWith("5000")) return "3 / 2"; if (fraction.endsWith("0000")) return `${Math.round(ratio)}`; return String(ratio); }; var SafeImage = ({ src, alt, className, loading = "lazy", aspectRatio, staggerIndex, onLoad, fallback }) => { const [failed, setFailed] = d(false); if (!src) return fallback ?? u("div", { "aria-hidden": "true", class: "atv-safe-image-fallback" }); if (failed) return fallback ?? u("div", { "aria-hidden": "true", class: "atv-safe-image-fallback" }); const img = u("img", { alt, class: className, loading, onError: () => setFailed(true), onLoad: (event) => { const image = event.currentTarget; if (image.naturalWidth && image.naturalHeight) onLoad?.({ height: image.naturalHeight, width: image.naturalWidth }); }, src }); if (aspectRatio !== void 0 || staggerIndex !== void 0) { const vars = {}; if (aspectRatio !== void 0) vars.aspectRatio = formatAspectRatio(aspectRatio); if (staggerIndex !== void 0) vars["--stagger-index"] = String(staggerIndex); return u("div", { class: "atv-safe-image-container", style: vars, children: img }); } return img; }; var PersonageCollaborators = ({ collaborators }) => { if (!collaborators?.collaborators.length) return null; const { allCollaboratorsHref, collaborators: entries } = collaborators; return u(Section, { id: "atv-personage-collaborators", title: "合作", ...allCollaboratorsHref ? { moreLink: { href: allCollaboratorsHref, text: "查看全部 →" } } : {}, children: u("ol", { class: "atv-personage-collaborators", children: entries.map((collaborator, index) => u("li", { class: "atv-personage-collaborator", "data-leading": index === 0 ? "true" : void 0, style: { "--stagger-index": String(index) }, children: [ u("span", { "aria-hidden": "true", class: "atv-personage-collaborator-rank", children: index + 1 }), collaborator.avatar ? u(SafeImage, { alt: "", className: "atv-personage-collaborator-avatar", fallback: u("span", { "aria-hidden": "true", class: "atv-personage-collaborator-avatar is-fallback", children: collaborator.name.slice(0, 1) }), src: collaborator.avatar }) : u("span", { "aria-hidden": "true", class: "atv-personage-collaborator-avatar is-fallback", children: collaborator.name.slice(0, 1) }), u("div", { class: "atv-personage-collaborator-info", children: [u("a", { class: "atv-personage-collaborator-name", href: collaborator.href, rel: "noopener", target: "_blank", children: collaborator.name }), u("span", { class: "atv-personage-collaborator-count", children: [collaborator.sharedWorkCount, " 次合作"] })] }), collaborator.sharedWorksHref ? u("a", { class: "atv-personage-collaborator-action", href: collaborator.sharedWorksHref, rel: "noopener", target: "_blank", children: ["查看共同作品", u("span", { class: "atv-personage-collaborator-arrow", "aria-hidden": "true", children: [" ", "↗"] })] }) : null ] }, collaborator.href)) }) }); }; var noop$3 = () => void 0; var PersonageGalleryImageTile = ({ alt, largeSrc, onOpenImage, src, staggerIndex }) => { const [aspectRatio, setAspectRatio] = d(null); return u("li", { style: { "--stagger-index": String(staggerIndex), ...aspectRatio ? { "--atv-personage-gallery-aspect-ratio": String(aspectRatio) } : {} }, children: u("button", { "aria-label": `查看${alt}`, class: "atv-image-preview-trigger", onClick: () => onOpenImage({ alt, previewSrc: src, src: largeSrc }), type: "button", children: u(SafeImage, { alt, loading: "lazy", onLoad: ({ width, height }) => setAspectRatio(width / height), src }) }) }); }; var PersonageGallerySection = ({ gallery, name, onOpenImage = noop$3 }) => { if (!gallery) return null; return u(Section, { id: "atv-personage-gallery", ...gallery.allImagesHref ? { moreLink: { href: gallery.allImagesHref, text: "查看全部 →" } } : {}, title: "图集", children: gallery.images.length > 0 ? u("ul", { "aria-label": `${name}的图片`, class: "atv-carousel atv-personage-gallery-rail", children: gallery.images.map((image, index) => { return u(PersonageGalleryImageTile, { alt: image.alt || `${name}的图片 ${index + 1}`, largeSrc: image.largeSrc, onOpenImage, src: image.src, staggerIndex: index }, image.src); }) }) : u("p", { class: "atv-personage-gallery-empty", children: "暂无公开图片" }) }); }; var splitPersonageName = (name) => { const originalNameStart = name.search(/\p{Script=Latin}/u); if (originalNameStart <= 0) return { originalName: null, primaryName: name }; return { originalName: name.slice(originalNameStart).trim(), primaryName: name.slice(0, originalNameStart).trim() }; }; var biographyPreviewLineCount = 3; var useBiographyDisclosure = (biography) => { const contentRef = A(null); const biographyKey = biography?.join("\n") ?? ""; const previousBiographyKeyRef = A(biographyKey); const [canExpand, setCanExpand] = d(false); const [isExpanded, setIsExpanded] = d(false); _(() => { if (previousBiographyKeyRef.current !== biographyKey) { previousBiographyKeyRef.current = biographyKey; setCanExpand(false); setIsExpanded(false); } }, [biographyKey]); _(() => { const content = contentRef.current; if (!content || !biographyKey) { setCanExpand(false); setIsExpanded(false); return; } if (canExpand) return; const measureOverflow = () => { const lineHeight = Number(window.getComputedStyle(content).lineHeight.replace("px", "")); const fullHeight = content.scrollHeight; const hasHiddenContent = Number.isFinite(lineHeight) && fullHeight > lineHeight * biographyPreviewLineCount + 1; setCanExpand(hasHiddenContent); if (!hasHiddenContent) setIsExpanded(false); }; measureOverflow(); const frame = window.requestAnimationFrame(measureOverflow); const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measureOverflow); observer?.observe(content); return () => { window.cancelAnimationFrame(frame); observer?.disconnect(); }; }, [biographyKey, canExpand]); return { canExpand, contentRef, isExpanded, setIsExpanded }; }; var computeStatsKicker = (profile) => { const totalWorks = profile.recentWorks?.totalCount ?? profile.representativeWorks?.totalCount ?? 0; const totalAwards = profile.awards?.totalCount ?? profile.awards?.awards.length ?? 0; if (totalWorks === 0 && totalAwards === 0) return null; const parts = []; if (totalWorks > 0) parts.push(`${totalWorks} 部作品`); if (totalAwards > 0) parts.push(`${totalAwards} 项荣誉`); return parts.join(" · "); }; var PersonageHeroPortrait = ({ portrait, primaryName, onOpenPortrait }) => { const handleClick = () => { onOpenPortrait(portrait, `${primaryName}的头像`); }; const handleKeyDown = (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); handleClick(); } }; return u("button", { "aria-label": `查看${primaryName}的头像`, class: "atv-personage-portrait-trigger atv-image-preview-trigger", onClick: handleClick, onKeyDown: handleKeyDown, type: "button", children: u(SafeImage, { alt: "", className: "atv-personage-portrait", fallback: u("div", { "aria-hidden": "true", class: "atv-personage-portrait is-empty" }), loading: "eager", src: portrait }) }); }; var PersonageBiography = ({ biography, canExpand, contentRef, isExpanded, setIsExpanded }) => u("div", { class: "atv-personage-biography", children: [u("div", { class: `atv-personage-biography-content ${canExpand && !isExpanded ? "is-clamped" : "is-expanded"}`, ref: contentRef, children: biography.map((paragraph, index) => u("p", { children: paragraph }, `${index}-${paragraph}`)) }), canExpand ? u("button", { "aria-expanded": isExpanded, onClick: () => setIsExpanded((expanded) => !expanded), type: "button", children: isExpanded ? "收起" : "展开" }) : null] }); var PersonageHero = ({ profile, onOpenPortrait }) => { const { canExpand, contentRef, isExpanded, setIsExpanded } = useBiographyDisclosure(profile.biography); const { originalName, primaryName } = splitPersonageName(profile.name); const [isRevealed, setIsRevealed] = d(false); h(() => { const frame = requestAnimationFrame(() => { setIsRevealed(true); }); return () => cancelAnimationFrame(frame); }, []); const statsKicker = computeStatsKicker(profile); return u("section", { class: `atv-personage-hero${isRevealed ? " is-revealed" : ""}`, children: u("div", { class: "atv-personage-hero-inner", children: [profile.portrait ? u(PersonageHeroPortrait, { portrait: profile.portrait, primaryName, onOpenPortrait }) : u("div", { "aria-hidden": "true", class: "atv-personage-portrait is-empty" }), u("div", { class: "atv-personage-identity", children: [ statsKicker ? u("p", { class: "atv-personage-kicker", children: statsKicker }) : null, u("h1", { children: primaryName }), originalName ? u("p", { class: "atv-personage-original-name", children: originalName }) : null, profile.facts.length > 0 ? u("dl", { class: "atv-personage-facts", children: profile.facts.map((fact, index) => u("div", { style: { "--fact-index": String(index) }, children: [u("dt", { children: fact.label }), u("dd", { children: fact.value })] }, fact.label)) }) : null, profile.biography?.length ? u(PersonageBiography, { biography: profile.biography, canExpand, contentRef, isExpanded, setIsExpanded }) : null ] })] }) }); }; var computeIndicatorMetrics = (activeEl, containerEl) => { const containerRect = containerEl.getBoundingClientRect(); const activeRect = activeEl.getBoundingClientRect(); return { left: activeRect.left - containerRect.left, width: activeRect.width }; }; var StickyNav = ({ activeSectionId = "", accessory, className = "", navRef, onJump, scrolling = false, sections, title, visible = false }) => { const markerRef = A(null); _(() => { if (!activeSectionId) return; const nav = navRef?.current; const marker = markerRef.current; if (!nav || !marker) return; const container = nav.querySelector(".atv-stickynav-jumps"); const activeLink = nav.querySelector(`[data-section-id="${activeSectionId}"]`); if (!container || !activeLink) return; const frame = requestAnimationFrame(() => { const { left, width } = computeIndicatorMetrics(activeLink, container); marker.style.transform = `translateX(${left}px)`; marker.style.width = `${width}px`; }); return () => { cancelAnimationFrame(frame); }; }, [activeSectionId, navRef]); return u("nav", { ...navRef ? { ref: navRef } : {}, class: `atv-stickynav${visible ? " is-visible" : ""}${scrolling ? " is-scrolling" : ""}${className ? ` ${className}` : ""}`, children: [ u("div", { class: "atv-stickynav-title", children: title }), accessory, u("div", { class: "atv-stickynav-jumps", children: [sections.map((section) => u("a", { class: activeSectionId === section.id ? "is-active" : void 0, "data-section-id": section.id, href: `#${section.id}`, onClick: (event) => { event.preventDefault(); onJump(section.id); }, children: section.label }, section.id)), u("div", { "aria-hidden": "true", class: "atv-stickynav-marker", ref: markerRef })] }) ] }); }; var PersonageStickyNav = ({ name, ...navigation }) => u(StickyNav, { ...navigation, title: name }); var PosterPlaceholder = () => u("div", { class: "atv-poster-placeholder", children: u(IconFilmPlaceholder, {}) }); var PosterImage = ({ alt, className, poster }) => u(SafeImage, { alt, ...className ? { className } : {}, fallback: u(PosterPlaceholder, {}), loading: "lazy", src: poster }); var groupByYear = (works) => { const map = new Map(); for (const work of works) { const key = work.year ?? "其他"; let group = map.get(key); if (!group) { group = []; map.set(key, group); } group.push(work); } return [...map.entries()].toSorted(([a], [b]) => { if (a === "其他") return 1; if (b === "其他") return -1; return Number(b) - Number(a); }); }; var WorkCard = ({ index, work }) => u("a", { class: "atv-timeline-card", href: work.href, rel: "noopener", style: { "--stagger-index": String(index) }, target: "_blank", children: [u("div", { class: "atv-timeline-card-poster", children: [u(PosterImage, { alt: work.title, poster: work.poster }), work.rating ? u("span", { class: "atv-timeline-card-rating", children: work.rating }) : null] }), u("span", { class: "atv-timeline-card-title", children: work.title })] }); var PersonageTimeline = ({ id, rail, title }) => { if (!rail?.works.length) return null; const groups = groupByYear(rail.works); return u(Section, { id, ...rail.allWorksHref ? { moreLink: { href: rail.allWorksHref, text: "查看全部 →" } } : {}, title, children: u("div", { class: "atv-timeline", style: { "--group-count": String(groups.length) }, children: groups.map(([year, works], gi) => u("div", { class: "atv-timeline-year-group", style: { "--group-index": String(gi) }, children: [u("span", { class: "atv-timeline-year-num", children: year }), u("div", { class: "atv-timeline-row", children: works.map((work, wi) => u(WorkCard, { index: wi, work }, work.href)) })] }, year)) }) }); }; var PersonageWorkRail = ({ id, rail, title }) => { if (!rail?.works.length) return null; return u(Section, { id, ...rail.allWorksHref ? { moreLink: { href: rail.allWorksHref, text: "查看全部 →" } } : {}, title, children: u("div", { class: "atv-personage-work-rail", children: rail.works.map((work, index) => u("a", { class: "atv-personage-work-card", href: work.href, rel: "noopener", target: "_blank", children: [ u("div", { class: "atv-personage-work-poster", children: [ u("span", { class: "atv-work-index", children: index + 1 }), work.rating ? u("span", { class: "atv-work-rating", children: work.rating }) : null, u(PosterImage, { alt: work.title, poster: work.poster }) ] }), u("span", { class: "atv-personage-work-title", children: work.title }), work.year ? u("span", { class: "atv-personage-work-year", children: work.year }) : null ] }, work.href)) }) }); }; var extractPrimaryName = (name) => { const originalNameStart = name.search(/\p{Script=Latin}/u); if (originalNameStart <= 0) return name; return name.slice(0, originalNameStart).trim(); }; var PersonagePage = ({ navigation, profile }) => { const activeImage = useModalRequest(); const primaryName = extractPrimaryName(profile.name); const handleOpenPortrait = (src, alt) => { activeImage.handleOpen({ alt, src }); }; return u(S, { children: [ navigation ? u(PersonageStickyNav, { ...navigation, name: primaryName }) : null, u("main", { class: "atv-personage", children: [ u(PersonageHero, { profile, onOpenPortrait: handleOpenPortrait }), u(PersonageAwardsSection, { awards: profile.awards }), u(PersonageTimeline, { id: "atv-personage-recent-works", rail: profile.recentWorks, title: "近作" }), u(PersonageWorkRail, { id: "atv-personage-representative-works", rail: profile.representativeWorks, title: "作品选" }), u(PersonageCollaborators, { collaborators: profile.collaborators }), u(PersonageGallerySection, { gallery: profile.gallery, name: primaryName, onOpenImage: activeImage.handleOpen }) ] }), activeImage.active ? u(ModalSession, { request: activeImage.active, children: u(PosterModal, { alt: activeImage.active.value.alt, onClose: activeImage.handleClose, ...activeImage.active.value.previewSrc ? { previewSrc: activeImage.active.value.previewSrc } : {}, src: activeImage.active.value.src }) }) : null ] }); }; var usePersonageStickyNavigation = (doc, sections) => { const [activeSectionId, setActiveSectionId] = d(""); const [visible, setVisible] = d(false); const [scrolling, setScrolling] = d(false); const lastVisibleRef = A(false); const navRef = A(null); h(() => { const view = doc.defaultView ?? window; let scrollTimer; const handleScroll = () => { const isVisible = view.scrollY > 300; if (isVisible !== lastVisibleRef.current) { lastVisibleRef.current = isVisible; setVisible(isVisible); } setScrolling(true); view.clearTimeout(scrollTimer); scrollTimer = view.setTimeout(() => setScrolling(false), 150); }; view.addEventListener("scroll", handleScroll, { passive: true }); handleScroll(); return () => { view.removeEventListener("scroll", handleScroll); view.clearTimeout(scrollTimer); }; }, [doc]); h(() => { const nav = navRef.current; if (!nav) return; animateWithReducedMotion(nav, { properties: visible ? { opacity: 1, transform: "translateY(0)" } : { opacity: 0, transform: "translateY(-100%)" }, reducedMotionProperties: { opacity: visible ? 1 : 0 }, springConfig: springConfigs.stickyNav }); }, [visible]); h(() => { const view = doc.defaultView ?? window; const elements = new Map(); for (const section of sections) { const element = doc.querySelector(`#${section.id}`); if (element) elements.set(section.id, element); } let pending = false; const pick = () => { let activeId = ""; let bestScore = -Infinity; for (const section of sections) { const element = elements.get(section.id); if (!element) continue; const rect = element.getBoundingClientRect(); const visibleTop = Math.max(rect.top, 56); const visibleBottom = Math.min(rect.bottom, view.innerHeight * .55); const score = Math.max(0, visibleBottom - visibleTop); if (score > bestScore) { activeId = section.id; bestScore = score; } } setActiveSectionId(activeId); pending = false; }; const observer = new view.IntersectionObserver(() => { if (pending) return; pending = true; view.requestAnimationFrame(pick); }, { threshold: [ 0, .25, .5 ] }); for (const element of elements.values()) observer.observe(element); pick(); return () => observer.disconnect(); }, [doc, sections]); return { activeSectionId, navRef, onJump: q((sectionId) => { const prefersReducedMotion = (doc.defaultView ?? window).matchMedia("(prefers-reduced-motion: reduce)").matches; doc.querySelector(`#${sectionId}`)?.scrollIntoView({ behavior: prefersReducedMotion ? "auto" : "smooth", block: "start" }); }, [doc]), scrolling, sections, visible }; }; var isBiographyExpansionPending = (doc) => [...doc.querySelectorAll(".subject-intro .fold-switch")].some((element) => element.textContent?.includes("展开")); var adoptPersonageProfile = (doc) => { const initialProfile = extractPersonageProfile(doc); if (!initialProfile || !isBiographyExpansionPending(doc)) return initialProfile; [...doc.querySelectorAll(".subject-intro .fold-switch")].find((element) => element.textContent?.includes("展开"))?.click(); return isBiographyExpansionPending(doc) ? null : extractPersonageProfile(doc); }; var adoptPersonageProfileWhenReady = (doc, onAdopted) => { const adopt = () => { const profile = adoptPersonageProfile(doc); if (profile) onAdopted(profile); }; if (doc.readyState === "complete" || !isBiographyExpansionPending(doc)) { adopt(); return; } const view = doc.defaultView; if (!view) { adopt(); return; } view.addEventListener("load", adopt, { once: true }); }; var isDynamicPersonageSourceOrDescendant = (node) => { if (!(node instanceof Element)) return false; return node.matches(".subject-awards, .subject-creations") || node.closest(".subject-awards, .subject-creations") !== null || node.querySelector(".subject-awards, .subject-creations") !== null; }; var hasDynamicPersonageSourceMutation = (mutations) => mutations.some((mutation) => isDynamicPersonageSourceOrDescendant(mutation.target) || [...mutation.addedNodes].some(isDynamicPersonageSourceOrDescendant)); var computePersonageNavSections = (profile) => { const sections = []; if (profile.awards?.awards.length) sections.push({ id: "atv-personage-awards", label: "荣誉" }); if (profile.recentWorks?.works.length) sections.push({ id: "atv-personage-recent-works", label: "近作" }); if (profile.representativeWorks?.works.length) sections.push({ id: "atv-personage-representative-works", label: "作品选" }); if (profile.collaborators?.collaborators.length) sections.push({ id: "atv-personage-collaborators", label: "合作" }); if (profile.gallery?.images.length) sections.push({ id: "atv-personage-gallery", label: "图集" }); return sections; }; var PersonageProfileAdoption = ({ doc, initialProfile }) => { const [profile, setProfile] = d(initialProfile); const sections = T(() => computePersonageNavSections(profile), [profile]); const navigation = usePersonageStickyNavigation(doc, sections); _(() => { const refreshProfile = () => { const nextProfile = extractPersonageProfile(doc); if (nextProfile) setProfile(nextProfile); }; const observer = new MutationObserver((mutations) => { if (hasDynamicPersonageSourceMutation(mutations)) refreshProfile(); }); observer.observe(doc.body, { childList: true, subtree: true }); return () => observer.disconnect(); }, [doc]); return u(PersonagePage, { navigation: sections.length > 0 ? navigation : void 0, profile }); }; var isPersonageHomepage = (location) => location.hostname === "www.douban.com" && /^\/personage\/\d+\/?$/u.test(location.pathname); var mountPersonage = (doc = document) => { if (doc.querySelector("#atv-douban-root")) return; adoptPersonageProfileWhenReady(doc, (profile) => { if (doc.querySelector("#atv-douban-root")) return; if (installEnhancedRoot(doc, (root) => R(u(PersonageProfileAdoption, { doc, initialProfile: profile }), root))) doc.title = `${profile.name} · 豆瓣`; }); }; var personagePage = { matches: isPersonageHomepage, mount: mountPersonage }; var LOGIN_FRAME_STYLE_ID = "atv-login-frame-theme"; var LOGIN_FRAME_CSS = ` :root { color-scheme: dark; --atv-login-bg: #1c1c1e; --atv-login-bg-elevated: rgba(255, 255, 255, 0.06); --atv-login-border: rgba(255, 255, 255, 0.12); --atv-login-border-strong: rgba(65, 190, 93, 0.32); --atv-login-text: #ffffff; --atv-login-text-muted: rgba(255, 255, 255, 0.64); --atv-login-text-faint: rgba(255, 255, 255, 0.42); --atv-login-accent: #41be5d; --atv-login-accent-bright: #4cd97a; --atv-login-danger: #ff6b6b; } html, body { box-sizing: border-box !important; min-width: 0 !important; margin: 0 !important; color: var(--atv-login-text) !important; background: var(--atv-login-bg) !important; } *, *::before, *::after { box-sizing: inherit !important; } body, button, input, select, textarea { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Helvetica Neue", "Microsoft YaHei", Inter, system-ui, sans-serif !important; } a, a:link, a:visited { color: var(--atv-login-accent) !important; text-decoration: none !important; background: transparent !important; } a:hover, a:active { color: var(--atv-login-accent-bright) !important; background: transparent !important; } a:focus-visible, button:focus-visible, input:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--atv-login-accent) !important; outline-offset: 2px !important; } .account-body, .account-main, .account-form, .login-wrap, .login-box { box-sizing: border-box !important; max-width: 100% !important; color: var(--atv-login-text) !important; background: transparent !important; box-shadow: none !important; } .account-body { width: 100% !important; min-width: 0 !important; padding: 0 0 10px !important; } .account-body-tabs, .account-tabcon-start, .account-tabcon-quick, .account-quick, .account-form { width: 100% !important; } .account-body-tabs { margin-bottom: 16px !important; } .account-body-tabs ul, .account-tab, .tab-start, .tab-quick { border-color: var(--atv-login-border) !important; } .account-tab a, .account-tab span, .account-tab-phone, .account-tab-account, .account-tab-scan { color: var(--atv-login-text-muted) !important; background: transparent !important; transition: color 180ms ease, border-color 180ms ease !important; } .account-tab a.on, .account-tab span.on, .account-tab-phone.on, .account-tab-account.on, .account-tab-scan.on, .account-tab .active { color: var(--atv-login-text) !important; border-color: var(--atv-login-accent) !important; } .account-tab-switch, .account-tab-switch-icon, .quick.icon-switch, .start.icon-switch { filter: saturate(0.4) brightness(1.8) !important; } .account-form-tips, .account-form-error, .account-form-err, .account-error, .error, .tips, .tip { line-height: 1.5 !important; } .account-form-tips { color: var(--atv-login-text-muted) !important; } .account-form-error, .account-form-err, .account-error, .error { color: var(--atv-login-danger) !important; } .account-form-field, .account-form-field-phone, .account-form-field-code, .account-form-field-password, .account-form-field-submit { box-sizing: border-box !important; } .account-form-field, .account-form-field-phone, .account-form-field-code, .account-form-field-password, .global-phone-input-input { min-height: 44px !important; color: var(--atv-login-text) !important; background: var(--atv-login-bg-elevated) !important; border-color: var(--atv-login-border) !important; border-radius: 10px !important; } .account-form-field:focus-within, .account-form-field-phone:focus-within, .account-form-field-code:focus-within, .account-form-field-password:focus-within, .global-phone-input-input:focus-within { border-color: var(--atv-login-border-strong) !important; box-shadow: 0 0 0 3px rgba(65, 190, 93, 0.12) !important; } .account-form-field input, .account-form-field-phone input, .account-form-field-code input, .account-form-field-password input, .global-phone-input-input input, input[type="text"], input[type="password"], input[type="tel"], input[type="phone"] { min-height: 42px !important; color: var(--atv-login-text) !important; caret-color: var(--atv-login-accent) !important; background: transparent !important; border-color: transparent !important; box-shadow: none !important; } .account-form-field input::placeholder, .account-form-field-phone input::placeholder, .account-form-field-code input::placeholder, .account-form-field-password input::placeholder, input::placeholder { color: var(--atv-login-text-faint) !important; } .account-form-field-area-code, .account-form-field-label, .account-form-raw, .account-form-tip, .account-form-tips, .account-form-ft, .account-form-3rd, .account-form-3rd-hd, .global-phone-input-area { color: var(--atv-login-text-muted) !important; } .account-form-field-area-code, .global-phone-input-area, .account-form-field-code { border-color: var(--atv-login-border) !important; } .account-form-field-submit .btn, .account-form-field-submit button, .account-form-field-submit input[type="submit"], .btn, .btn-phone, .btn-account, .btn-submit, .btn-account { color: #fff !important; background: var(--atv-login-accent) !important; border-color: var(--atv-login-accent) !important; border-radius: 999px !important; box-shadow: 0 10px 26px rgba(65, 190, 93, 0.24) !important; } .account-form-field-submit .btn:hover, .account-form-field-submit button:hover, .account-form-field-submit input[type="submit"]:hover, .btn:hover, .btn-phone:hover, .btn-account:hover, .btn-submit:hover, .btn-account:hover { color: #fff !important; background: var(--atv-login-accent-bright) !important; border-color: var(--atv-login-accent-bright) !important; } .account-form-3rd, .account-form-ft { border-color: var(--atv-login-border) !important; } .account-form-3rd a, .account-form-3rd .link-3rd, .link-3rd-wx, .link-3rd-wb, .link-3rd-qq { background-color: rgba(255, 255, 255, 0.06) !important; border-color: var(--atv-login-border) !important; filter: grayscale(1) brightness(1.4) !important; } .account-quick, .account-quick .qrcode, .account-quick .qr-code, .account-quick img, .qrcode, .qr-code { color: var(--atv-login-text-muted) !important; background-color: transparent !important; border-color: var(--atv-login-border) !important; } .account-quick img, .qrcode img, .qr-code img { border-radius: 12px !important; } @media (max-width: 380px) { .account-body { padding-right: 0 !important; padding-left: 0 !important; } .account-form-field, .account-form-field-phone, .account-form-field-code, .account-form-field-password, .global-phone-input-input { border-radius: 9px !important; } } `; var isDoubanLoginFrame = (loc = window.location) => loc.hostname === "accounts.douban.com" && loc.pathname.startsWith("/passport/login"); var installLoginFrameTheme = (doc = document) => { if (doc.querySelector(`#${LOGIN_FRAME_STYLE_ID}`)) return; const style = doc.createElement("style"); style.id = LOGIN_FRAME_STYLE_ID; style.textContent = LOGIN_FRAME_CSS; (doc.head ?? doc.documentElement).append(style); }; var extractAwards = (doc) => $$("ul.award", doc).map((ul) => { const lis = $$("li", ul); const orgEl = lis[0] ? $("a", lis[0]) : null; const org = lis[0] ? safeText(lis[0]) : ""; const name = lis[1] ? safeText(lis[1]) : ""; const personEl = lis[2] ? $("a", lis[2]) : null; const person = lis[2] ? safeText(lis[2]) : ""; return { name, org, orgLink: orgEl ? orgEl.href : "", person, personLink: personEl ? personEl.href : "" }; }).filter((a) => a.org); var RE_SLASH_SEP = /\s*\/\s*/gu; var RE_WS_GLOBAL = /\s+/gu; var RE_COLON_WS = /[::\s]/gu; var RE_YEAR_TRAIL = /\(\d{4}\)\s*$/u; var RE_H1_YEAR = /\((?\d{4})\)\s*$/u; var RE_WS = /\s+/u; var RE_YEAR = /(?\d{4})/u; var RE_NON_DIGIT = /\D/gu; var RE_HSPACE = /[ \t]+/gu; var RE_NL_MULTI = /\n{3,}/gu; var RE_IMDB_ID = /(?tt\d+)/u; var RE_BG_URL = /url\(["']?(?[^"')]+)["']?\)/u; var RE_SUBJECT_ID = /subject\/(?\d+)/u; var RE_ALLSTAR = /allstar(?\d{2})/u; var RE_HTTP = /^https?:\/\//u; var RE_ONLINE_VIDEO = /online-video/u; var RE_INTEREST_ACTIVE = /done|active|on\b|j_a\b/u; var RE_IMDB_LINK = /^tt\d+$/u; var RE_SEASON_SUFFIX = /\d$/u; var RE_SEASON_EP = /^第[一二三四五六七八九十百\d]+[季集]\s*/u; var RE_PLAY_SOURCES = /sources\[(?\d+)\]\s*=\s*\[\s*\{play_link:\s*"(?[^"]+)"/gu; var RE_SOURCES_SCRIPT = /\bsources\s*\[/u; var upgradePoster = (url) => { if (!url) return null; return encodeURI(url.replace("/s_ratio_poster/", "/l_ratio_poster/").replace("s_ratio_poster", "l_ratio_poster")); }; var upgradePhoto = (url) => { if (!url) return null; return encodeURI(url.replace("/sqxs/", "/large/").replace("/m/", "/l/")); }; var thumbnailPhoto = (url) => { if (!url) return null; return encodeURI(url.replace("/sqxs/", "/photo/")); }; var extractTitle = (doc) => { const h1 = $("#content h1", doc); const full = safeText($("span[property=\"v:itemreviewed\"]", h1 ?? doc)) || safeText(h1).replace(RE_YEAR_TRAIL, "").trim(); let primary = full; let original = ""; const idx = full.search(RE_WS); if (idx > 0) { primary = full.slice(0, idx).trim(); original = full.slice(idx).trim().replace(RE_SEASON_EP, ""); } return { full, original, primary }; }; var extractYear = (doc) => { return safeText($("#content h1 .year", doc)).match(RE_YEAR)?.[1] ?? ""; }; var extractPoster = (doc) => { const img = $("#mainpic img", doc) || $("a.nbgnbg img", doc); if (!img) return null; return upgradePoster(img.src || img.dataset.src || ""); }; var extractSubjectId = (doc) => { return (doc.defaultView?.location.pathname ?? "").match(RE_SUBJECT_ID)?.[1] ?? ""; }; var MAX_DISCUSSION_TOPICS = 10; var DOUBAN_ORIGIN = "https://www.douban.com/"; var TOPIC_PATH = /^\/(?:group\/topic\/\d+|subject\/\d+\/discussion\/\d+)\/?$/u; var DISCUSSION_COLLECTION_PATH = /^\/subject\/\d+\/discussion\/?$/u; var COLLECTION_PATH = /^\/(?:group\/\d+|subject\/\d+\/discussion)\/?$/u; var START_DISCUSSION_PATH = /^\/(?:group\/\d+|subject\/\d+\/discussion\/create|register)\/?$/u; var AUTHOR_PROFILE_PATH = /^\/people\/[^/]+\/?$/u; var DISCUSSION_TIMESTAMP = /^(?\d{4}-\d{2}-\d{2})\s+(?