

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Inter', sans-serif;
            background: var(--bg);
            color: var(--text);
            padding: 16px;
            min-height: 100vh;
            -webkit-font-smoothing: antialiased;
        }

        .header {
            text-align: center;
            margin-bottom: 24px;
            animation: fadeIn 0.6s ease;
        }

        .header h1 {
            font-size: 28px;
            font-weight: 600;
            color: var(--text);
            letter-spacing: -0.5px;
        }

        .controls-wrapper {
            margin-bottom: 24px;
            padding-top: 12px;
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .sort-container {
            display: flex;
            gap: 3px;
            padding: 0 4px;
            /* anchor for the period pull-down menu */
            position: relative;
        }

        .search-row {
            display: flex;
            align-items: center;
            padding: 0 4px;
        }

        .search-container {
            flex: 1;
            height: 44px;
            position: relative;
            display: flex;
            align-items: center;
        }

        /* Search session close chip (ADR-0011): the chat list's
           .chat-list-search-close twin. Collapsed to a zero-width slot at
           rest; while the field is focused or holds a query (.searching on
           the row) it grows to a round glass chip beside the field with the
           same width/opacity reveal, so the field shrinks to make room and no
           peer control moves. Material comes from .c-tool-btn (index.html) -
           the toolbar chips and this one are one chip family; only geometry
           and the reveal live here. Height tracks THIS field (48, not the
           chat list's 44). Press is the shared grammar: glassPress grows the
           glass, body.no-blur .c-tool-btn:active shrinks the dense chip, so
           no local :active transform. Desktop keeps the field in the toolbar
           strip (_communityApplyLayout) where there is no slot for it. */
        #community-root .search-row .search-container { min-width: 0; }
        #community-root .search-row .search-close-chip {
            width: 0;
            min-width: 0;
            height: 44px;
            margin-left: 0;
            overflow: hidden;
            opacity: 0;
            visibility: hidden;
            pointer-events: none;
            color: var(--text);
            transform: scale(0.86);
            transition:
                width 180ms cubic-bezier(0.4, 0, 0.2, 1),
                min-width 180ms cubic-bezier(0.4, 0, 0.2, 1),
                margin-left 180ms cubic-bezier(0.4, 0, 0.2, 1),
                opacity 130ms cubic-bezier(0.4, 0, 0.2, 1),
                transform 180ms cubic-bezier(0.4, 0, 0.2, 1),
                background 130ms cubic-bezier(0.4, 0, 0.2, 1),
                visibility 180ms;
        }
        #community-root .search-row .search-close-chip svg {
            width: 20px;
            height: 20px;
            stroke: var(--text);
            opacity: 0.55;
        }
        #community-root .search-row.searching .search-close-chip {
            width: 44px;
            min-width: 44px;
            margin-left: 8px;
            opacity: 1;
            visibility: visible;
            pointer-events: auto;
            transform: none;
        }
        #community-root .search-row .search-close-chip:active svg { opacity: 1; }
        html.layout-desktop #community-root .search-row .search-close-chip { display: none; }
        @media (prefers-reduced-motion: reduce) {
            #community-root .search-row .search-close-chip { transition: none; }
        }

        .search-icon {
            position: absolute;
            left: 14px;
            width: 18px;
            height: 18px;
            stroke: var(--text3);
            pointer-events: none;
            z-index: 1;
        }

        .search-input {
            width: 100%;
            height: 44px;
            padding: 0 44px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--chrome-glass-fill-readable);
            backdrop-filter: var(--chrome-glass-filter-readable);
            -webkit-backdrop-filter: var(--chrome-glass-filter-readable);
            box-shadow: var(--chrome-glass-shadow);
            color: var(--text);
            font-size: 15px;
            font-family: inherit;
            transition: background 130ms cubic-bezier(.4,0,.2,1);
            appearance: none;
            -webkit-appearance: none;
        }

        .search-input::-webkit-search-cancel-button { display: none; }

        /* focus never swaps the material (ADR-0011): the field is shell chrome */
        .search-input:focus {
            outline: none;
            background: var(--chrome-glass-fill-readable);
        }

        .search-input::placeholder {
            color: var(--text3);
        }

        .search-clear {
            position: absolute;
            top: 4px;
            right: 4px;
            width: 40px;
            height: 40px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: transparent;
            color: var(--text2);
            font-size: 16px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 130ms cubic-bezier(.4,0,.2,1), color 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
        }

        @media (hover: hover) {
            .search-clear:hover {
                background: var(--glass-bold);
                color: var(--text);
            }
        }

        .search-clear:active {
            background: var(--accent-soft);
            color: var(--accent-text);
            transform: scale(0.94);
        }

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

        .search-progress {
            display: none;
            position: absolute;
            top: 17px;
            right: 17px;
            width: 14px;
            height: 14px;
            border: 2px solid var(--glass-bold);
            border-top-color: var(--accent);
            border-radius: 50%;
            animation: spin 0.6s linear infinite;
            pointer-events: none;
        }

        .search-container.search-pending .search-progress { display: block; }
        .search-container.search-pending.has-query .search-progress { right: 48px; }
        .search-container.search-pending.has-query .search-input { padding-right: 76px; }

        .filter-dropdown {
            position: relative;
        }

        .filter-dropdown-btn {
            width: 48px;
            height: 48px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--glass);
            color: var(--text2);
            cursor: pointer;
            transition: background 130ms cubic-bezier(.4,0,.2,1);
            display: flex;
            align-items: center;
            justify-content: center;
        }

        @media (hover: hover) {
            .filter-dropdown-btn:hover {
                background: var(--glass-bold);
                color: var(--text);
            }
        }

        .filter-dropdown-btn:active {
            background: var(--glass-bold);
        }

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

        .filter-dropdown-btn.active {
            background: var(--accent-soft);
            color: var(--accent-text);
        }

        .filter-icon {
            width: 20px;
            height: 20px;
            stroke: currentColor;
        }

        .filter-dropdown-menu {
            position: static;
            width: 100%;
            min-height: 0;
            background: var(--chrome-glass-fill-media);
            border: none;
            border-radius: var(--r-island, 24px);
            padding: 0;
            z-index: 100;
            box-shadow: var(--chrome-glass-shadow);
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            max-height: 70vh;
            overflow-y: auto;
            overflow-x: hidden;
        }
        .filter-dropdown-menu-inner { padding: 8px; }

        .filter-dropdown-menu::-webkit-scrollbar {
            width: 6px;
        }

        .filter-dropdown-menu::-webkit-scrollbar-track {
            background: transparent;
            border-radius: 3px;
        }

        .filter-dropdown-menu::-webkit-scrollbar-thumb {
            background: var(--glass-bold);
            border-radius: 3px;
        }

        .filter-dropdown-menu::-webkit-scrollbar-thumb:hover {
            background: rgba(255,255,255,0.18);
        }
        @media (hover: hover) {
            .filter-option:hover {
                background: var(--glass);
                color: var(--text);
            }
        }

        .filter-option:active {
            background: var(--glass);
        }

        .filter-option.active {
            background: var(--accent);
            color: #fff;
        }

        .filter-badge {
            position: absolute;
            top: 8px;
            right: 8px;
            width: 18px;
            height: 18px;
            background: var(--accent);
            color: #fff;
            border-radius: 50%;
            font-size: 11px;
            font-weight: 600;
            display: flex;
            align-items: center;
            justify-content: center;
            pointer-events: none;
        }

        /* Filters are M3 filter chips: a wrapping cloud of tonal pills,
           selected = accent-soft fill. The native checkbox is hidden. */
        .filter-section {
            padding: 6px 10px;
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }

        .filter-section-title {
            flex-basis: 100%;
            padding: 6px 6px 0;
            font-size: 13px;
            line-height: 18px;
            font-weight: 600;
            color: var(--text2);
            letter-spacing: 0.4px;
            text-transform: uppercase;
        }

        .filter-divider {
            height: 1px;
            background: var(--sep);
            margin: 8px 12px;
        }

        /* Multi-select token cloud. One tap anywhere on the token toggles it -
           there is no trailing "x", that is the input-chip idiom and it makes
           the chip carry two targets. Selected is the tonal selection state
           (--accent-soft + --accent-text) plus a LEADING check. The check's
           slot is RESERVED on every token and only its opacity animates: a
           chip that grows when you tap it re-wraps the whole cloud and walks
           the rest of the row out from under the finger. */
        .filter-checkbox {
            position: relative;
            display: inline-flex;
            align-items: center;
            padding: 0 14px;
            cursor: pointer;
            transition: background 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
            border-radius: var(--r-pill, 999px);
            background: var(--glass-bold);
            min-height: 36px;
        }
        /* 36px pill -> 44px target. The cloud's 8px gap sets the inset: at -4px
           the boxes touch edge to edge, never overlap. */
        .filter-checkbox::before {
            content: '';
            position: absolute;
            inset: -4px;
            border-radius: inherit;
        }

        /* press goes LIGHTER, the direction every press in the product moves */
        .filter-checkbox:active {
            background: color-mix(in srgb, var(--text) 7%, var(--glass-bold));
            transform: scale(0.94);
        }

        .filter-checkbox input[type="checkbox"],
        .filter-checkbox input[type="radio"] {
            position: absolute;
            opacity: 0;
            width: 0;
            height: 0;
            margin: 0;
            pointer-events: none;
        }

        .filter-label {
            color: var(--text2);
            font-size: 13px;
            font-weight: 500;
            user-select: none;
            white-space: nowrap;
            transition: color 130ms cubic-bezier(.4,0,.2,1);
        }

        /* the weight stays 500: a bolder label is wider, and a token that grows
           when you tap it re-wraps the cloud - fill and color ARE the state */
        .filter-checkbox:has(input:checked) .filter-label {
            color: var(--accent-text);
        }

        /* the panel is glass, so the selected token takes the OPAQUE tonal fill:
           a 20% tint here has nothing under it and renders as a window (one pill
           came out green on its left end and purple on its right, and the label
           dropped to 2.85:1 over a bright card) while its unselected neighbour
           is solid --glass-bold. The cloud is one material or it is not a cloud. */
        .filter-checkbox:has(input:checked) {
            background: var(--accent-soft-solid);
        }

        /* the selected token gets the same press as the neutral one: :has
           outranks :active, so without this rule a selected chip only shrank
           and never lit up under the finger */
        .filter-checkbox:has(input:checked):active {
            background: color-mix(in srgb, var(--text) 7%, var(--accent-soft-solid));
        }

        .filter-checkbox:has(input:focus-visible) {
            outline: 2px solid var(--accent);
            outline-offset: 2px;
        }

        /* M3 connected button group: selected segment fills tonally and
           morphs to a full pill (mirrors .seg-group in style.css) */
        .sort-btn {
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 0 16px;
            /* fixed height so the pill-end radius below can be exactly half of
               it; a 999px pill on the outer corners would otherwise make the
               browser scale ALL radii down to fit the vertical edge, crushing
               the inner 10px to ~0 */
            height: 44px;
            white-space: nowrap;
            border: none;
            border-radius: 10px;
            background: var(--glass);
            color: var(--text2);
            cursor: pointer;
            transition: background 130ms cubic-bezier(.4,0,.2,1),
                        color 130ms cubic-bezier(.4,0,.2,1),
                        border-radius 130ms cubic-bezier(.4,0,.2,1);
            font-size: 14px;
            font-weight: 500;
            letter-spacing: 0.2px;
        }
        .sort-btn:first-child { border-top-left-radius: 22px; border-bottom-left-radius: 22px; }
        .sort-btn:last-child { border-top-right-radius: 22px; border-bottom-right-radius: 22px; }

        @media (hover: hover) {
            .sort-btn:hover {
                background: var(--glass-bold);
                color: var(--text);
            }
        }

        .sort-btn:active {
            background: var(--glass-bold);
        }
        .sort-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

        .sort-btn.active {
            background: var(--accent-soft);
            color: var(--accent-text);
            border-radius: 22px;
            font-weight: 600;
        }

        /* Caret on the active Popular segment: the segment doubles as an iOS
           pull-down (tap the selected segment again to pick the time window).
           Width+opacity animate in with the same timing as the seg morph. */
        .sort-caret {
            width: 0;
            height: 16px;
            margin-left: 0;
            opacity: 0;
            flex: none;
            transition: width 130ms cubic-bezier(0.4, 0, 0.2, 1),
                        margin 130ms cubic-bezier(0.4, 0, 0.2, 1),
                        opacity 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .sort-btn.active .sort-caret {
            width: 16px;
            margin-left: 4px;
            opacity: 1;
        }

        /* Time-period pull-down menu for the Popular sort. iOS glass menu:
           mirrors .chat-dropdown in style.css (glass at 80% mix, full-bleed
           row highlight, hairline separators, leading check glyph). Pops from
           the anchor corner like a UIMenu. */
        /* Keep this pull-down outside a filtered tab ancestor. The .g-tabs
           glass lives on its ::before layer so this menu can sample the cards
           below instead of being cut off by a nested Backdrop Root.
           Verify glass with a real compositor capture (grim): page.screenshot
           can repaint backdrop-filter and hide live compositor regressions. */
        .sort-period-menu {
            position: static;
            min-width: 210px;
            min-height: 0;
            background: var(--chrome-glass-fill-media);
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            border: none;
            border-radius: 16px;
            overflow: hidden;
            z-index: 100;
            box-shadow: var(--chrome-glass-shadow);
        }
        body.no-blur .sort-period-menu {
            background: var(--bg-tint);
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
        }
        .sort-period-item {
            display: flex;
            align-items: center;
            gap: 10px;
            width: 100%;
            padding: 12px 16px;
            border: none;
            background: none;
            font-family: inherit;
            font-size: 15px;
            font-weight: 400;
            line-height: 1.2;
            color: var(--text);
            text-align: left;
            white-space: nowrap;
            cursor: pointer;
            position: relative;
        }
        .sort-period-item::after {
            content: '';
            position: absolute;
            left: 0;
            right: 0;
            bottom: 0;
            height: 1px;
            background: rgba(255,255,255,0.06);
        }
        .sort-period-item:last-child::after { display: none; }
        .sort-period-item:active { background: rgba(255,255,255,0.07); }
        @media (hover: hover) {
            .sort-period-item:hover { background: rgba(255,255,255,0.07); }
        }
        .sort-period-item:focus-visible {
            outline: 2px solid var(--accent);
            outline-offset: -2px;
        }
        .sort-period-check {
            width: 18px;
            height: 18px;
            flex: none;
            visibility: hidden;
        }
        .sort-period-item.active .sort-period-check { visibility: visible; }

        /* Character modal ⋯ chip: the .close-modal recipe shifted left of it.
           Both selectors single-class, so the compound wins regardless of order */
        .close-modal.modal-menu-btn { right: 66px; }
        .close-modal.modal-menu-btn svg { width: 22px; height: 22px; }
        /* Static community menus use the same chatmenu grid endpoints/timing
           as Vue menus. hidden is the closed rest state, so selected
           checkmarks cannot leak out of a permanently mounted wrapper. */
        .menu-reveal[hidden] { display: none; }
        .menu-reveal > .filter-dropdown-menu,
        .menu-reveal > .sort-period-menu,
        .menu-reveal > .char-menu { min-height: 0; }
        .menu-reveal.menu-animating > .filter-dropdown-menu { overflow-y: hidden; }
        .filter-menu-reveal {
            top: calc(100% + 8px);
            right: 0;
            width: min(340px, calc(100vw - 32px));
        }
        .sort-period-reveal {
            top: calc(100% + 6px);
            right: auto;
            left: 4px;
        }

        /* Character modal ⋯ pull-down: same recipe as .sort-period-menu,
           anchored under the chip pair (top right), reuses its item rows */
        .char-menu-reveal {
            top: 64px;
            right: 16px;
            z-index: 30;
        }
        .char-menu {
            position: static;
            min-width: 230px;
            min-height: 0;
            background: var(--chrome-glass-fill-media);
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            border: none;
            border-radius: 16px;
            overflow: hidden;
            z-index: 30;
            box-shadow: var(--chrome-glass-shadow);
        }
        body.no-blur .char-menu {
            background: var(--bg-tint);
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
        }

        /* Character share sheet: community .modal stack (z, Esc/Back, body:has
           pause) + the app's .modal-sheet look from style.css */
        .char-share-modal.active {
            display: flex;
            align-items: flex-end;
            justify-content: center;
            padding: 0 12px calc(20px + var(--safe-bottom, 0px));
        }
        .char-share-desc {
            text-align: center;
            color: var(--text2);
            font-size: 13px;
            line-height: 1.5;
            margin: 0 0 14px;
        }

        .pagination {
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 8px;
            margin: 20px 0;
            flex-wrap: wrap;
        }

        .page-btn {
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--glass);
            color: var(--text2);
            cursor: pointer;
            transition: background 130ms cubic-bezier(.4,0,.2,1);
            min-width: 44px;
            height: 36px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            font-size: 13px;
            font-weight: 500;
        }
        .page-btn svg { width: 16px; height: 16px; }

        @media (hover: hover) {
            .page-btn:hover:not(:disabled) {
                background: var(--glass-bold);
                color: var(--text);
            }
        }

        .page-btn:active:not(:disabled) {
            background: var(--glass-bold);
        }
        .page-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

        .page-btn.active {
            background: var(--accent-soft);
            color: var(--accent-text);
        }

        .page-btn:disabled {
            opacity: 0.3;
            cursor: not-allowed;
            pointer-events: none;
        }

        .page-indicator {
            height: 36px;
            min-width: 72px;
            padding: 0 16px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--glass-bold);
            color: var(--text);
            font-size: 13px;
            font-weight: 600;
            font-family: inherit;
            font-variant-numeric: tabular-nums;
            cursor: pointer;
        }
        .page-indicator:active {
            background: var(--glass-border);
        }

        /* "go to page" mode: numeric field + confirm (pager switches on indicator
           tap). Keeps the .page-indicator pill geometry it morphs from. */
        .page-input {
            width: 84px;
            height: 36px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--glass-bold);
            color: var(--text);
            font-family: inherit;
            font-size: 14px;
            font-weight: 600;
            font-variant-numeric: tabular-nums;
            text-align: center;
            outline: none;
            caret-color: var(--accent);
            -moz-appearance: textfield;
        }
        .page-input::-webkit-outer-spin-button,
        .page-input::-webkit-inner-spin-button {
            -webkit-appearance: none;
            margin: 0;
        }
        .page-of {
            color: var(--text2);
            font-size: 13px;
            font-weight: 600;
            font-variant-numeric: tabular-nums;
        }
        .page-go {
            background: var(--accent-soft);
            color: var(--accent-text);
        }

        .cards-container {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 12px;
            margin: 20px 0;
            /* on the BASE so the soft-loading UNdim animates too: declared only
               inside the state class, the transition vanishes with it and the
               0.52 -> 1 return snaps (the publicrooms list is the reference) */
            transition: opacity 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }

        @media (min-width: 768px) {
            .cards-container {
                grid-template-columns: repeat(3, 1fr);
                gap: 16px;
            }
        }

        @media (min-width: 1024px) {
            .cards-container {
                grid-template-columns: repeat(4, 1fr);
                gap: 20px;
            }
        }

        .card {
            background: var(--glass);
            border-radius: var(--r-card, 18px);
            overflow: hidden;
            cursor: pointer;
            transition: background 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
            border: none;
            display: flex;
            flex-direction: column;
        }

        @media (hover: hover) {
            .card:hover {
                background: var(--glass-bold);
            }
        }

        .card:active {
            transform: scale(0.98);
        }

        .card-image-wrapper {
            position: relative;
            width: 100%;
            aspect-ratio: 1;
            /* no own background: this layer shows ONLY while the image loads
               (cover art and the monogram both fill it), and a forced var(--bg)
               made the pending well a black hole cut into the card's glass -
               transparent leaves the card body tone, same read as .skeleton-image */
            overflow: hidden;
        }

        .card-image {
            width: 100%;
            height: 100%;
            object-fit: cover;
            opacity: 0;
            /* arrival grammar: 250ms decelerate, same as .content-in and .g-tile-img */
            transition: opacity 250ms cubic-bezier(0.05, 0.7, 0.1, 1);
        }

        .card-image.loaded {
            opacity: 1;
        }

        .card-image-mono {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: 800;
            font-size: 64px;
            color: rgba(255, 255, 255, 0.92);
            user-select: none;
        }

        .card-image.loading {
            background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.04) 75%);
            background-size: 200% 100%;
            animation: shimmer 1.5s infinite;
        }

        @keyframes shimmer {
            0% { background-position: 200% 0; }
            100% { background-position: -200% 0; }
        }

        .skeleton-card {
            background: var(--glass);
            border-radius: var(--r-card, 18px);
            overflow: hidden;
            border: none;
            display: flex;
            flex-direction: column;
            pointer-events: none;
        }

        .skeleton-image {
            width: 100%;
            aspect-ratio: 1;
            background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.04) 75%);
            background-size: 200% 100%;
            animation: shimmer 1.5s infinite;
        }

        .skeleton-content {
            padding: 12px;
            flex: 1;
        }

        .skeleton-title {
            height: 18px;
            width: 60%;
            background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.04) 75%);
            background-size: 200% 100%;
            animation: shimmer 1.5s infinite;
            border-radius: 4px;
            margin-bottom: 8px;
        }

        .skeleton-text {
            height: 14px;
            width: 100%;
            background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.04) 75%);
            background-size: 200% 100%;
            animation: shimmer 1.5s infinite;
            border-radius: 4px;
            margin-bottom: 6px;
        }

        .skeleton-text:last-child {
            width: 80%;
            margin-bottom: 0;
        }

        /* Over-art stat pill, shared by catalog/profile cards and the gallery
           like chip (.g-like-chip). Real glass, verbatim recipe; solid no-blur
           fallback. Placement variants are added per host (.card-stat below,
           .g-like-chip in the gallery block). */
        .stat-chip {
            display: flex;
            align-items: center;
            gap: 5px;
            padding: 4px 9px;
            border-radius: var(--r-pill, 999px);
            color: #fff;
            font-size: 12px;
            font-weight: 600;
            font-variant-numeric: tabular-nums;
            pointer-events: none;
            background: var(--chrome-glass-fill-media);
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            box-shadow: var(--chrome-glass-shadow-thin);
        }
        .stat-chip svg { width: 13px; height: 13px; }
        /* no-blur fallback: same solid tonal fill + elevation as the sibling
           .card-author-chip on the same card (rim removed). Both over-art glass
           pills must degrade to the identical M3 opaque surface, not a
           translucent black scrim (that matched neither the author chip above
           nor either design language). */
        body.no-blur .stat-chip {
            background: var(--glass);
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
            box-shadow:
                0 1px 2px rgb(0 0 0 / 25%),
                0 4px 12px rgb(0 0 0 / 12%);
        }

        /* Catalog/profile card placement: messages bottom-left, likes bottom-right */
        .card-stat { position: absolute; bottom: 8px; z-index: 2; }
        .card-stat.at-left { left: 8px; }
        .card-stat.at-right { right: 8px; }

        .stat-icon {
            width: 16px;
            height: 16px;
            stroke: #ffffff;
            filter: drop-shadow(0 1px 2px rgba(0,0,0,0.8));
        }

        /* Clickable author attribution over card art. Real glass (verbatim
           recipe), mirroring the over-photo profile action buttons. Measured
           cost (docs/perf-backdrop-filter.md): ~2x bare-grid compositing on
           weak devices, free on mid+. If live testing shows catalog scroll
           jank on low-end phones, fall back to the v5.31.0 static scrim.
           z3 keeps it under the status chip (.g-status-chip, z5). */
        .card-author-chip {
            position: absolute;
            top: 8px;
            left: 8px;
            z-index: 3;
            display: flex;
            align-items: center;
            gap: 5px;
            max-width: calc(100% - 16px);
            padding: 4px 10px 4px 5px;
            border: 0;
            background: var(--chrome-glass-fill-media);
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            box-shadow: var(--chrome-glass-shadow-thin);
            border-radius: var(--r-pill, 999px);
            color: var(--text);
            font-size: 11px;
            font-weight: 500;
            font-family: inherit;
            cursor: pointer;
            transition: opacity 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }

        /* invisible hit-area expander: 22px pill -> ~44px touch target */
        .card-author-chip::before {
            content: '';
            position: absolute;
            inset: -11px;
            border-radius: inherit;
        }

        @media (hover: hover) {
            .card-author-chip:hover {
                opacity: 0.88;
            }
        }

        .card-author-chip:active {
            opacity: 0.62;
        }

        /* no-blur fallback: solid fill + elevation, rim removed (translucency
           without blur looks like mud; rim on opaque fill reads as cheap M3) */
        body.no-blur .card-author-chip {
            background: var(--glass);
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
            box-shadow:
                0 1px 2px rgb(0 0 0 / 25%),
                0 4px 12px rgb(0 0 0 / 12%);
        }

        .card-author-chip-av {
            width: 14px;
            height: 14px;
            border-radius: 50%;
            object-fit: cover;
            flex-shrink: 0;
            background: rgba(255, 255, 255, 0.1);
        }

        .card-author-avatars {
            display: inline-flex;
            align-items: center;
            flex: 0 0 auto;
        }
        .card-author-avatars .card-author-chip-av + .card-author-chip-av {
            margin-left: -5px;
            box-shadow: 0 0 0 1px var(--glass-bold);
        }
        .card-author-chip-more {
            flex: 0 0 auto;
            color: var(--text2);
            font-variant-numeric: tabular-nums;
        }

        .card-author-chip-name {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .card-content {
            padding: 12px;
            height: 166px;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        .card-name {
            font-size: 15px;
            font-weight: 600;
            margin-bottom: 4px;
            color: var(--text);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            /* never let an overgrown tag block crush the name vertically */
            flex-shrink: 0;
        }

        .card-desc-wrap {
            flex: 1;
            min-height: 0;
            overflow: hidden;
        }

        .card-description {
            font-size: 13px;
            color: var(--text2);
            line-height: 1.4;
            overflow: hidden;
            display: -webkit-box;
            -webkit-box-orient: vertical;
        }

        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.72);
            /* No fullscreen backdrop-filter here on purpose. It is the single
               most expensive blur in the app: a viewport-sized blur that FF/Chromium
               re-run in full on every produced frame while ANY pixel changes
               (scrolling the modal body, a toast, the sheet slide), ~50ms/frame
               each and stacking linearly per open modal (measured: 1 modal + any
               motion = ~12fps, 3 = ~7fps; docs/perf-backdrop-filter.md). Behind a
               0.72 black scrim the blur was barely visible anyway. The opaque
               .modal-content carries the glass; the backdrop is dim-only. */
            z-index: 1000;
            /* Never a scroller: sheets translate below the viewport while
               swipe-dismissed/animated, and an auto overflow here grows a
               desktop scrollbar mid-gesture, shrinking clientWidth and
               reflowing the sheet text. Content scrolls inside
               .modal-content / .modal-scroll. */
            overflow: hidden;
            /* Затемнение едет по background-color, а НЕ по opacity оверлея.
               Предок с анимируемой прозрачностью становится backdrop root, и
               стеклянные дети листа (чип закрытия, чип с многоточием, стрелки
               и точки карусели, меню персонажа) теряют backdrop-filter: они
               рисуются голым прозрачным фоном. То же правило и та же причина,
               что у .modal-enter-active в style.css (владелец, 2026-08-20).
               Chromium восстанавливается сам, WebKit - нет: слой, поднятый под
               анимацию opacity во время загрузки страницы (вход по
               startapp=char_<id>: модалка открывается в том же такте, что и
               первая отрисовка), остаётся неправильным до конца сессии, а
               модалка персонажа - синглтон, поэтому "просвечивает" потом и на
               всех остальных персонажах. */
            animation: modal-scrim-in 0.3s ease;
        }
        /* Один кадр from: конечное значение берётся из самой .modal, поэтому
           затемнение не приходится дублировать двумя числами. */
        @keyframes modal-scrim-in { from { background-color: rgba(0, 0, 0, 0); } }

        .modal-backdrop {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0);
            transition: background 0.3s ease;
          }

        .modal-backdrop.active {
            background: rgba(0, 0, 0, 0.55);
            /* Same whale as .modal: a fullscreen live blur re-filtered per frame
               during the sheet slide-in (frames ARE produced) = a stuttery open.
               Dim-only, matching .modal. */
        }

        /* Kept as an explicit no-op guard: no fullscreen modal blur ships anymore
           (removed for perf, docs/perf-backdrop-filter.md), so no-blur mode has
           nothing extra to strip here, but a future re-add must keep this. */
        body.no-blur .modal,
        body.no-blur .modal-backdrop.active {
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
        }

        /* While a modal covers the grid, every animation frame under it
           (shimmer skeletons, badge glows, card-effect animations) invalidates
           the modal's full-screen backdrop blur and forces a re-filter at 60fps
           even when the user does nothing (visible as fps drops while scrolling
           e.g. an author profile). Pause the covered content, .card included
           (its card-effect animations are a real offender).
           NUANCE: scoped to .modal ONLY, never the filter dropdown. The dropdown
           is small, has no full-screen blur to protect, and the grid stays
           visible beside it, so pausing there froze freshly filtered .card
           entrances at opacity:0 (cards rendered but invisible). Under a full
           modal the grid is hidden, and a paused entrance simply resumes when
           the modal closes (:has stops matching), so pausing .card is safe. */
        body:has(.modal.active) .cards-container *,
        body:has(.modal.active) .pagination * {
            animation-play-state: paused !important;
        }

        .modal.active {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
            padding-top: calc(20px + var(--tg-top, 0px));
        }

      /* Opaque on purpose: the backdrop behind the sheet is already blurred and
         dimmed by .modal, so glass here is visually identical to a solid fill
         but costs a huge re-filtered area on every frame (worst on phones) */
      .modal-content {
          background: var(--bg-tint);
          border-radius: var(--r-island, 24px) var(--r-island, 24px) 0 0;
          max-width: 600px;
          width: 100%;
          max-height: calc(90vh - var(--tg-top, 0px));
          overflow-y: auto;
          overflow-x: hidden;
          overflow-anchor: none;
          scroll-padding-bottom: 140px;
          position: relative;
          border: none;
          transform: translateY(100%);
          transition: transform 0.35s cubic-bezier(0.32, 0.72, 0, 1);
          touch-action: pan-y;
          overscroll-behavior: contain;
          -webkit-overflow-scrolling: touch;
          box-sizing: border-box;
          box-shadow: 0 -1px 2px rgb(0 0 0 / 30%), 0 -8px 12px 6px rgb(0 0 0 / 15%), inset 0 1px 0 rgba(255,255,255,0.08);
      }

      /* Character detail + gallery post modals: flex layout so .modal-scroll
         is the single scroller (the swipe-dismiss engine depends on it) */
      /* Контейнер внутри листа берёт --glass-bold, а не --glass. Лист покрашен
         в --bg-tint, и на нём --glass даёт 1.10:1 - карточки фактически нет.
         --glass-bold даёт 1.24:1, ровно ту же разницу, что у самой Apple в
         тёмной теме (#000000 против #1C1C1E = 1.23:1). Обводку не ставим:
         сгруппированный список у Apple держится ТОЛЬКО заливкой, кант по
         периметру карточки там не встречается ни в одной версии. */
      .modal-body .island { background: var(--glass-bold); }

      #characterModal > .modal-content,
      #postModal > .modal-content {
          overflow: hidden;
          display: flex;
          flex-direction: column;
          /* definite height so the pager's height chain resolves and every post
             is the same sheet height (no resize jump when swiping between posts) */
          height: calc(90vh - var(--tg-top, 0px));
      }

      .modal-scroll {
          flex: 1;
          overflow-y: auto;
          overflow-x: hidden;
          -webkit-overflow-scrolling: touch;
          overscroll-behavior: contain;
      }
        .modal.active .modal-content {
            transform: translateY(0);
        }
        /* Без этого переход выше НЕ ИГРАЕТ ни в одном движке: лист приезжает
           из display:none, у него нет предыдущего вычисленного стиля, и оба
           (замерено в WebKit 26 и Chromium) ставили его сразу на место -
           подъём подменяла общая прозрачность оверлея. @starting-style даёт
           переходу стартовое значение; движок без его поддержки просто
           показывает лист на месте, как и раньше. */
        @starting-style {
            .modal.active .modal-content { transform: translateY(100%); }
        }
        .modal-image {
            width: 100%;
            max-height: 400px;
            object-fit: contain;
            background: var(--bg);
            display: block;
        }

        .modal-image-carousel {
            position: relative;
            width: 100%;
            user-select: none;
            -webkit-user-select: none;
        }
        .modal-image-carousel .carousel-viewport {
            position: relative;
            width: 100%;
            overflow: hidden;
            touch-action: pan-y;
            background: var(--bg);
        }
        .modal-image-carousel .carousel-track {
            display: flex;
            width: 300%;
            transform: translate3d(-33.3333%, 0, 0);
            will-change: transform;
        }
        .modal-image-carousel .carousel-slide {
            flex: 0 0 33.3333%;
            width: 33.3333%;
            display: block;
            aspect-ratio: 1 / 1;
            max-height: 480px;
            object-fit: contain;
            background: var(--bg);
            pointer-events: none;
            -webkit-user-drag: none;
        }
        .modal-image-carousel .modal-image {
            cursor: zoom-in;
        }
        .modal-image-carousel .carousel-arrow {
            display: none;
            position: absolute;
            top: 50%;
            transform: translateY(-50%);
            width: 40px;
            height: 40px;
            border-radius: var(--r-pill, 999px);
            border: none;
            background: var(--chrome-glass-fill-media);
            color: #fff;
            cursor: pointer;
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            box-shadow: var(--chrome-glass-shadow-thin);
            transition: background 0.15s ease;
            z-index: 2;
        }
        body.no-blur .modal-image-carousel .carousel-arrow,
        body.no-blur .modal-image-carousel .carousel-dots {
            background: rgba(0,0,0,0.65);
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
        }
        .modal-image-carousel .carousel-arrow svg { width: 18px; height: 18px; display: block; }
        .modal-image-carousel.has-multiple .carousel-arrow {
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .modal-image-carousel .carousel-arrow:hover {
            background: rgba(0,0,0,0.6);
        }
        .modal-image-carousel .carousel-arrow-prev { left: 16px; }
        .modal-image-carousel .carousel-arrow-next { right: 16px; }
        .modal-image-carousel .carousel-dots {
            display: none;
            position: absolute;
            bottom: 12px;
            left: 50%;
            transform: translateX(-50%);
            gap: 8px;
            padding: 6px 10px;
            background: var(--chrome-glass-fill-media);
            border: none;
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            box-shadow: var(--chrome-glass-shadow-thin);
            border-radius: 999px;
            z-index: 2;
        }
        .modal-image-carousel.has-multiple .carousel-dots {
            display: flex;
            align-items: center;
        }
        .modal-image-carousel .carousel-dot {
            width: 6px;
            height: 6px;
            border-radius: 999px;
            background: rgba(255,255,255,0.35);
        }
        .modal-image-carousel .carousel-dot-pill {
            position: absolute;
            left: 10px;
            top: 50%;
            height: 6px;
            width: 6px;
            border-radius: 999px;
            background: #fff;
            transform: translate3d(0, 0, 0);
            transform-origin: left center;
            margin-top: -3px;
            pointer-events: none;
            will-change: transform, width;
        }
        .char-lightbox {
            position: fixed;
            inset: 0;
            background: rgba(0,0,0,0.92);
            z-index: 5000;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            touch-action: none;
            overflow: hidden;
            user-select: none;
            -webkit-user-select: none;
        }
        .char-lightbox-track {
            position: absolute;
            inset: 0;
            display: flex;
            width: 300%;
            will-change: transform;
            transform: translate3d(-33.3333%, 0, 0);
        }
        .char-lightbox-slide {
            flex: 0 0 33.3333%;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
        }
        .char-lightbox-side {
            max-width: 96%;
            max-height: 90vh;
            object-fit: contain;
            border-radius: 8px;
            user-select: none;
            -webkit-user-select: none;
            /* a photo that pages under a drag is never picked up as a file */
            -webkit-user-drag: none;
            pointer-events: none;
        }
        .char-lightbox-main {
            will-change: transform;
            pointer-events: auto;
        }
        /* chrome = the chat media viewer dialect (.lb-chip/.lb-counter in
           style.css): neutral dark non-theme glass, one look on both viewers.
           Values mirror --lb-glass/--lb-solid, viewer-local, not app tokens. */
        .char-lightbox-btn,
        .char-lightbox-arrow {
            width: 40px;
            height: 40px;
            border-radius: var(--r-pill, 999px);
            background: var(--chrome-glass-fill-neutral);
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            box-shadow: var(--chrome-glass-shadow-thin);
            border: none;
            color: #fff;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            z-index: 10;
            font-family: inherit;
            transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .char-lightbox-btn {
            position: absolute;
            top: calc(12px + max(env(safe-area-inset-top, 0px), var(--tg-top, 0px)));
            right: 12px;
        }
        .char-lightbox-btn svg,
        .char-lightbox-arrow svg {
            width: 18px;
            height: 18px;
            display: block;
        }
        .char-lightbox-btn:active,
        .char-lightbox-arrow:active {
            transform: scale(0.92);
        }
        .char-lightbox-btn:disabled,
        .char-lightbox-arrow:disabled { opacity: 0.32; pointer-events: none; }
        .char-lightbox-btn:focus-visible,
        .char-lightbox-arrow:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        body.no-blur .char-lightbox-btn,
        body.no-blur .char-lightbox-arrow { background: #1f1f21; }
        .char-lightbox-arrow {
            position: absolute;
            top: 50%;
            margin-top: -20px;
        }
        .char-lightbox-arrow-prev { left: 10px; }
        .char-lightbox-arrow-next { right: 10px; }
        /* video post in the viewer: the clip takes the main slide, sized to
           its poster's box (community.js _clbVideoShow); no zoom on a clip */
        .char-lightbox-video { display: none; }
        /* the bottom capsule sits where the chat viewer keeps its toolbar
           (.lb-bottombar > .lb-actions in style.css): bottom centre, ONE pill
           of 52x44 actions with 22px glyphs (sound on a video post, download).
           One viewer dialect for both fullscreens; the top row stays close +
           counter. Shown only while a tool is on (community.js _clbToolsSync). */
        .char-lightbox-tools {
            position: absolute;
            left: 50%;
            bottom: calc(12px + var(--safe-bottom, env(safe-area-inset-bottom, 0px)));
            transform: translateX(-50%);
            display: none;
            align-items: center;
            gap: 2px;
            padding: 4px;
            border-radius: var(--r-pill, 999px);
            background: var(--chrome-glass-fill-neutral);
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            box-shadow: var(--chrome-glass-shadow-thin);
            z-index: 10;
        }
        .char-lightbox-tool {
            width: 52px;
            height: 44px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: transparent;
            color: #fff;
            display: none;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            font-family: inherit;
            transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1), transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .char-lightbox-tool svg { width: 22px; height: 22px; display: block; }
        @media (hover: hover) {
            .char-lightbox-tool:hover { background: rgba(255,255,255,0.08); }
        }
        .char-lightbox-tool:active { transform: scale(0.92); background: rgba(255,255,255,0.12); }
        .char-lightbox-tool:disabled { opacity: 0.32; pointer-events: none; }
        .char-lightbox-tool:focus-visible { outline: 2px solid #fff; outline-offset: -2px; }
        /* one labeled action under the frame (character history photo restore):
           the same neutral viewer material as the chips, a 44px capsule */
        .char-lightbox-action {
            position: absolute;
            left: 50%;
            bottom: calc(24px + env(safe-area-inset-bottom, 0px));
            transform: translateX(-50%);
            min-height: 44px;
            padding: 0 18px;
            border-radius: var(--r-pill, 999px);
            background: var(--chrome-glass-fill-neutral);
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            box-shadow: var(--chrome-glass-shadow-thin);
            border: none;
            color: #fff;
            font-family: inherit;
            font-size: 14px;
            font-weight: 600;
            display: none;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 1px;
            cursor: pointer;
            z-index: 10;
            white-space: nowrap;
            transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .char-lightbox-action:active { transform: translateX(-50%) scale(0.97); }
        .char-lightbox-action[aria-disabled="true"] { opacity: 0.55; cursor: default; }
        .char-lightbox-action[aria-disabled="true"]:active { transform: translateX(-50%); }
        .char-lightbox-action-hint { font-size: 11px; font-weight: 500; opacity: 0.8; }
        .char-lightbox-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        body.no-blur .char-lightbox-action { background: #1f1f21; }
        .char-lightbox.single .char-lightbox-arrow,
        .char-lightbox.single .char-lightbox-counter {
            display: none;
        }
        /* counter rides the top chrome row, centered against the close chip */
        .char-lightbox-counter {
            position: absolute;
            top: calc(20px + max(env(safe-area-inset-top, 0px), var(--tg-top, 0px)));
            left: 50%;
            transform: translateX(-50%);
            height: 24px;
            padding: 0 10px;
            border-radius: 12px;
            background: rgba(0,0,0,0.5);
            color: #fff;
            font-size: 13px;
            font-weight: 600;
            font-variant-numeric: tabular-nums;
            display: flex;
            align-items: center;
            white-space: nowrap;
            z-index: 10;
        }
        /* Догрузка полного кадра: индикатор в углу, потому что сама фотография
           на экране уже есть (затравка), а счётчик занимает верх. */
        .char-lightbox > .media-loader {
            right: 16px;
            bottom: calc(16px + env(safe-area-inset-bottom, 0px));
        }

        .modal-body {
            padding: 24px;
            box-sizing: border-box;
            width: 100%;
        }
        /* 320px phones: 16 (sheet margin) + 24 (this) eats a quarter of the
           screen before the first field starts. Forms feel it first - three
           equal segments of a picker have ~78px each there - so they get the
           narrower inset. */
        @media (max-width: 340px) {
            .form-modal .modal-body { padding: 24px 16px; }
        }
        /* an island inside a modal body is already inset by that body */
        #characterModal .modal-body .island,
        .form-modal .modal-body .island { margin: 0; }
        /* Form islands: the hairline sits on the TOP of every following child,
           because rows here are wrapped by conditionally hidden divs and a
           bottom hairline would survive its own row disappearing. */
        .form-modal .island > * + * { position: relative; }
        .form-modal .island > * + *::before {
            content: '';
            position: absolute;
            left: var(--pad);
            right: 0;
            top: 0;
            height: 1px;
            background: var(--core-separator);
        }

        .character-heading {
            display: flex;
            flex-direction: column;
            gap: 14px;
            margin-bottom: 18px;
            container: character-heading / inline-size;
        }

        .character-title-row {
            display: flex;
            align-items: flex-start;
            justify-content: space-between;
            gap: 12px;
        }

        .character-author:empty {
            display: none;
        }

        .character-author .author-tag { margin: 0; }

        .modal-name {
            font-size: 24px;
            font-weight: 600;
            margin-bottom: 16px;
            color: var(--text);
        }

        #characterModal .modal-name {
            min-width: 0;
            font-size: 28px;
            line-height: 1.12;
            letter-spacing: -0.4px;
            font-weight: 700;
            margin: 0;
        }

        .character-edit-btn {
            position: relative;
            flex: 0 0 auto;
            min-height: 40px;
            padding: 0 14px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--glass-bold);
            color: var(--text2);
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 7px;
            font: inherit;
            font-size: 13px;
            font-weight: 600;
            cursor: pointer;
            transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1), background 130ms cubic-bezier(0.4, 0, 0.2, 1), color 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }

        .character-edit-btn svg {
            width: 17px;
            height: 17px;
            fill: currentColor;
        }

        @media (hover: hover) {
            .character-edit-btn:hover {
                background: var(--glass-border);
                color: var(--text);
            }
        }

        .character-edit-btn:active {
            background: var(--glass-border);
            color: var(--text);
            transform: scale(0.97);
        }
        .character-edit-btn:disabled { opacity: 0.32; pointer-events: none; }
        .character-edit-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

        .modal-stats {
            display: flex;
            flex-wrap: wrap;
            gap: 8px 16px;
            margin-top: 12px;
        }

        .modal-stat-item {
            display: flex;
            align-items: center;
            gap: 8px;
            color: var(--text2);
            font-size: 13px;
            font-weight: 600;
            font-variant-numeric: tabular-nums;
        }

        .modal-stat-item .stat-icon {
            width: 17px;
            height: 17px;
            fill: currentColor;
            filter: none;
        }

        .modal-description {
            font-size: 15px;
            line-height: 1.6;
            color: var(--text2);
            margin-bottom: 20px;
            white-space: pre-wrap;
            word-wrap: break-word;
            overflow-wrap: anywhere;
            max-width: 100%;
            box-sizing: border-box;
        }
        /* ─── Scenarios ────────────────────────────────────────────────
           Unified list-item used in 3 contexts:
           - picker (.scenario-row inside #scenariosList — user-side, clickable)
           - editor (.scenario-row inside form — creator-side, drag + tap to edit)
           - Both share visuals; only handlers differ.
        */
        .scenarios-section {
            margin-top: 4px;
        }
        /* Character-modal instance only (#scenariosSection): a character may or
           may not have scenarios, so instead of a fixed reserved height, the
           reveal animates in (grid-rows 0fr->1fr) once the detail fetch resolves,
           rather than the section snapping straight to full height and shoving
           the prompt/similar/comments sections below it down. */
        #scenariosSection {
            display: grid;
            grid-template-rows: 0fr;
            transition: grid-template-rows 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        #scenariosSection.open {
            grid-template-rows: 1fr;
            margin-bottom: 20px;
        }
        #scenariosSection .scenarios-section-inner {
            min-height: 0;
            overflow: hidden;
        }
        .scenarios-section .form-label {
            margin-bottom: 0;
        }
        .scenarios-form-header {
            display: flex;
            align-items: baseline;
            justify-content: space-between;
            gap: 8px;
            margin-bottom: 8px;
        }
        .scenarios-count {
            font-size: 12px;
            color: var(--text3);
            font-variant-numeric: tabular-nums;
        }
        .scenarios-count.full {
            color: var(--text2);
        }
        .char-authors-head {
            display: flex;
            align-items: baseline;
            justify-content: space-between;
            gap: 8px;
            margin-bottom: 8px;
        }
        .char-authors-head .form-label { margin-bottom: 0; }
        .char-authors-count {
            color: var(--text3);
            font-size: 12px;
            font-variant-numeric: tabular-nums;
        }
        .char-authors-list { overflow: hidden; }
        .char-author-row,
        .char-author-add-row {
            min-height: 56px;
            box-sizing: border-box;
        }
        .char-author-row {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 8px 8px 8px 12px;
        }
        .char-author-row.is-removed .char-author-avatar,
        .char-author-row.is-removed .char-author-copy { opacity: 0.4; }
        .char-author-row .char-author-avatar,
        .char-author-row .char-author-copy {
            transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .char-author-avatar {
            width: 36px;
            height: 36px;
            flex: 0 0 auto;
            border-radius: 50%;
            object-fit: cover;
            background: var(--glass);
        }
        .char-author-copy {
            min-width: 0;
            flex: 1;
            display: flex;
            flex-direction: column;
            gap: 2px;
        }
        .char-author-name,
        .char-author-username {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .char-author-name { color: var(--text); font-size: 14px; font-weight: 550; }
        .char-author-username { color: var(--text3); font-size: 12px; }
        .char-author-username:empty { display: none; }
        .char-author-state-reveal {
            display: grid;
            grid-template-rows: 0fr;
            transition: grid-template-rows 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .char-author-row.has-inline-state .char-author-state-reveal { grid-template-rows: 1fr; }
        .char-author-inline-state {
            min-height: 0;
            overflow: hidden;
            color: var(--text3);
            font-size: 11px;
            line-height: 1.25;
            opacity: 0;
            text-overflow: ellipsis;
            transition: opacity 60ms cubic-bezier(0.4, 0, 0.2, 1);
            white-space: nowrap;
        }
        .char-author-row.has-inline-state .char-author-inline-state {
            opacity: 1;
            transition-delay: 75ms;
        }
        .char-author-action {
            position: relative;
            width: 44px;
            height: 44px;
            flex: 0 0 44px;
            display: grid;
            place-items: center;
            padding: 0;
            border: 0;
            border-radius: 50%;
            background: transparent;
            color: var(--text2);
            cursor: pointer;
            transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1), flex-basis 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .char-author-action svg,
        .char-author-undo-label { grid-area: 1 / 1; }
        .char-author-action svg {
            width: 18px;
            height: 18px;
            opacity: 1;
            transform: scale(1);
            transition: opacity 60ms 60ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .char-author-undo-label {
            opacity: 0;
            transform: scale(0.94);
            transition: opacity 60ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .char-author-action:active { opacity: 0.55; }
        @media (hover: hover) { .char-author-action:hover { color: var(--text); } }
        .char-author-action.is-undo {
            width: 64px;
            flex-basis: 64px;
            border-radius: var(--r-pill);
            color: var(--accent-text);
            font-size: 12px;
            font-weight: 600;
        }
        .char-author-action.is-undo svg {
            opacity: 0;
            transform: scale(0.9);
            transition-delay: 0ms;
        }
        .char-author-action.is-undo .char-author-undo-label {
            opacity: 1;
            transform: scale(1);
            transition-delay: 60ms, 0ms;
        }
        .char-author-add-row {
            width: 100%;
            max-height: 56px;
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 0 14px;
            overflow: hidden;
            border: 0;
            background: transparent;
            color: var(--text);
            font: inherit;
            font-size: 14px;
            text-align: left;
            cursor: pointer;
            opacity: 1;
            transition: min-height 150ms cubic-bezier(0.4, 0, 0.2, 1), max-height 150ms cubic-bezier(0.4, 0, 0.2, 1), opacity 85ms 42ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .char-author-add-row.is-collapsed {
            min-height: 0;
            max-height: 0;
            opacity: 0;
            pointer-events: none;
            transition-duration: 150ms, 150ms, 60ms;
            transition-delay: 0ms;
        }
        .form-modal .island > .char-author-add-row.is-collapsed::before { opacity: 0; }
        .char-author-add-row > svg:first-child { width: 22px; height: 22px; color: var(--accent-text); }
        .char-author-add-row > span { flex: 1; }
        .char-author-add-row > svg:last-child { width: 18px; height: 18px; color: var(--text3); }
        .char-author-add-row:active { opacity: 0.55; }
        .char-author-add-row:focus-visible,
        .char-author-action:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
        .char-authors-load-row {
            width: 100%;
            min-height: 44px;
            display: grid;
            place-items: center;
            padding: 8px 14px;
            border: 0;
            background: transparent;
            color: var(--text3);
            font: inherit;
            font-size: 12px;
            text-align: center;
        }
        button.char-authors-load-row { color: var(--accent-text); cursor: pointer; }

        /* These are viewport sheets, not content cards. The generic .modal rule
           deliberately clears the app nav for character/profile cards; doing it
           here lifted the sheet by 64px and created a dead band below its CTA.
           Also avoid .modal's ancestor-opacity animation: while it runs, child
           backdrop-filter cannot sample the real backdrop and the sheet changes
           colour on the final frame. */
        .character-authors-modal,
        .coauthor-picker-modal,
        .collaboration-invite-modal,
        .action-sheet-modal {
            display: flex;
            align-items: flex-end !important;
            justify-content: center;
            padding: 0 10px calc(10px + var(--safe-bottom, 0px)) !important;
            background-color: rgba(0, 0, 0, 0);
            animation: none;
            visibility: hidden;
            pointer-events: none;
            transition: background-color 300ms ease, visibility 0s linear 350ms;
        }
        .character-authors-modal.active,
        .coauthor-picker-modal.active,
        .collaboration-invite-modal.active,
        .action-sheet-modal.active {
            padding-bottom: calc(10px + var(--safe-bottom, 0px)) !important;
            background-color: rgba(0, 0, 0, 0.72);
            visibility: visible;
            pointer-events: auto;
            transition-delay: 0s;
        }
        .character-authors-modal .modal-sheet,
        .coauthor-picker-modal .modal-sheet,
        .collaboration-invite-modal .modal-sheet,
        .action-sheet-modal .modal-sheet {
            transform: translateY(calc(100% + 24px + var(--safe-bottom, 0px)));
            transition: transform 350ms cubic-bezier(0.32, 0.72, 0, 1);
            outline: none;
        }
        /* At rest the sheet carries NO transform, like the app's Vue sheet once
           its slide animation ends: an identity translateY keeps the sheet on
           its own compositing surface, and at a fractional device pixel ratio
           the 1px row hairlines inside such a surface paint on some frames and
           not on others (owner, Firefox; the glass rim documents the same
           failure). `none` still transitions from the off-screen translate. */
        .character-authors-modal.active .modal-sheet,
        .coauthor-picker-modal.active .modal-sheet,
        .collaboration-invite-modal.active .modal-sheet,
        .action-sheet-modal.active .modal-sheet {
            transform: none;
        }
        @media (prefers-reduced-motion: reduce) {
            .character-authors-modal,
            .coauthor-picker-modal,
            .collaboration-invite-modal,
            .action-sheet-modal,
            .character-authors-modal .modal-sheet,
            .coauthor-picker-modal .modal-sheet,
            .collaboration-invite-modal .modal-sheet,
            .action-sheet-modal .modal-sheet,
            .char-author-row .char-author-avatar,
            .char-author-row .char-author-copy,
            .char-author-state-reveal,
            .char-author-inline-state,
            .char-author-action,
            .char-author-action svg,
            .char-author-undo-label,
            .char-author-add-row { transition: none; }
        }
        .coauthor-picker-modal .modal-sheet { padding-bottom: 16px; }
        .uname-field {
            display: flex;
            align-items: center;
            min-height: 44px;
            margin: 0;
            padding: 0 12px;
            border-radius: var(--r-btn);
            background: var(--glass-bold);
            color: var(--text2);
        }
        .uname-input {
            min-width: 0;
            flex: 1;
            padding: 0;
            border: 0;
            outline: 0;
            background: transparent;
            color: var(--text);
            caret-color: var(--accent);
            font: inherit;
            font-size: 15px;
        }
        /* focus = a lighter tone, never a coloured outline (design-system: text
           and search fields). --glass-border sits BELOW --glass-bold in the
           ladder, so the step is mixed up from the rest fill; the native
           search clear would double ours */
        .uname-field:focus-within { background: color-mix(in srgb, var(--glass-bold) 90%, var(--text) 10%); }
        .uname-input::-webkit-search-cancel-button,
        .uname-input::-webkit-search-decoration { -webkit-appearance: none; display: none; }
        .uname-progress {
            width: 16px;
            height: 16px;
            flex: 0 0 auto;
            border: 2px solid var(--glass-border);
            border-top-color: var(--accent);
            border-radius: 50%;
            animation: spin 0.7s linear infinite;
        }
        .uname-clear {
            width: 44px;
            height: 44px;
            flex: 0 0 44px;
            display: grid;
            place-items: center;
            margin: 0 -10px 0 0;
            padding: 0;
            border: 0;
            background: transparent;
            color: var(--text2);
            cursor: pointer;
        }
        .uname-clear[hidden] { display: none; }
        .uname-clear svg { width: 16px; height: 16px; }
        @media (hover: hover) { .uname-clear:hover { color: var(--text); } }
        .uname-clear:active { opacity: 0.55; }
        .uname-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; border-radius: 50%; }
        .uname-hint {
            min-height: 56px;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 8px 16px;
            color: var(--text3);
            font-size: 13px;
            text-align: center;
        }
        .uname-row {
            width: 100%;
            min-height: 60px;
            display: flex;
            align-items: center;
            gap: 12px;
            padding: 10px 12px;
            border: 0;
            border-radius: var(--r-btn);
            background: var(--glass-bold);
            color: var(--text);
            font: inherit;
            text-align: left;
            cursor: pointer;
        }
        .uname-row:active { opacity: 0.6; }
        .uname-row .char-author-copy { flex: 1; }
        .uname-row > svg { width: 20px; height: 20px; color: var(--accent-text); }

        .collaboration-invite-modal .modal-sheet {
            padding-bottom: 16px;
        }
        .collaboration-invite-stage > .loading {
            min-height: 220px;
            display: grid;
            place-items: center;
        }
        .collaboration-invite-hero {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 10px;
            margin-bottom: 16px;
        }
        .collaboration-invite-cover {
            width: 84px;
            height: 84px;
            border-radius: var(--r-btn);
            object-fit: cover;
            background: var(--glass-bold);
        }
        .collaboration-invite-character {
            max-width: 100%;
            overflow: hidden;
            color: var(--text);
            font-size: 20px;
            font-weight: 700;
            line-height: 1.2;
            text-align: center;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .collaboration-invite-owner {
            width: 100%;
            min-height: 56px;
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 10px 12px;
            border: 0;
            border-radius: var(--r-btn);
            background: var(--glass-bold);
            color: var(--text);
            font: inherit;
            text-align: left;
        }
        button.collaboration-invite-owner { cursor: pointer; }
        button.collaboration-invite-owner {
            transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        button.collaboration-invite-owner:active { background: var(--glass-border); }
        button.collaboration-invite-owner:focus-visible {
            outline: 2px solid var(--accent);
            outline-offset: -2px;
        }
        .collaboration-invite-from {
            overflow: hidden;
            margin: 0 2px 6px;
            color: var(--text3);
            font-size: 12px;
            line-height: 1.2;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .collaboration-invite-explanation {
            margin: 16px 2px;
            color: var(--text2);
            font-size: 14px;
            line-height: 1.45;
        }
        .collaboration-invite-error {
            min-height: 18px;
            color: var(--danger-text, #ff9b9b);
            font-size: 12px;
            line-height: 1.4;
            text-align: center;
        }
        .modal-actions.collaboration-invite-actions {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            margin: 12px 0 0;
        }
        .collaboration-invite-actions > .btn-secondary { margin-top: 0; }
        .collaboration-invite-actions > :only-child { grid-column: 1 / -1; }
        .collaboration-invite-actions .is-loading {
            position: relative;
            color: transparent;
            opacity: 1;
        }
        .collaboration-invite-actions .is-loading::after {
            content: '';
            position: absolute;
            inset: 50% auto auto 50%;
            width: 18px;
            height: 18px;
            margin: -9px 0 0 -9px;
            border: 2px solid rgba(255, 255, 255, 0.35);
            border-top-color: #fff;
            border-radius: 50%;
            animation: actionPendingSpin 700ms linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
            .collaboration-invite-actions .is-loading::after { animation: none; }
        }
        .scenarios-list {
            display: flex;
            flex-direction: column;
            gap: 6px;
        }
        /* Form-mode rows: flat single-line, drag + remove × + click to edit. */
        .scenario-row {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 10px 12px;
            background: var(--glass);
            border: none;
            border-radius: var(--r-btn, 14px);
            transition: background-color 0.15s ease, border-color 0.15s ease;
            -webkit-tap-highlight-color: transparent;
            min-height: 44px;
            box-sizing: border-box;
            position: relative;
            cursor: pointer;
            user-select: none;
            -webkit-user-select: none;
        }
        /* Picker-mode cards: accordion with a head that shows title + tag chips
           + one-line preview, expanding to full description + Start button.
           Animation uses the grid-template-rows 0fr→1fr trick for natural-height
           collapse; kept short (~140ms) so users can expand many cards in a
           row without feeling sluggish. */
        .scenario-pick,
        .fold-pick {
            background: var(--glass);
            border: none;
            border-radius: var(--r-btn, 14px);
            transition: background-color 0.14s ease, border-color 0.14s ease;
            -webkit-tap-highlight-color: transparent;
            overflow: hidden;
            user-select: none;
            -webkit-user-select: none;
        }
        @media (hover: hover) {
            .scenario-pick:hover,
            .fold-pick:hover {
                background: var(--glass-bold);
            }
        }
        .scenario-pick.expanded,
        .fold-pick.expanded {
            background: var(--glass-bold);
        }
        .scenario-pick-head,
        .fold-pick-head {
            display: flex;
            align-items: flex-start;
            gap: 10px;
            padding: 12px 14px;
            min-height: 44px;
            box-sizing: border-box;
            cursor: pointer;
        }
        .scenario-pick-head-text,
        .fold-pick-head-text {
            flex: 1;
            min-width: 0;
            display: flex;
            flex-direction: column;
            gap: 3px;
        }
        .scenario-pick-top {
            display: flex;
            align-items: flex-start;
            gap: 8px;
            min-width: 0;
        }
        /* Same clamp mechanics as the description below: one line + ellipsis
           collapsed, full wrap expanded, height animated in JS. Keeps the
           50-char titles readable on narrow screens without shrinking the
           limit (desktop fits them on one line anyway). */
        .scenario-pick-title,
        .fold-pick-title {
            font-size: 14px;
            font-weight: 500;
            line-height: 1.35;
            color: var(--text);
            overflow: hidden;
            overflow-wrap: anywhere;
            min-width: 0;
            flex: 0 1 auto;
            transition: height 0.14s cubic-bezier(0.4, 0, 0.2, 1);
        }
        .scenario-pick-title.clamped {
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .scenario-pick-tags {
            display: flex;
            gap: 4px;
            flex-shrink: 0;
            /* optical centering against the title's first line (19px box) */
            margin-top: 1px;
        }
        /* Single description element, SAME type size in both states (the old
           12px -> 13px + nowrap -> pre-wrap flip made the text visibly jump on
           expand). Collapsed = .clamped (one line, ellipsis); expand/collapse
           animates the element's height in JS (_animateScenarioDesc), because
           line-clamp itself can't transition. */
        .scenario-pick-desc {
            font-size: 13px;
            color: var(--text2);
            line-height: 1.45;
            overflow: hidden;
            white-space: pre-wrap;
            overflow-wrap: anywhere;
            transition: height 0.14s cubic-bezier(0.4, 0, 0.2, 1);
        }
        .scenario-pick-desc.clamped {
            display: -webkit-box;
            -webkit-box-orient: vertical;
            -webkit-line-clamp: 1;
        }
        .scenario-pick-chevron,
        .fold-pick-chevron {
            flex-shrink: 0;
            margin-top: 2px;
            color: var(--text3);
            transition: transform 0.14s cubic-bezier(0.4, 0, 0.2, 1), color 0.14s ease;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .scenario-pick-chevron svg,
        .fold-pick-chevron svg {
            width: 14px;
            height: 14px;
            display: block;
        }
        .scenario-pick.expanded .scenario-pick-chevron,
        .fold-pick.expanded > .fold-pick-head .fold-pick-chevron {
            transform: rotate(180deg);
            color: var(--text2);
        }
        .scenario-pick-body-wrap,
        .fold-pick-body-wrap {
            display: grid;
            grid-template-rows: 0fr;
            transition: grid-template-rows 0.14s cubic-bezier(0.4, 0, 0.2, 1);
        }
        /* fold-pick side is child-scoped: folds nest (audience inside the
           stats card), a descendant match would open the inner fold too */
        .scenario-pick.expanded .scenario-pick-body-wrap,
        .fold-pick.expanded > .fold-pick-body-wrap {
            grid-template-rows: 1fr;
        }
        .scenario-pick-body,
        .fold-pick-body {
            min-height: 0;
            overflow: hidden;
        }
        .scenario-pick-body-inner,
        .fold-pick-body-inner {
            padding: 0 14px 14px;
            display: flex;
            flex-direction: column;
            gap: 10px;
        }
        .scenario-pick-start {
            align-self: stretch;
            min-height: 44px;
            padding: 0 16px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--accent);
            color: #fff;
            font-family: inherit;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1), transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        /* hover/press = the next tone of the accent, never a brightness filter */
        @media (hover: hover) {
            .scenario-pick-start:hover {
                background: color-mix(in srgb, var(--accent) 88%, white);
            }
        }
        .scenario-pick-start:active {
            transform: scale(0.97);
        }
        .scenario-pick-start:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        /* нейтральный близнец для второго действия в строке сценария
           (групповой чат). НЕ токен-заливка: карточка сценария сама --glass
           (+7% светлоты), а glass-bold всего +11% - на карточке кнопка
           сливалась с фоном. Полупрозрачный белый слой поверх даёт видимый
           тон-степ на любом оттенке темы (идиома btn-secondary:active) */
        .scenario-pick-start.secondary {
            background: rgba(255, 255, 255, 0.10);
            color: var(--text);
        }
        @media (hover: hover) {
            .scenario-pick-start.secondary:hover {
                background: rgba(255, 255, 255, 0.15);
            }
        }
        /* Скрытый промпт сценария: раскрывается только в режиме модерации,
           тем же тональным блоком, что и раскрытый промпт персонажа. */
        .scenario-prompt {
            padding: 12px 14px;
            border-radius: var(--r-btn, 14px);
            background: var(--glass-bold);
        }
        .scenario-prompt-label {
            font-size: 11.5px;
            font-weight: 600;
            color: var(--text3);
            margin-bottom: 6px;
        }
        .scenario-prompt-text,
        .fold-pick-text {
            font-size: 13.5px;
            line-height: 1.55;
            color: var(--text2);
            white-space: pre-wrap;
            overflow-wrap: anywhere;
            user-select: text;
            -webkit-user-select: text;
        }
        .scenario-prompt-text.is-empty {
            color: var(--text3);
            font-style: italic;
        }
        /* Show-more button under the picker list */
        /* standalone text action = pill (sonnet sweep, 2026-08-20) */
        .scenario-pick-more {
            width: 100%;
            min-height: 40px;
            padding: 10px 16px;
            margin-top: 4px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: transparent;
            color: var(--text2);
            font-family: inherit;
            font-size: 13px;
            font-weight: 500;
            cursor: pointer;
            transition: background-color 0.14s ease, color 0.14s ease, border-color 0.14s ease;
        }
        @media (hover: hover) {
            .scenario-pick-more:hover {
                background: var(--glass);
                color: var(--text);
            }
            .scenario-row:hover {
                background: var(--glass-bold);
            }
        }
        .scenario-row.dragging {
            opacity: 0.92;
            z-index: 5;
            box-shadow: 0 12px 28px rgba(0,0,0,0.4);
            transition: none;
        }
        .scenario-row-handle {
            color: var(--text3);
            flex-shrink: 0;
            display: flex;
            align-items: center;
            cursor: grab;
            touch-action: none;
            padding: 2px;
            margin: -2px;
        }
        .scenario-row-handle:active {
            cursor: grabbing;
        }
        .scenario-row-handle svg {
            width: 14px;
            height: 14px;
            opacity: 0.7;
        }
        .scenario-row-body {
            flex: 1;
            min-width: 0;
            display: flex;
            flex-direction: column;
            gap: 2px;
        }
        .scenario-row-title {
            font-size: 14px;
            font-weight: 500;
            color: var(--text);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .scenario-row-desc {
            font-size: 12px;
            color: var(--text2);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            line-height: 1.35;
        }
        .scenario-row-tags {
            display: flex;
            gap: 4px;
            flex-shrink: 0;
        }
        .scenario-row-tag {
            font-size: 10px;
            font-weight: 600;
            color: var(--text2);
            background: var(--glass);
            border: none;
            padding: 2px 7px;
            border-radius: 999px;
            line-height: 1.4;
            letter-spacing: 0.3px;
        }
        .scenario-row-remove {
            position: absolute;
            top: -8px;
            right: -8px;
            width: 22px;
            height: 22px;
            border-radius: var(--r-pill, 999px);
            background: var(--glass-bold);
            color: var(--text);
            border: none;
            font-size: 14px;
            line-height: 1;
            padding: 0;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: inherit;
            z-index: 2;
        }
        .scenario-row-remove svg { opacity: 0.72; transition: opacity 0.15s; }
        /* corner badge: widen the hit area a little without reaching the
           neighbouring row (a full 44px pad would overlap it) */
        .scenario-row-remove::before { content: ''; position: absolute; inset: -6px; border-radius: inherit; }
        .scenario-row-remove:active { transform: scale(0.92); }
        @media (hover: hover) {
            .scenario-row-remove:hover svg { opacity: 1; }
        }
        .scenario-add-row {
            width: 100%;
            padding: 0 16px;
            min-height: 44px;
            margin-top: 8px;
            border-radius: var(--r-pill, 999px);
            border: none;
            background: var(--glass-bold);
            color: var(--text2);
            font-family: inherit;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 6px;
            transition: color 130ms cubic-bezier(0.4, 0, 0.2, 1), background 130ms cubic-bezier(0.4, 0, 0.2, 1), transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        @media (hover: hover) {
            .scenario-add-row:hover:not(:disabled) {
                background: var(--glass-border);
                color: var(--text);
            }
        }
        .scenario-add-row:active:not(:disabled) {
            background: var(--glass-border);
            color: var(--text);
            transform: scale(0.97);
        }
        .scenario-add-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        .scenario-add-row:disabled {
            opacity: 0.32;
            pointer-events: none;
        }
        .scenarios-empty {
            font-size: 12px;
            color: var(--text3);
            padding: 4px 2px 8px;
        }

        /* Character-form + scenario-editor token cloud. The SAME component as
           the catalog cloud above - geometry, type, fill, states, leading
           check - only the class name is feature-local. */
        .scenario-tag-row {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }
        .scenario-tag-chip {
            position: relative;
            display: inline-flex;
            align-items: center;
            min-height: 36px;
            padding: 0 14px;
            border-radius: var(--r-pill, 999px);
            border: none;
            background: var(--glass-bold);
            color: var(--text2);
            font-size: 13px;
            font-weight: 500;
            font-family: inherit;
            white-space: nowrap;
            cursor: pointer;
            transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1), color 130ms cubic-bezier(0.4, 0, 0.2, 1), transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        /* 36px pill -> 44px target, inset set by the cloud's own 8px gap */
        .scenario-tag-chip::before {
            content: '';
            position: absolute;
            inset: -4px;
            border-radius: inherit;
        }
        @media (hover: hover) {
            .scenario-tag-chip:hover {
                background: color-mix(in srgb, var(--text) 7%, var(--glass-bold));
            }
        }
        .scenario-tag-chip:active {
            background: color-mix(in srgb, var(--text) 7%, var(--glass-bold));
            transform: scale(0.94);
        }
        .scenario-tag-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        .scenario-tag-chip.active {
            background: var(--accent-soft);
            color: var(--accent-text);
        }
        /* a selected token answers the press from ITS tone, one step up */
        .scenario-tag-chip.active:active {
            background: color-mix(in srgb, var(--accent-text) 14%, var(--accent-soft));
        }
        @media (hover: hover) {
            .scenario-tag-chip.active:hover {
                background: color-mix(in srgb, var(--accent-text) 14%, var(--accent-soft));
            }
        }

        /* справочник тегов не доехал: строка состояния рядом с кнопкой повтора,
           в той же строке, что и чипы (это ряд flex-элементов) */
        .tag-suggest-note {
            align-self: center;
            font-size: 13px;
            color: var(--text3);
            margin-right: 4px;
        }

        .prompt-section {
            margin-top: 24px;
        }

        /* Fold card: the scenario-pick accordion grammar reused for lone
           disclosures (character prompt, author statistics, gallery post
           prompt). Card, chevron and body share the scenario rules above;
           only the button head and the locked state differ. */
        .fold-pick-head {
            width: 100%;
            margin: 0;
            padding: 12px 14px;
            background: none;
            border: none;
            font-family: inherit;
            color: inherit;
            text-align: left;
            align-items: center;
        }
        .fold-pick-head:focus-visible {
            outline: 2px solid var(--accent-text);
            outline-offset: -2px;
            border-radius: var(--r-btn, 14px);
        }
        .fold-pick-chevron {
            margin-top: 0;
        }
        .fold-pick .post-anon-note {
            margin-left: 0;
            white-space: nowrap;
        }
        .prompt-tokens {
            flex: 0 0 auto;
            margin-left: auto;
            font-size: 12px;
            font-weight: 500;
            color: var(--text3);
            white-space: nowrap;
            font-variant-numeric: tabular-nums;
        }
        /* hidden prompt, non-privileged viewer: the row is a static fact,
           not a control */
        .fold-pick.locked .fold-pick-head {
            cursor: default;
        }
        @media (hover: hover) {
            .fold-pick.locked:hover {
                background: var(--glass);
            }
        }

        .close-modal {
            position: absolute;
            top: 16px;
            right: 16px;
            width: 40px;
            height: 40px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--chrome-glass-fill);
            backdrop-filter: var(--chrome-glass-filter);
            -webkit-backdrop-filter: var(--chrome-glass-filter);
            box-shadow: var(--chrome-glass-shadow-thin);
            color: var(--text);
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1), background 130ms cubic-bezier(0.4, 0, 0.2, 1), scale 120ms ease-out;
            z-index: 10;
        }

        .close-modal svg { width: 20px; height: 20px; display: block; fill: currentColor; }
        .search-clear svg { width: 12px; height: 12px; display: block; }
        .close-modal:hover {
            background: color-mix(in srgb, var(--glass-bold) 72%, transparent);
        }

        body.no-blur .close-modal:active {
            transform: scale(0.92);
        }
        .close-modal:disabled { opacity: 0.32; pointer-events: none; }
        .close-modal:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

        body.no-blur .close-modal {
            background: var(--bg-tint);
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
        }

        .loading {
            text-align: center;
            padding: 60px 20px;
            color: var(--text3);
            grid-column: 1 / -1;
            font-size: 15px;
        }

        .empty-state {
            text-align: center;
            padding: 80px 20px;
            color: var(--text3);
            grid-column: 1 / -1;
            font-size: 15px;
        }

        .empty-state-text {
            font-size: 16px;
            font-weight: 500;
            color: var(--text2);
        }

        .empty-state-icon {
            width: 34px;
            height: 34px;
            margin: 0 auto 14px;
            color: var(--text3);
        }

        .empty-state-icon svg { width: 100%; height: 100%; display: block; }
        .empty-state-desc { margin-top: 7px; color: var(--text3); font-size: 13px; }
        .cards-container.soft-loading { opacity: 0.52; pointer-events: none; }

        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        /* Loading completion: fresh content replaces skeletons with ONE quiet
           fade of the grid container (.content-in, style.css), applied one-shot
           by JS at render time. Cards themselves enter with no animation: a
           per-card animation restarts on every display toggle of the kept-alive
           overlay, and staggered card entrances are a generated-UI marker. */

        @media (prefers-reduced-motion: reduce) {
            .card-image,
            .g-tile .g-tile-img { transition: none; }
        }

        /* Scrollbar */
        ::-webkit-scrollbar {
            width: 8px;
        }

        ::-webkit-scrollbar-track {
            background: transparent;
        }

        ::-webkit-scrollbar-thumb {
            background: var(--glass-bold);
            border-radius: 4px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: rgba(255,255,255,0.18);
        }
        .modal-actions {
            display: flex;
            gap: 8px;
            margin-bottom: 24px;
        }

        .reaction-group {
            min-width: 0;
            flex: 1 1 auto;
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: 3px;
        }

        #characterModal .action-btn,
        #postModal .action-btn {
            min-width: 0;
            min-height: 44px;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            padding: 0 14px;
            border: none;
            border-radius: var(--r-btn, 14px);
            background: var(--glass);
            color: var(--text2);
            cursor: pointer;
            transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1), background 130ms cubic-bezier(0.4, 0, 0.2, 1), color 130ms cubic-bezier(0.4, 0, 0.2, 1);
            font-size: 14px;
            font-weight: 600;
            font-variant-numeric: tabular-nums;
        }

        /* ─── Card reactions: separate tiles ──────────────────────────────
           ONE recipe for the character card and the gallery post (owner,
           2026-08-22: the two screens are siblings and must speak the same
           dialect - the post viewer's old glued 14/6 pill pair is gone).
           Like / dislike / save (report on a post) are SEPARATE tiles, never a
           glued pair with a square stuck to it. Equal widths, a 24px glyph over
           a 13/16 count, on a fill one tone step above the dialog body
           (--bg-tint), so the tiles never dissolve into it. Neutral at rest;
           selection is the tonal state (accent-soft + accent-text + the filled
           glyph). Columns are IMPLICIT: a post of your own drops the report
           tile, and the remaining two must still fill the row instead of
           leaving a third-of-a-row hole. */
        #characterModal .modal-actions,
        #postModal .modal-actions {
            display: grid;
            grid-auto-flow: column;
            grid-auto-columns: minmax(0, 1fr);
            gap: 8px;
            margin: 14px 0 20px;
        }
        /* the two reactions join the grid of their parent, so all tiles are
           exactly equal (a nested group is 4px off) */
        #characterModal .reaction-group,
        #postModal .reaction-group { display: contents; }
        #characterModal .action-btn,
        #postModal .action-btn {
            /* two fixed rows - glyph, then the count - so the save tile, which
               has no count, still puts its glyph on the same line as the other
               two instead of centering itself 9px lower */
            display: grid;
            grid-template-rows: 24px 16px;
            align-content: center;
            justify-items: center;
            gap: 3px;
            min-height: 60px;
            padding: 8px 6px;
            border-radius: var(--r-btn, 14px);
            background: var(--glass-bold);
            color: var(--text);
            font-size: 13px;
            line-height: 16px;
            font-weight: 600;
        }
        /* no count under the bookmark: the glyph centres in ITS tile instead
           of hanging on the shared first row with a hole below (owner,
           2026-08-19) */
        #characterModal .save-action-btn,
        #postModal .save-action-btn { width: auto; padding: 8px 6px; grid-template-rows: none; }
        #characterModal .action-icon,
        #postModal .action-icon { width: 24px; height: 24px; }

        @media (hover: hover) {
            .action-btn:hover {
                background: var(--glass-bold);
                color: var(--text);
            }
            #characterModal .action-btn:hover,
            #postModal .action-btn:hover {
                background: color-mix(in srgb, var(--text) 7%, var(--glass-bold));
                color: var(--text);
            }
        }

        /* content pressable: one tone step + the .97 shrink, no ripple */
        #characterModal .action-btn:active,
        #postModal .action-btn:active {
            transform: scale(0.97);
            background: color-mix(in srgb, var(--text) 7%, var(--glass-bold));
        }
        #characterModal .action-btn:focus-visible,
        #postModal .action-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        #characterModal .action-btn:disabled,
        #postModal .action-btn:disabled { opacity: 0.32; pointer-events: none; }

        #characterModal .action-btn.liked,
        #characterModal .action-btn.favorited,
        #characterModal .action-btn.disliked,
        #postModal .action-btn.liked,
        #postModal .action-btn.favorited,
        #postModal .action-btn.disliked {
            background: var(--accent-soft);
            color: var(--accent-text);
        }
        /* a selected tile answers the press from ITS tone, one step up */
        #characterModal .action-btn:is(.liked, .favorited, .disliked):active,
        #postModal .action-btn:is(.liked, .favorited, .disliked):active {
            background: color-mix(in srgb, var(--accent-text) 14%, var(--accent-soft));
        }
        @media (hover: hover) {
            #characterModal .action-btn:is(.liked, .favorited, .disliked):hover,
            #postModal .action-btn:is(.liked, .favorited, .disliked):hover {
                background: color-mix(in srgb, var(--accent-text) 14%, var(--accent-soft));
                color: var(--accent-text);
            }
        }

        .action-icon {
            width: 20px;
            height: 20px;
            fill: currentColor;
        }

        .action-icon-filled {
            display: none;
        }

        .action-btn.liked .action-icon-outline,
        .action-btn.disliked .action-icon-outline,
        .action-btn.favorited .action-icon-outline {
            display: none;
        }

        .action-btn.liked .action-icon-filled,
        .action-btn.disliked .action-icon-filled,
        .action-btn.favorited .action-icon-filled {
            display: block;
        }

        .reaction-group.busy .action-btn {
            pointer-events: none;
        }

        #characterModal .action-btn.pending {
            position: relative;
        }

        #characterModal .action-btn.pending > * {
            visibility: hidden;
        }

        /* absolutely centered over the hidden content (margin centering, not
           translate: the spin keyframe owns transform) */
        #characterModal .action-btn.pending::after {
            content: '';
            position: absolute;
            left: 50%;
            top: 50%;
            width: 16px;
            height: 16px;
            margin: -8px 0 0 -8px;
            border: 2px solid color-mix(in srgb, currentColor 28%, transparent);
            border-top-color: currentColor;
            border-radius: 50%;
            animation: actionPendingSpin 700ms linear infinite;
        }

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

        @media (prefers-reduced-motion: reduce) {
            #characterModal .action-btn.pending::after { animation: none; }
        }
        /* moderation states use the shared .g-status-chip (see the gallery
           section); the old centered colored banner + full-card blur are gone */
        /* Declined-character notice (creator only): tonal card with status,
           moderator reason and a resubmit action */
        .resubmit-section {
            background: var(--glass, rgba(255,255,255,0.07));
            border-radius: var(--r-card, 18px);
            padding: 16px;
            margin-top: 16px;
        }

        .resubmit-status {
            font-size: 15px;
            font-weight: 600;
            color: var(--text, #fff);
        }

        .resubmit-reason {
            margin-top: 6px;
            font-size: 13px;
            line-height: 1.45;
            color: var(--text2, rgba(255,255,255,0.55));
            overflow-wrap: anywhere;
        }

        .resubmit-section .form-btn {
            display: block;
            width: 100%;
            margin-top: 12px;
        }

        /* destructive secondary: opaque tonal destructive mix, pill like every
           other text button (pressables grammar, design-system §3) */
        .delete-btn {
            width: 100%;
            min-height: 48px;
            padding: 0 20px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: color-mix(in srgb, var(--destructive) 12%, var(--glass));
            color: var(--destructive);
            cursor: pointer;
            font-family: inherit;
            font-size: 15px;
            font-weight: 600;
            line-height: 1.2;
            margin-top: 16px;
            transition: background 130ms cubic-bezier(.4,0,.2,1), color 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
        }

        @media (hover: hover) {
            .delete-btn:hover:not(:disabled) {
                background: color-mix(in srgb, var(--destructive) 18%, var(--glass));
            }
        }

        .delete-btn:active:not(:disabled) {
            background: color-mix(in srgb, var(--destructive) 24%, var(--glass));
            transform: scale(0.97);
        }
        .delete-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

        .delete-btn:disabled {
            opacity: 0.32;
            pointer-events: none;
        }

        /* FAB: 56px solid-accent circle, the island's elevation, no brightness
           filter; the plus is a 24px svg so it centers by geometry */
        /* Owner rule (2026-08-19): on a narrow phone, where the nav row spans
           the screen minus its 16px margins, the FAB sits in the SAME column as
           the avatar chip (centre-aligned above it); on a wider non-desktop
           screen, where the nav is centred with free margins, it stays pinned
           to the screen's right edge. One formula does both: the chip's centre
           is 16px + nav-h/2 from the edge on a narrow screen. */
        .create-fab {
            position: fixed;
            bottom: calc(var(--nav-h, 64px) + var(--safe-bottom, 0px) + 12px + 8px);
            right: calc(16px + (var(--nav-h, 64px) - 56px) / 2);
            width: 56px;
            height: 56px;
            padding: 0;
            border-radius: var(--r-pill, 999px);
            background: var(--accent);
            color: #fff;
            border: none;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 1px 2px rgb(0 0 0 / 30%), 0 4px 8px 3px rgb(0 0 0 / 15%), inset 0 1px 0 rgba(255,255,255,0.15);
            transition: transform 130ms cubic-bezier(.4,0,.2,1), background 130ms cubic-bezier(.4,0,.2,1);
            z-index: 999;
        }
        .create-fab svg { width: 24px; height: 24px; display: block; }

        @media (hover: hover) {
            .create-fab:hover {
                background: color-mix(in srgb, var(--accent) 88%, white);
            }
        }

        .create-fab:active {
            transform: scale(0.92);
        }
        .create-fab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

        .form-modal .modal-content {
            max-height: calc(90vh - var(--tg-top, 0px));
        }

        .form-group {
            margin-bottom: 20px;
        }

        /* grouped-list header, identical to .section-lbl in style.css */
        .form-label {
            display: block;
            color: var(--text2);
            font-size: 13px;
            line-height: 18px;
            font-weight: 600;
            letter-spacing: 0.4px;
            text-transform: uppercase;
            margin-bottom: 8px;
        }

        .form-input,
        .form-textarea,
        .form-select {
            width: 100%;
            padding: 12px 16px;
            border: none;
            border-radius: var(--r-btn, 14px);
            background: var(--glass);
            color: var(--text);
            font-size: 15px;
            font-family: inherit;
            caret-color: var(--accent);
            transition: background-color 0.25s cubic-bezier(.4,0,.2,1);
        }

        .form-input:focus,
        .form-textarea:focus,
        .form-select:focus {
            outline: none;
            background: var(--glass-bold);
        }

        /* closed select looks like our field; the opened list is the OS picker
           (native sheet/wheel on phones, which is fine in a webview) */
        .form-select {
            appearance: none;
            -webkit-appearance: none;
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-opacity='0.45' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
            background-repeat: no-repeat;
            background-position: right 14px center;
            background-size: 16px;
            padding-right: 42px;
        }

        .form-textarea {
            width: 100%;
            padding: 12px;
            border: none;
            border-radius: var(--r-btn, 14px);
            background: var(--glass);
            color: var(--text);
            font-size: 15px;
            font-family: inherit;
            caret-color: var(--accent);
            transition: background-color 0.2s ease;
            resize: none; /* Disable manual resize */
            overflow-y: hidden; /* Hide scrollbar */
            min-height: 50px;
            height: auto;
            field-sizing: content; /* Modern browser support for auto-grow */
        }

        .char-limit {
            font-size: 12px;
            color: var(--text3);
            text-align: right;
            margin-top: 4px;
        }

        /* supporting text under a field: left, like the label it explains. It
           was riding the right edge because the character counter above it
           does - which turns a two-line sentence into a ragged-left block. */
        .char-hint {
            font-size: 11px;
            color: var(--text3);
            opacity: 0.75;
            margin-top: 2px;
            text-align: left;
        }

        .photo-upload {
            border: none;
            border-radius: var(--r-card, 18px);
            padding: 24px;
            text-align: center;
            cursor: pointer;
            background: var(--glass-bold);
            transition: background 130ms cubic-bezier(.4,0,.2,1);
            position: relative;
            overflow: hidden;
        }

        @media (hover: hover) {
            .photo-upload:hover {
                background: var(--glass-border);
            }
        }

        .photo-upload input[type="file"] {
            display: none;
        }

        .photo-preview {
            max-width: 100%;
            max-height: 300px;
            border-radius: var(--r-btn, 14px);
            margin-top: 12px;
        }

        .photo-upload-text {
            color: var(--text2);
            font-size: 14px;
        }

        .photo-upload.has-image {
            padding: 12px;
        }

        .photo-upload-empty {
            cursor: pointer;
            padding: 18px;
            text-align: center;
        }
        .photo-upload-hint {
            font-size: 12px;
            color: var(--text2, rgba(255,255,255,0.55));
            margin-top: 10px;
            text-align: center;
        }
        .photo-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, 80px);
            gap: 10px;
            justify-content: center;
            position: relative;
            /* scroll passes through; reorder engages on hold (reorder.js) */
            touch-action: pan-y;
        }
        .photo-grid .photo-tile {
            position: relative;
            width: 80px;
            height: 80px;
            border-radius: var(--r-btn, 14px);
            background-size: cover;
            background-position: center;
            background-color: rgba(255,255,255,0.04);
            border: none;
            cursor: grab;
            transition: transform 0.18s ease, opacity 0.18s ease, box-shadow 0.18s ease;
            user-select: none;
            -webkit-user-select: none;
        }
        .photo-grid .photo-tile.primary::before {
            content: "1";
            position: absolute;
            bottom: 4px;
            left: 4px;
            background: var(--accent);
            color: #fff;
            font-size: 10px;
            font-weight: 700;
            padding: 2px 6px;
            border-radius: 6px;
            line-height: 1;
        }
        .photo-grid .photo-tile.dragging {
            cursor: grabbing;
            opacity: 0.85;
            box-shadow: 0 12px 28px rgba(0,0,0,0.55);
            z-index: 10;
        }
        .photo-grid .tile-remove {
            position: absolute;
            top: -8px;
            right: -8px;
            width: 22px;
            height: 22px;
            border-radius: var(--r-pill, 999px);
            background: rgba(0,0,0,0.78);
            color: #fff;
            border: none;
            font-size: 14px;
            line-height: 1;
            padding: 0;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: inherit;
            touch-action: manipulation;
            z-index: 2;
        }
        /* corner badge: a modest hit pad, a full 44px one would overlap the
           next tile (10px grid gap) */
        .photo-grid .tile-remove::before { content: ''; position: absolute; inset: -6px; border-radius: inherit; }
        .photo-grid .tile-remove:active { transform: scale(0.92); }
        @media (hover: hover) {
            .photo-grid .tile-remove:hover {
                background: rgba(0,0,0,0.95);
            }
        }
        .photo-grid .photo-tile-add {
            width: 80px;
            height: 80px;
            border-radius: var(--r-btn, 14px);
            border: none;
            background: var(--glass-bold);
            color: var(--text2);
            font-size: 28px;
            line-height: 1;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: inherit;
            transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1), color 130ms cubic-bezier(0.4, 0, 0.2, 1), transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        @media (hover: hover) {
            .photo-grid .photo-tile-add:hover {
                background: var(--glass-border);
                color: var(--text);
            }
        }
        .photo-grid .photo-tile-add:active { background: var(--glass-border); color: var(--text); transform: scale(0.97); }
        .photo-grid .photo-tile-add:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

        .form-buttons {
            display: flex;
            gap: 12px;
            margin-top: 24px;
        }
        /* Character modal start row: the one primary CTA, label never wraps */
        .char-start-row {
            margin: 0 0 10px;
        }
        .char-start-row .form-btn {
            flex: 1 1 0%;
            min-width: auto;
            white-space: nowrap;
            margin: 0;
        }
        /* The second, described way to start (room of people on a community
           card, group chat of characters on a ready-made one): title + who is
           in it, compact tonal button at the trailing edge, the App Store list
           cell. Only the button is a control; the text is passive. */
        .char-start-alt {
            display: flex;
            align-items: center;
            gap: 12px;
            margin: 0 0 20px;
        }
        .char-start-alt-text { flex: 1 1 auto; min-width: 0; }
        .char-start-alt-title {
            font-size: 15px;
            font-weight: 600;
            line-height: 1.3;
            color: var(--text);
        }
        .char-start-alt-line {
            font-size: 13px;
            line-height: 1.4;
            color: var(--text2);
            margin-top: 2px;
        }
        /* the button is the app's compact .btn-sm (style.css: 40h, 14/600,
           tonal); here ::before pads its hit area to 44px without moving it */
        .char-start-alt .btn-sm {
            position: relative;
            flex: 0 0 auto;
            white-space: nowrap;
        }
        .char-start-alt .btn-sm::before { content: ''; position: absolute; inset: -2px 0; border-radius: inherit; }

        /* футер группы публикации, iOS-грамматика (тонированная строка-ссылка
           под островом, About X & Privacy); выравнивание по текстовому инсету
           острова, акцент без подчёркивания (Android settings guidelines) */
        .form-rules-link {
            border: none;
            background: none;
            display: block;
            position: relative;
            margin: 2px 0 0;
            padding: 6px 16px;
            text-align: left;
            font-size: 13px;
            font-family: inherit;
            cursor: pointer;
            color: var(--accent-text);
        }
        /* невидимое расширение тап-зоны до ~44px, как у .toast-act */
        .form-rules-link::before {
            content: '';
            position: absolute;
            inset: -9px 0;
        }
        .form-rules-link:active {
            opacity: 0.7;
        }

        /* text buttons follow the app's .btn-primary/.btn-secondary: pill,
           48px, 15/600; secondary = tonal --glass-bold, primary = the ONE solid
           accent of its screen. Hover is the next tone step, never brightness() */
        .form-btn {
            flex: 1;
            min-width: 0;
            min-height: 48px;
            padding: 0 20px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--glass-bold);
            color: var(--text);
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            font-family: inherit;
            font-size: 15px;
            font-weight: 600;
            line-height: 1.2;
            transition: background 130ms cubic-bezier(.4,0,.2,1), color 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
        }

        .form-btn.primary {
            background: var(--accent);
            color: #fff;
        }

        @media (hover: hover) {
            .form-btn:hover:not(:disabled):not(.primary) {
                background: var(--glass-border);
            }
            .form-btn.primary:hover:not(:disabled) {
                background: color-mix(in srgb, var(--accent) 88%, white);
            }
        }

        .form-btn:active:not(:disabled):not(.primary) {
            background: var(--glass-border);
            transform: scale(0.97);
        }

        .form-btn.primary:active:not(:disabled) {
            transform: scale(0.97);
        }

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

        .form-btn:disabled {
            opacity: 0.32;
            pointer-events: none;
        }

        /* pending keeps the control readable: the fill stays, the label goes
           transparent (bare text or span alike; !important beats the
           #community-root color shims) and a spinner sits centered (margin
           centering, the spin keyframe owns transform) */
        .form-btn.pending,
        .delete-btn.pending {
            position: relative;
            opacity: 1;
            pointer-events: none;
            color: transparent !important;
        }
        .form-btn.pending::after,
        .delete-btn.pending::after {
            content: '';
            position: absolute;
            left: 50%;
            top: 50%;
            width: 18px;
            height: 18px;
            margin: -9px 0 0 -9px;
            border: 2px solid color-mix(in srgb, var(--text) 35%, transparent);
            border-top-color: var(--text);
            border-radius: 50%;
            animation: actionPendingSpin 700ms linear infinite;
        }
        .form-btn.primary.pending::after {
            border-color: rgb(255 255 255 / 35%);
            border-top-color: #fff;
        }
        .delete-btn.pending::after {
            border-color: color-mix(in srgb, var(--destructive) 35%, transparent);
            border-top-color: var(--destructive);
        }
        @media (prefers-reduced-motion: reduce) {
            .form-btn.pending::after,
            .delete-btn.pending::after { animation: none; }
        }

        /* compact twin (40h, 14/600) for small inline actions */
        .form-btn.compact {
            min-height: 40px;
            padding: 0 16px;
            font-size: 14px;
        }

        .edit-btn {
            width: 100%;
            min-height: 48px;
            padding: 0 20px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--glass);
            color: var(--text2);
            cursor: pointer;
            font-family: inherit;
            font-size: 15px;
            font-weight: 600;
            margin-top: 12px;
            transition: background 130ms cubic-bezier(.4,0,.2,1), color 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
        }

        @media (hover: hover) {
            .edit-btn:hover {
                background: var(--glass-bold);
                color: var(--text);
            }
        }

        .edit-btn:active {
            background: var(--glass-bold);
            color: var(--text);
            transform: scale(0.97);
        }
        .edit-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        .edit-btn:disabled { opacity: 0.32; pointer-events: none; }

        .banned-screen {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: var(--bg);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 9999;
            padding: 24px;
            text-align: center;
        }

        .banned-content {
            max-width: 400px;
        }

        .banned-icon {
            font-size: 64px;
            margin-bottom: 16px;
        }
        .card.has-effect {
        }
        .profile-loader {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: var(--bg);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            z-index: 20;
        }
        .spinner {
            width: 40px;
            height: 40px;
            border: 4px solid var(--glass-bold);
            border-top: 4px solid var(--accent-text);
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-bottom: 16px;
        }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        .profile-corner {
            position: absolute;
            right: 0;
            width: 40px;
            height: 40px;
            border-radius: var(--r-pill, 999px);
            background: var(--glass);
            cursor: pointer;
            overflow: hidden;
            border: none;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 130ms cubic-bezier(.4,0,.2,1), color 130ms cubic-bezier(.4,0,.2,1), border-color 130ms cubic-bezier(.4,0,.2,1);
        }
        @media (hover: hover) {
            .profile-corner:hover { background: var(--glass-bold); }
        }
        .profile-corner:active { background: var(--glass-bold); }
        .profile-corner svg { width: 20px; height: 20px; stroke: var(--text2); }
        .profile-header-bg {
            background: var(--bg-tint);
            padding: 32px 20px;
            border-radius: var(--r-island, 24px) var(--r-island, 24px) 0 0;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .profile-avatar-large {
            width: 100px;
            height: 100px;
            border-radius: 50%;
            object-fit: cover;
            border: 3px solid var(--bg);
            box-shadow: 0 0 0 2px var(--glass-border);
            margin-bottom: 16px;
            background: var(--glass);
        }

        .profile-name-large {
            font-size: 24px;
            font-weight: 700;
            color: #fff;
            margin-bottom: 8px;
        }

        .profile-bio {
            color: var(--text2);
            font-size: 14px;
            line-height: 1.5;
            text-align: center;
            max-width: 80%;
            margin-bottom: 20px;
        }

        .profile-stats-row {
            display: flex;
            gap: 24px;
            margin-bottom: 20px;
        }

        .profile-stat {
            text-align: center;
        }

        .profile-stat-value {
            font-size: 18px;
            font-weight: 700;
            color: #fff;
            display: block;
        }

        .profile-stat-label {
            font-size: 12px;
            color: var(--text3);
            margin-top: 4px;
            display: block;
        }

        .profile-avatar-wrapper {
            width: 100px;
            height: 100px;
            margin: 0 auto 16px;
            position: relative;
        }
        .profile-avatar {
            width: 100%;
            height: 100%;
            border-radius: 50%;
            object-fit: cover;
            border: none;
        }
        .edit-avatar-btn {
            position: absolute;
            bottom: 0;
            right: 0;
            background: var(--accent);
            color: #fff;
            border-radius: var(--r-pill, 999px);
            width: 30px;
            height: 30px;
            border: none;
            cursor: pointer;
            box-shadow: 0 1px 2px rgb(0 0 0 / 30%), 0 1px 3px 1px rgb(0 0 0 / 15%);
        }
        #profileEditMode {
            display: none;
            width: 100%;
            max-width: 320px; /* Limit width */
            margin: 16px auto 0; /* Center horizontally */
            animation: fadeIn 0.3s ease;
            flex-direction: column;
            align-items: center;
        }

        #profileEditMode .form-input,
        #profileEditMode .form-textarea {
            text-align: center; /* Center text inside input */
            margin-bottom: 4px; /* Space for counter */
        }

        /* Character Counters for Profile */
        .profile-char-limit {
            font-size: 11px;
            color: var(--text3);
            text-align: right;
            width: 100%;
            margin-bottom: 12px;
            padding-right: 4px;
        }
        /* (.profile-name is the APP profile's class, styled in style.css; the old
           community rule here leaked an extra 8px under the name) */
        .profile-bio { color: var(--text2); font-size: 14px; line-height: 1.4; max-width: 300px; margin: 0 auto 16px; }
        .profile-stats { display: flex; justify-content: center; gap: 12px; margin-top: 12px; }
        .stat-badge { background: var(--glass); padding: 6px 14px; border-radius: var(--r-pill, 999px); font-size: 13px; color: var(--text2); border: none; }

        .author-tag {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            min-height: 40px;
            padding: 6px 12px;
            position: relative;
            background: var(--glass);
            border: 0;
            border-radius: var(--r-pill, 999px);
            margin: 0;
            color: var(--text2);
            font: inherit;
            appearance: none;
            cursor: pointer;
            transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1), transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
            max-width: 100%;
            min-width: 0;
        }
        .author-tag::after {
            content: '';
            position: absolute;
            inset: -2px 0;
        }
        @media (hover: hover) {
            .author-tag:hover { background: var(--glass-bold); }
        }
        .author-tag:active { transform: scale(0.97); }
        .author-tag:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        .author-avatar-small { width: 20px; height: 20px; border-radius: 50%; object-fit: cover; }
        .author-name-text {
            min-width: 0;
            overflow: hidden;
            color: var(--text2);
            font-size: 13px;
            font-weight: 500;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .character-author {
            display: flex;
            align-items: center;
            justify-content: flex-start;
            flex-wrap: wrap;
            gap: 8px 6px;
            margin-bottom: 0;
        }
        .character-author .author-tag { margin: 0; }
        .character-author:empty { display: none; }
        .character-authors-prefix {
            color: var(--text3);
            font-size: 12px;
        }
        .character-authors-more {
            min-width: 38px;
            justify-content: center;
            color: var(--text2);
            font-size: 13px;
            font-variant-numeric: tabular-nums;
            font-weight: 600;
        }
        .character-authors-more-narrow { display: none; }
        .character-authors-more.is-narrow-only { display: none; }
        .character-authors-sheet-list {
            display: flex;
            flex-direction: column;
            gap: 0;
            padding: 0;
            overflow: hidden;
            border-radius: var(--r-card);
            background: var(--glass-bold);
        }
        .character-author-sheet-row {
            position: relative;
            width: 100%;
            min-height: 60px;
            display: flex;
            align-items: center;
            gap: 12px;
            padding: 10px 12px;
            border: 0;
            background: transparent;
            color: var(--text);
            font: inherit;
            text-align: left;
            cursor: pointer;
        }
        .character-author-sheet-row + .character-author-sheet-row::before {
            content: '';
            position: absolute;
            top: 0;
            right: 0;
            left: 60px;
            height: 1px;
            background: var(--core-separator);
        }
        .character-author-sheet-row:active { background: var(--glass); }
        .character-author-sheet-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
        .character-author-sheet-row .char-author-copy { flex: 1; }

        @container character-heading (max-width: 382px) {
            .character-edit-btn {
                width: 40px;
                padding: 0;
            }

            .character-edit-btn span { display: none; }
            .character-edit-btn svg { width: 20px; height: 20px; }
            .character-edit-btn::after {
                content: '';
                position: absolute;
                inset: -2px;
            }
            .character-edit-btn:active { transform: scale(0.92); }
        }

        @container character-heading (max-width: 300px) {
            .character-author-secondary { display: none; }
            .character-authors-more-wide { display: none; }
            .character-authors-more-narrow { display: inline; }
            .character-authors-more.is-narrow-only { display: inline-flex; }
        }
        .character-tag {
            background: var(--glass);
            color: var(--text2);
            padding: 6px 12px;
            border-radius: var(--r-pill, 999px);
            font-size: 13px;
            font-weight: 500;
        }

        .comments-section {
            margin-top: 32px;
            max-width: 100%;
            box-sizing: border-box;
            scroll-margin-block-start: 72px;
        }
        .comments-head {
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            gap: 14px;
            margin-bottom: 16px;
        }
        .comments-head .section-title {
            margin-bottom: 0;
        }
        /* M3 Expressive connected segmented group, hug-width: the segments
           share 3px gaps over the page bg and the selected one morphs into a
           full accent-soft pill. Mirrors .seg-group/.seg-btn in style.css at a
           compact height; outer corners use exactly half the height so the
           pill end stays a clean half-circle without crushing the inner radius. */
        .comment-sort {
            display: inline-flex;
            align-items: stretch;
            gap: 3px;
            max-width: 100%;
        }
        .comment-sort-btn {
            height: 38px;
            min-width: 0;
            padding: 0 16px;
            border: none;
            border-radius: 10px;
            background: var(--glass);
            color: var(--text2);
            font-family: inherit;
            font-size: 13px;
            font-weight: 500;
            line-height: 1;
            white-space: nowrap;
            cursor: pointer;
            flex: 0 0 auto;
            transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1), color 130ms cubic-bezier(0.4, 0, 0.2, 1), border-radius 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .comment-sort-btn:first-child {
            border-top-left-radius: 19px;
            border-bottom-left-radius: 19px;
        }
        .comment-sort-btn:last-child {
            border-top-right-radius: 19px;
            border-bottom-right-radius: 19px;
        }
        .comment-sort-btn:hover {
            background: var(--glass-bold);
            color: var(--text);
        }
        .comment-sort-btn:active:not(.active) {
            background: var(--glass-bold);
        }
        .comment-sort-btn:focus-visible {
            outline: 2px solid var(--accent);
            outline-offset: 2px;
        }
        .comment-sort-btn.active {
            background: var(--accent-soft);
            color: var(--accent-text);
            border-radius: 19px;
            font-weight: 600;
        }
        /* grouped-list header, identical to .section-lbl in style.css */
        .section-title {
            font-size: 13px;
            line-height: 18px;
            font-weight: 600;
            letter-spacing: 0.4px;
            text-transform: uppercase;
            margin-bottom: 10px;
            color: var(--text2);
        }
        /* A character may have no similar matches, so this animates in
           (grid-rows 0fr->1fr) rather than reserving fixed space or snapping
           straight to full height and shoving the comments section below it. */
        .similar-section {
            display: grid;
            grid-template-rows: 0fr;
            transition: grid-template-rows 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .similar-section.open {
            grid-template-rows: 1fr;
            margin-top: 28px;
        }
        .similar-section-inner { min-height: 0; overflow: hidden; }
        .similar-list {
            display: flex;
            gap: 10px;
            overflow-x: auto;
            padding-bottom: 6px;
            -webkit-overflow-scrolling: touch;
        }
        /* Не карточка. Заливка --glass на листе --bg-tint даёт контраст 1.10:1 -
           это призрак рамки, а не группа, и вдобавок она врёт: обещает, что
           внутри вся модерация, тогда как решения стоят кнопками под ней.
           Секция берёт отступ у .modal-body (24px) и выравнивается с остальным
           содержимым модалки. */
        .admin-actions-section { margin-top: 24px; }
        /* заголовок группы уже стоит в отступе секции: свой горизонтальный
           паддинг увёл бы его вправо от рядов (был 24px против 16px) */
        .admin-actions-section .section-lbl { padding: 0 0 8px; }
        /* Ряды группы модерации. Секция уже залита --glass, поэтому всё
           внутри неё - ПЛОСКИЕ ряды с волосяной линией, а не залитые блоки
           (owner, 2026-08-19: "нет вложенных блоков, раз переносим в iOS").
           Заливка вложенного блока совпадала с заливкой секции, и он просто
           исчезал: так пропало поле причины отказа, кнопка вердикта и ряд
           возраста. Одно правило на всю группу, чтобы не повторилось. */
        .admin-row {
            position: relative;
            display: flex;
            align-items: center;
            gap: 12px;
            width: 100%;
            min-height: 44px;
            padding: 12px 0;
            margin: 0;
            background: transparent;
            border: none;
            border-radius: 0;
            font-family: inherit;
            font-size: 16px;
            font-weight: 400;
            color: var(--text);
            text-align: left;
        }
        /* Линия РАЗДЕЛЯЕТ ряды, а не подчёркивает каждый: сверху у всех,
           кроме первого, поэтому под последним рядом её нет. */
        .admin-row + .admin-row::before {
            content: '';
            position: absolute;
            left: 0;
            right: 0;
            top: 0;
            height: 1px;
            background: var(--core-separator);
        }
        .admin-row-label { flex: 1; min-width: 0; }
        /* Кегль равен подписи, разводит их ЦВЕТ - правило ряда со значением
           (style.css .settings-val). Выравнивание вправо нужно ровно для
           переноса: короткое значение и так прижато флексом, а длинное
           (несколько заявленных категорий) обязано лечь по правому краю,
           иначе вторая строка висит посреди ряда. */
        .admin-row-val { color: var(--text2); text-align: right; }
        /* Вторичная кнопка. Заливка --glass-bold на листе это 1.24:1, то есть
           форму держит НЕ она, а кант: порог видимости границы элемента 3:1. */
        .admin-verdict-btn {
            width: 100%;
            margin-top: 20px;
            background: var(--glass-bold);
            color: var(--text);
        }
        @media (hover: hover) {
            .admin-verdict-btn:hover:not(:disabled) {
                background: color-mix(in srgb, var(--text) 6%, var(--glass-bold));
            }
        }
        /* Кнопки вердикта отбиты от последнего ряда данных - тот же отступ в
           обеих очередях штата. У поста ряд лежит прямо в секции, поэтому
           селектор берёт только прямого ребёнка: ряду подтверждения внутри
           .admin-decline-area отступ ставит своё правило. */
        #modalAdminPrimaryRow, #postModPanel > .admin-actions-row { margin-top: 20px; }
        /* The decline area is a grouped form INSIDE the moderation island: the
           reason field and the ban switch are flat rows with hairlines, never
           a filled box inside the filled section (owner, 2026-08-19: "нет
           вложенных блоков, раз переносим в iOS"). The field's own --glass fill
           equals the section fill anyway, which is what left the reason box
           invisible while the switch card floated on its own. */
        /* Поле ввода остаётся ПОЛЕМ, как везде в приложении: заливка --glass и
           радиус --r-btn. Плоским его сделали, когда панель была залита --glass
           и заливка поля совпадала с секцией; секции больше нет, причина ушла.
           Кант тут по той же арифметике, что у кнопки вердикта и блока
           результата: --glass на листе это 1.10:1, форму держит не заливка. */
        /* block, а не inline-block по умолчанию: у строчного поля под
           коробкой остаётся хвост строки (~7px), и заданный отступ до
           переключателя перестаёт быть заданным. */
        .admin-decline-area .form-textarea { display: block; background: var(--glass-bold); }
        .admin-decline-area .form-textarea:focus {
            background: color-mix(in srgb, var(--text) 6%, var(--glass-bold));
        }
        /* Остров ряда переключателя. Кант по той же арифметике, что у поля и
           кнопок этой панели: --glass на листе --bg-tint это 1.10:1, то есть
           заливка сама по себе коробку не рисует. .switch-row внутри острова
           обесцвечивается и идёт в край сам, свои правила ему не нужны. */
        /* Селектор не слабее общего гасителя полей (#characterModal
           .modal-body .island { margin: 0 }), иначе отступ до поля
           причины пропадает и переключатель липнет к нему. */
        #characterModal .modal-body .admin-decline-area .island { margin: 12px 0 0; }
        .admin-decline-area .admin-actions-row { margin-top: 20px; }
        .admin-verdict {
            margin-top: 12px;
            padding: 12px 14px;
            border-radius: var(--r-btn, 14px);
            background: var(--glass-bold);
            color: var(--text2);
            font-size: 13px;
            line-height: 1.5;
            white-space: pre-wrap;
        }
        .admin-actions-row { display: flex; gap: 10px; }
        /* Точка невозврата: отклонение и, возможно, бан. Сейчас акцентная
           кнопка скрыта, поэтому сплошной деструктив здесь единственный CTA. */
        .admin-btn-decline-confirm {
            background: var(--destructive);
            color: #fff;
        }
        @media (hover: hover) {
            .admin-btn-decline-confirm:hover:not(:disabled) {
                background: color-mix(in srgb, var(--destructive) 88%, #fff);
            }
        }
        .comment-thread {
            padding: 2px 0 18px;
        }
        .comment-thread + .comment-thread {
            border-top: 1px solid var(--sep);
            padding-top: 18px;
        }
        .comment-item {
            display: flex;
            gap: 10px;
            min-width: 0;
            border-radius: var(--r-btn, 14px);
            position: relative;
            scroll-margin-block: 96px;
        }
        .comment-item.highlighted {
            animation: commentTargetFlash 900ms cubic-bezier(0.2, 0, 0, 1);
        }
        .comment-item.is-reply {
            margin-top: 12px;
        }
        .comment-branch > .comment-item.is-reply {
            margin-top: 0;
        }
        /* Thread line stub: from under the avatar of a comment that owns a
           lane below (root with branches, head with children) down to the
           first elbow. Avatar is 32px at x0, so its center column is x16. */
        .comment-item.has-kids::after {
            content: '';
            position: absolute;
            left: 16px;
            top: 40px;
            bottom: 0;
            border-left: 1px solid var(--sep);
            pointer-events: none;
        }
        .comment-avatar-btn {
            width: 32px;
            height: 32px;
            flex: 0 0 32px;
            padding: 0;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: transparent;
            cursor: pointer;
        }
        .comment-avatar-btn:disabled {
            cursor: default;
        }
        .comment-avatar {
            width: 32px;
            height: 32px;
            border-radius: 50%;
            object-fit: cover;
            display: block;
        }
        .comment-body {
            flex: 1 1 auto;
            min-width: 0;
        }
        .comment-header {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            gap: 10px;
            margin-bottom: 4px;
        }
        .comment-author-wrapper {
            min-width: 0;
            display: inline-flex;
            align-items: center;
            flex-wrap: wrap;
            gap: 4px;
        }
        .comment-author {
            min-width: 0;
            padding: 0;
            border: none;
            background: transparent;
            color: var(--text);
            cursor: pointer;
            font-size: 13px;
            font-weight: 650;
            line-height: 1.25;
            text-align: left;
        }
        .comment-author:disabled {
            cursor: default;
            color: var(--text3);
        }
        .comment-date {
            flex: 0 0 auto;
            font-size: 11px;
            color: var(--text3);
            line-height: 1.3;
            white-space: nowrap;
        }
        /* "Liked by the author" rides on the date line in the date's own
           type (one style per meta line - the Instagram meta-row idiom, gap
           instead of a dot so a wrapped mark never starts a line with an
           orphan separator). Owner-only, never on the owner's own comment
           (server rule). Arrives and leaves by opacity: inside the date span
           the line keeps its height everywhere the pair fits (a deep lane on
           a 360px phone is the one place it wraps). */
        .comment-date {
            display: inline-flex;
            flex-wrap: wrap;
            column-gap: 8px;
        }
        .comment-author-like {
            white-space: nowrap;
            transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        /* "edited" trails the comment text in the date's type (the Discord
           idiom): the date line keeps at most the date and the author's like */
        .comment-edited {
            margin-left: 6px;
            font-size: 11px;
            color: var(--text3);
            white-space: nowrap;
        }
        .comment-author-like.pending {
            opacity: 0;
        }
        /* Author pin. Same glyph and the same muted weight as the pinned chat
           row in style.css (.chat-pin-ico): a solid stroke dimmed by group
           opacity, never an alpha stroke (the round caps double at the joint
           of the stem). The mark arrives through the house auto-height idiom
           (grid-rows 0fr->1fr, the .comment-branch-kids-wrap timing) + an
           opacity fade, so pin/unpin grows/collapses the line instead of
           teleporting it. On a replace both marks are the same height and
           share the curve, so the tapped action row does not move at all.
           Margin lives on the WRAP and transitions with it: a 0fr row cannot
           compress margins (the branch-kids padding trap). */
        .comment-pin-mark-wrap {
            display: grid;
            grid-template-rows: 0fr;
            margin-bottom: 0;
            transition: grid-template-rows 150ms cubic-bezier(0.4, 0, 0.2, 1),
                        margin-bottom 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .comment-pin-mark-wrap.open {
            grid-template-rows: 1fr;
            margin-bottom: 5px;
        }
        .comment-pin-mark {
            min-height: 0;
            min-width: 0;
            overflow: hidden;
            display: flex;
            align-items: center;
            gap: 5px;
            color: var(--text);
            opacity: 0;
            transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1);
            font-size: 11.5px;
            font-weight: 600;
            line-height: 1.2;
        }
        .comment-pin-mark-wrap.open .comment-pin-mark {
            opacity: 0.42;
        }
        .comment-pin-mark svg {
            width: 12px;
            height: 12px;
            display: block;
            flex: 0 0 auto;
        }
        .comment-reply-context {
            width: fit-content;
            max-width: 100%;
            margin: 2px 0 6px;
            padding: 4px 8px;
            border-radius: var(--r-pill, 999px);
            background: var(--glass);
            color: var(--text3);
            font-size: 12px;
            line-height: 1.2;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .comment-text {
            font-size: 14px;
            color: var(--text);
            line-height: 1.45;
            word-break: break-word;
            white-space: pre-wrap;
        }
        .comment-item.is-deleted .comment-text {
            color: var(--text3);
            font-style: italic;
        }
        .comment-actions {
            margin-top: 8px;
            display: flex;
            align-items: center;
            flex-wrap: wrap;
            gap: 8px;
        }
        .comment-reaction-group {
            display: inline-grid;
            grid-template-columns: repeat(2, 64px);
            gap: 3px;
        }
        .comment-react-btn {
            width: 64px;
            min-width: 64px;
            height: 30px;
            padding: 0 8px;
            border: none;
            background: var(--glass);
            color: var(--text3);
            cursor: pointer;
            display: inline-flex;
            position: relative;
            align-items: center;
            justify-content: center;
            gap: 4px;
            font-size: 12px;
            font-weight: 650;
            font-variant-numeric: tabular-nums;
            transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1), background 130ms cubic-bezier(0.4, 0, 0.2, 1), color 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .comment-react-btn:first-child {
            border-radius: var(--r-btn, 14px) 6px 6px var(--r-btn, 14px);
        }
        .comment-react-btn:last-child {
            border-radius: 6px var(--r-btn, 14px) var(--r-btn, 14px) 6px;
        }
        @media (hover: hover) {
            .comment-react-btn:hover {
                background: var(--glass-bold);
                color: var(--text);
            }
        }
        .comment-react-btn:active {
            transform: scale(0.94);
        }
        .comment-react-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
        .comment-react-btn.selected {
            background: var(--accent-soft);
            color: var(--accent-text);
        }
        .comment-action-icon {
            width: 16px;
            height: 16px;
            fill: currentColor;
            flex: 0 0 auto;
        }
        .comment-react-btn .action-icon-filled {
            display: none;
        }
        .comment-react-btn.selected .action-icon-outline {
            display: none;
        }
        .comment-react-btn.selected .action-icon-filled {
            display: block;
        }
        .comment-action-btn {
            position: relative;
            min-height: 30px;
            padding: 0 2px;
            background: none;
            border: none;
            color: var(--text3);
            font-family: inherit;
            font-size: 12px;
            font-weight: 600;
            cursor: pointer;
        }
        /* text action in a 30px row: invisible pad brings the hit to 44px */
        .comment-action-btn::before {
            content: '';
            position: absolute;
            inset: -7px -4px;
        }
        @media (hover: hover) {
            .comment-action-btn:hover {
                color: var(--text);
            }
        }
        .comment-action-btn:active {
            color: var(--text);
        }
        .comment-action-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
        .comment-action-btn.destructive {
            color: var(--destructive);
        }
        .comment-action-btn.active {
            color: var(--accent-text);
        }
        /* Comment tree lanes (v5.86). Rail x sits at the parent avatar's
           center column: lane margin-left 16 == avatar center. Items carry
           .c-rail (elbow into their avatar) and the last one .c-rail-last
           (the vertical line ends at its elbow). --rail-x = lane padding,
           --rail-run = elbow width; both shrink on narrow screens. */
        .comment-replies,
        .comment-branch-sub,
        .comment-branch-lane,
        .comment-kids-lane {
            --rail-x: 14px;
            --rail-run: 12px;
            margin-left: 16px;
            padding-left: 14px;
        }
        /* Chain-mode branch container: the dialogue continues on the head's
           own avatar column, so no indent at all. */
        .comment-branch-kids.comment-branch-chain {
            margin-left: 0;
            padding-left: 0;
        }
        .comment-replies,
        .comment-kids-lane {
            margin-top: 2px;
        }
        .comment-branch,
        .comment-node {
            margin-top: 12px;
        }
        .comment-branch-sub {
            margin-top: 10px;
        }
        .comment-node > .comment-item {
            margin-top: 0;
        }
        .c-rail {
            position: relative;
        }
        .c-rail::before {
            content: '';
            position: absolute;
            left: calc(0px - var(--rail-x, 14px));
            top: -14px;
            width: var(--rail-run, 12px);
            height: 30px;
            border-left: 1px solid var(--sep);
            border-bottom: 1px solid var(--sep);
            border-bottom-left-radius: 12px;
            pointer-events: none;
        }
        .c-rail:not(.c-rail-last)::after {
            content: '';
            position: absolute;
            left: calc(0px - var(--rail-x, 14px));
            top: -14px;
            bottom: 0;
            border-left: 1px solid var(--sep);
            pointer-events: none;
        }
        /* Chain continuation (a comment's single reply): no elbow, a
           straight X-style connector down the shared avatar column. */
        .comment-node.c-chain::before {
            left: 16px;
            top: -14px;
            width: 0;
            height: 14px;
            border-bottom: none;
            border-bottom-left-radius: 0;
        }
        .comment-branch-toggle {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            min-height: 32px;
            margin-left: -10px;
            padding: 5px 10px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: transparent;
            color: var(--accent-text);
            font-size: 13px;
            font-weight: 600;
            cursor: pointer;
            transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        @media (hover: hover) {
            .comment-branch-toggle:hover {
                background: var(--glass);
            }
        }
        .comment-branch-toggle:active {
            transform: scale(0.98);
        }
        .comment-branch-toggle:disabled {
            color: var(--text3);
            cursor: default;
        }
        .comment-branch-toggle svg {
            width: 12px;
            height: 12px;
            flex: 0 0 auto;
            transition: transform 160ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .comment-branch-toggle.open svg {
            transform: rotate(180deg);
        }
        .comment-branch-toggle.c-rail::before,
        .comment-more-replies-btn.c-rail::before {
            height: 30px;
        }
        /* The toggle box is shifted -10px so its label aligns with the
           avatar column; its rail pseudos must shift back or the whole
           vertical lands 10px off the column (the v5.87.0 seam bug). */
        .comment-branch-toggle.c-rail::before,
        .comment-branch-toggle.c-rail::after {
            left: calc(10px - var(--rail-x, 14px));
        }
        .comment-branch-kids-wrap {
            display: grid;
            grid-template-rows: 0fr;
            transition: grid-template-rows 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .comment-branch-kids-wrap.open {
            grid-template-rows: 1fr;
            transition: grid-template-rows 220ms cubic-bezier(0.05, 0.7, 0.1, 1);
        }
        .comment-branch-kids {
            min-height: 0;
            /* grid item: without min-width 0 the track grows to the deep
               rows' min-content and the whole thread pans past the modal */
            min-width: 0;
            overflow-x: visible;
            overflow-y: clip;
            padding-top: 2px;
            visibility: hidden;
            transition: visibility 0s 150ms;
        }
        /* Collapsed 0fr rows cannot compress padding; the leftover 2px
           strip leaks rail fragments, so drop it while closed. */
        .comment-branch-kids-wrap:not(.open) > .comment-branch-kids {
            padding-top: 0;
        }
        .comment-branch-kids-wrap.open .comment-branch-kids {
            visibility: visible;
            transition: visibility 0s;
        }
        @media (prefers-reduced-motion: reduce) {
            .comment-branch-kids-wrap,
            .comment-branch-kids,
            .comment-branch-toggle svg,
            .comment-pin-mark-wrap,
            .comment-pin-mark,
            .comment-author-like {
                transition: none;
            }
        }
        .comment-more-replies-btn {
            min-height: 36px;
            margin-top: 12px;
            padding: 0 14px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--glass);
            color: var(--text2);
            font-size: 13px;
            font-weight: 600;
            cursor: pointer;
            transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1), background 130ms cubic-bezier(0.4, 0, 0.2, 1), color 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        @media (hover: hover) {
            .comment-more-replies-btn:hover {
                background: var(--glass-bold);
                color: var(--text);
            }
        }
        .comment-more-replies-btn:active {
            background: var(--glass-bold);
            color: var(--text);
            transform: scale(0.97);
        }
        .comment-more-replies-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        .comment-more-replies-btn:disabled { opacity: 0.32; pointer-events: none; }
        .comments-empty {
            text-align: center;
            padding: 16px 0 10px;
        }
        .comments-loading-more {
            text-align: center;
            padding: 12px 0 4px;
            color: var(--text3);
            font-size: 13px;
        }
        .comments-list.soft-loading {
            opacity: 0.5;
            pointer-events: none;
        }
        .comment-scroll-sentinel {
            width: 100%;
            height: 1px;
        }
        .comment-reply-box {
            margin-top: 10px;
            display: flex;
            flex-direction: column;
            gap: 8px;
        }
        /* in-place editor: takes the text's slot, so no top margin of its own */
        .comment-reply-box.comment-edit-box {
            margin-top: 2px;
        }
        .comment-reply-box .comment-reply-input {
            width: 100%;
            min-height: 38px;
            box-sizing: border-box;
            resize: none;
        }
        .comment-reply-box-actions {
            display: flex;
            justify-content: flex-end;
            align-items: center;
            gap: 8px;
        }
        .comment-reply-cancel {
            min-height: 40px;
            padding: 0 12px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: transparent;
            color: var(--text2);
            font-family: inherit;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
        }
        @media (hover: hover) {
            .comment-reply-cancel:hover {
                color: var(--text);
            }
        }
        .comment-reply-cancel:active { color: var(--text); }
        .comment-reply-cancel:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        /* same grammar as the root composer's send: tonal-accent pill, the
           screen's single solid-accent CTA stays the chat button */
        .comment-reply-send {
            min-height: 40px;
            padding: 0 16px;
            flex: 0 0 auto;
            border-radius: var(--r-pill, 999px);
            background: var(--accent-soft);
            color: var(--accent-text);
            font-size: 14px;
        }
        @keyframes commentTargetFlash {
            0%, 35% {
                background: color-mix(in srgb, var(--text) 7%, transparent);
            }
            100% {
                background: transparent;
            }
        }
        @media (max-width: 560px) {
            .comments-head {
                gap: 12px;
                margin-bottom: 14px;
            }
            .comment-sort {
                gap: 3px;
            }
            .comment-sort-btn {
                height: 36px;
                padding: 0 14px;
                font-size: 13px;
            }
            .comment-header {
                flex-direction: column;
                align-items: flex-start;
                gap: 2px;
            }
            .comment-date {
                white-space: normal;
            }
            .comment-replies,
            .comment-branch-sub,
            .comment-branch-lane {
                --rail-x: 10px;
                --rail-run: 8px;
                margin-left: 16px;
                padding-left: 10px;
            }
            /* Deep lanes tighten so deep text keeps ~200px at 390px.
               margin-left stays 16px: the rail must sit under the parent
               avatar's center column. */
            .comment-kids-lane {
                --rail-x: 8px;
                --rail-run: 7px;
                margin-left: 16px;
                padding-left: 8px;
            }
            .comment-branch-kids.comment-branch-chain {
                margin-left: 0;
                padding-left: 0;
            }
            .c-rail::before {
                border-bottom-left-radius: 8px;
            }
            .comment-kids-lane > .c-rail::before {
                border-bottom-left-radius: 7px;
            }
            /* Keep like + dislike + reply + delete on one line at branch
               indent: 4 actions no longer fit 390px with 64px react chips. */
            .comment-reaction-group {
                grid-template-columns: repeat(2, 56px);
            }
            .comment-react-btn {
                width: 56px;
                min-width: 56px;
            }
            .comment-actions {
                gap: 6px;
            }
        }
        /* Profile Modal sits below character detail */
        #profileModal {
            z-index: 1000;
        }
        .profile-modal-dynamic {
        }

        /* character-modal scroll family: the content is a non-scrolling flex
           column and .modal-scroll is the single scroller, so the close chip
           (and edit mode's sticky footer) stay pinned regardless of scroll */
        .profile-modal-dynamic > .modal-content {
            overflow: hidden;
            display: flex;
            flex-direction: column;
        }
        .profile-modal-dynamic > .modal-content > .modal-scroll {
            min-height: 0;
        }

        .profile-modal-dynamic .close-modal-btn {
            position: absolute;
            top: 12px;
            right: 12px;
            z-index: 18;
            color: var(--text);
        }

        .profile-modal-dynamic .close-modal-btn svg {
            width: 20px;
            height: 20px;
        }

        .profile-modal-dynamic .close-modal-btn:focus-visible,
        .profile-modal-dynamic .pf-poster-chip:focus-visible {
            outline: 2px solid var(--accent-text);
            outline-offset: 2px;
        }

        /* The switch is ONE control for both surfaces and lives in style.css
           (iOS 26 64x28 track, wide pill knob, nothing grows under the finger).
           Nothing about it may be redefined here: community.css loads later and
           a local copy would silently win on the community forms only. */

        /* Filter inputs are hidden; chips show state via tonal fill
           (see .filter-checkbox near the top of the file) */
        .profile-chars-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr); /* 2 columns mobile default */
            gap: 12px;
            padding: 20px;
        }
        
        @media (min-width: 768px) {
            .profile-chars-grid {
                gap: 16px;
            }
        }

        /* Reusing .card styling but ensuring it fits */
        .profile-chars-grid .card {
            animation: none; /* Disable animation for smoother loading */
            opacity: 1;
        }
        
        .edit-profile-btn {
            background: var(--glass);
            border: none;
            color: var(--text);
            padding: 8px 24px;
            border-radius: var(--r-pill, 999px);
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: background 130ms cubic-bezier(.4,0,.2,1), color 130ms cubic-bezier(.4,0,.2,1), border-color 130ms cubic-bezier(.4,0,.2,1);
        }
      .comment-input-area {
          background: var(--glass);
          transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1);
          border: none;
          border-radius: var(--r-btn, 14px);
          padding: 10px 12px;
          margin: 0 0 16px;
          box-sizing: border-box;
          display: flex;
          align-items: center;
          flex-wrap: wrap;
          gap: 8px;
      }
      .comment-input-area:focus-within {
          background: var(--glass-bold);
      }
        .comment-input-area .form-textarea {
            flex: 1 1 auto;
            width: auto;
            min-height: 36px;
            max-height: 100px;
            margin: 0;
            padding: 8px 0;
            resize: none;
            border: none;
            background: transparent;
            border-radius: 0;
        }

        .comment-input-area .form-textarea:focus {
            border: none;
            background: transparent;
            box-shadow: none;
        }

        .comment-input-area .form-btn {
            flex: 0 0 auto;
            width: auto !important;
            min-width: 0;
            margin: 0 !important;
            padding: 0 16px;
            height: 36px;
            font-size: 14px;
            font-weight: 600;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: var(--r-pill, 999px);
            background: var(--accent-soft);
            color: var(--accent-text);
            border: none;
        }
        .comments-list {
            padding-bottom: 8px;
            /* base transition: the soft-loading undim must animate (see .cards-container) */
            transition: opacity 130ms cubic-bezier(0.4, 0, 0.2, 1);
            /* The widget manages its own geometry on in-place height changes
               (pin marks grow/collapse as an equal-height cancelling pair).
               Native scroll anchoring compensates the shrink side on ITS OWN
               anchor node, which un-cancels the pair and walks the tapped row
               down by a full mark height. */
            overflow-anchor: none;
        }
        .edit-profile-btn:hover { background: var(--glass-bold); color: var(--text); }
        .edit-profile-btn:active { background: var(--glass-border); color: var(--text); }
        /* Follow toggle: accent CTA when not following, tonal once following. */
        .p-follow-btn { background: var(--accent); color: #fff; }
        .p-follow-btn:hover { background: var(--accent); color: #fff; }
        .p-follow-btn:active { background: var(--accent); color: #fff; }
        .p-follow-btn.following { background: var(--glass-bold); color: var(--text); }
        .p-follow-btn.following:hover { background: var(--glass-bold); color: var(--text); }
        .p-follow-btn.following:active { background: var(--glass-bold); color: var(--text); }
        .user-badges {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            vertical-align: middle;
            margin-left: 6px;
        }
        
        .badge-icon {
            width: 18px;
            height: 18px;
            display: inline-block;
            border-radius: 4px;
        }
        .badge-creator {
            /* neutral + theme-adaptive: white on dark, dark on light. No hue, so the
               filled logo stays MD3-restrained instead of reading as a color blob. */
            color: var(--text);
            filter: drop-shadow(0 0 3px color-mix(in srgb, var(--text) 28%, transparent));
        }
        .badge-verified {
            color: #1da1f2; /* Twitter Blue */
            filter: drop-shadow(0 0 5px rgba(29, 161, 242, 0.4));
        }
        
        .badge-admin { 
            color: #d1d5db; /* Светло-серый металлик */
            filter: drop-shadow(0 0 4px rgba(209, 213, 219, 0.25)); 
        }
        .badge-moderator { 
            color: #10b981; /* Green */
            filter: drop-shadow(0 0 4px rgba(16, 185, 129, 0.3)); 
        }
        .badge-trusted { color: #00fa9a; filter: drop-shadow(0 0 5px rgba(0, 250, 154, 0.4)); }
        
        .badge-lite { 
            color: #e8dcb5; /* Приятный бежевый */
            filter: drop-shadow(0 0 5px rgba(232, 220, 181, 0.2)); 
        }
        
        .badge-premium { 
            color: #ffc107; /* Янтарное золото */
            filter: drop-shadow(0 0 6px rgba(255, 193, 7, 0.4)); 
        }
        
        .badge-ultimate {
            color: #00e5ff; /* Электрик циан (Алмаз) */
            filter: drop-shadow(0 0 7px rgba(0, 229, 255, 0.5));
        }

        /* Streak badges */
        .badge-streak-common {
            color: #ffd700;
            filter: drop-shadow(0 0 4px rgba(255, 215, 0, 0.4));
        }
        .badge-streak-rare {
            color: #ff8c00;
            filter: drop-shadow(0 0 5px rgba(255, 140, 0, 0.5));
        }
        .badge-streak-epic {
            color: #ff3030;
            filter: drop-shadow(0 0 6px rgba(255, 48, 48, 0.5));
        }
        .badge-streak-legendary {
            color: #ff2020;
            filter: drop-shadow(0 0 8px rgba(255, 32, 32, 0.7));
            animation: badge-glow 2s ease-in-out infinite alternate;
        }
        .badge-streak-mythic {
            color: #da70d6;
            filter: drop-shadow(0 0 10px rgba(218, 112, 214, 0.7));
            animation: badge-glow-mythic 1.5s ease-in-out infinite alternate;
        }
        .badge-streak-eternal {
            color: #b8ecff;
            filter: drop-shadow(0 0 11px rgba(125, 230, 255, 0.8));
            animation: badge-glow-eternal 1.2s ease-in-out infinite alternate;
        }
        .badge-streak-immortal {
            color: #ff9ad5;
            animation: badge-iri-glow 8s linear infinite;
        }
        .badge-streak-immortal path {
            fill: url(#strk-ir);
        }
        @keyframes badge-glow {
            from { filter: drop-shadow(0 0 5px rgba(255, 32, 32, 0.5)); }
            to   { filter: drop-shadow(0 0 12px rgba(255, 32, 32, 0.9)); }
        }
        @keyframes badge-glow-mythic {
            from { filter: drop-shadow(0 0 6px rgba(218, 112, 214, 0.5)); }
            to   { filter: drop-shadow(0 0 14px rgba(218, 112, 214, 0.9)); }
        }
        @keyframes badge-glow-eternal {
            from { filter: drop-shadow(0 0 6px rgba(125, 230, 255, 0.5)); }
            to   { filter: drop-shadow(0 0 15px rgba(125, 230, 255, 0.95)); }
        }
        @keyframes badge-iri-glow {
            0%, 100% { filter: drop-shadow(0 0 11px rgba(255, 216, 107, 0.7)); }
            25% { filter: drop-shadow(0 0 11px rgba(255, 154, 213, 0.7)); }
            50% { filter: drop-shadow(0 0 11px rgba(192, 154, 255, 0.7)); }
            75% { filter: drop-shadow(0 0 11px rgba(139, 233, 255, 0.7)); }
        }

        .profile-header-bg {
            background: var(--bg-tint);
            padding: 32px 20px;
            border-radius: 20px 20px 0 0;
            display: flex;
            flex-direction: column;
            align-items: center;
            position: relative;
            overflow: hidden;
            z-index: 1;
        }

        .profile-modal-dynamic .profile-header-bg {
            min-height: 292px;
            background: var(--bg-tint);
            padding: 0;
            align-items: stretch;
            text-align: center;
            border-bottom: none;
        }

        /* Narrow-viewport scaling (v5.103): the card was tuned on wide/high-DPI
           viewports; on the 360-430px CSS widths most phones actually have, its
           fixed px sizes filled the whole square and read oversized. Everything
           inside the poster scales with vw clamps whose MAX is the old fixed
           value, so any viewport wider than ~440px (including desktop) renders
           pixel-identical to before and narrow phones shrink smoothly. */
        .profile-poster-content {
            position: relative;
            z-index: 2;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: clamp(9px, 3vw, 12px);
            width: 100%;
            min-height: 292px;
            /* Bottom padding reserves the strip where .p-view-actions floats */
            padding: clamp(14px, 4.5vw, 18px) clamp(12px, 4vw, 16px) 72px;
        }

        .profile-identity-card {
            position: relative;
            isolation: isolate;
            overflow: hidden;
            width: min(100%, clamp(300px, 86vw, 360px));
            margin: 0 auto;
            padding: clamp(11px, 3.3vw, 14px) clamp(14px, 4.4vw, 18px) clamp(12px, 3.6vw, 15px);
            border-radius: var(--r-island);
            /* Flat variant: separation is the tone step alone, no shadow.
               The elevation/glass shadow belongs to .has-backdrop (over a photo
               the island floats; over a plain tonal bg it does not). */
            background: var(--glass);
        }

        .profile-identity-card > * {
            position: relative;
            z-index: 1;
        }
        .p-ava-wrap { position: relative; }
        /* Pin the wrap to the avatar size (move the spacing onto the wrap) so the
           1.2x frame overlay centres on the avatar and not on avatar+margin. */
        .profile-header-bg .p-ava-wrap { width: clamp(78px, 23vw, 100px); height: clamp(78px, 23vw, 100px); margin: 0 auto; }
        .profile-header-bg .p-ava-wrap .profile-avatar-large { margin: 0; display: block; width: 100%; height: 100%; }
        /* Flat variant: bare M3 avatar, no cutout ring (the ring is a photo
           idiom; .has-backdrop below restores it as a var(--bg-tint) border).
           border-color only, so the 3px geometry the frame overlay relies on
           stays identical. */
        .profile-modal-dynamic .profile-header-bg .profile-avatar-large {
            border-color: transparent;
            box-shadow: none;
        }
        /* Actions float over the poster photo, pinned to its bottom edge.
           .profile-poster-content reserves padding-bottom so a tall bio
           never collides with them. Glass idiom: they sit on content. */
        .p-view-actions {
            position: absolute;
            left: 16px;
            right: 16px;
            bottom: 14px;
            z-index: 3;
        }
        /* Flat variant: M3 tonal pills (mirrors .form-btn tones). Glass and
           shadows belong only over the photo; see .has-backdrop rules below. */
        .p-view-actions .edit-profile-btn {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-height: 44px;
            padding: 0 22px;
            color: var(--text);
            background: var(--glass-bold);
            transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1), background 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .p-view-actions .edit-profile-btn:hover {
            color: var(--text);
            background: var(--glass-border);
        }
        .p-view-actions .edit-profile-btn:active {
            color: var(--text);
            background: var(--glass-border);
            transform: scale(0.94);
        }
        /* Gift waits one request for the recipient's giftable tiers: the
           control stays readable, only the second tap is held back */
        .p-view-actions .p-gift-btn[aria-busy="true"] { opacity: 0.7; pointer-events: none; }
        /* Follow stays the one filled-accent CTA; opaque, no blur needed. */
        .p-view-actions .p-follow-btn,
        .p-view-actions .p-follow-btn:hover,
        .p-view-actions .p-follow-btn:active {
            color: #fff;
            background: var(--accent);
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
        }
        .p-view-actions .p-follow-btn.following,
        .p-view-actions .p-follow-btn.following:hover,
        .p-view-actions .p-follow-btn.following:active {
            color: var(--text);
            background: var(--glass-bold);
        }
        /* Over the photo the actions ARE glass: full recipe, rim + float shadow. */
        .profile-header-bg.has-backdrop .p-view-actions .edit-profile-btn {
            background: var(--chrome-glass-fill-media);
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            box-shadow: var(--chrome-glass-shadow-thin);
        }
        .profile-header-bg.has-backdrop .p-view-actions .edit-profile-btn:hover,
        .profile-header-bg.has-backdrop .p-view-actions .edit-profile-btn:active {
            background: color-mix(in srgb, var(--glass) 80%, transparent);
        }
        /* Accent CTA over the photo stays opaque: elevation only, no glass rim
           (rim light on an opaque fill reads as cheap M3-with-highlights). */
        .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn,
        .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn:hover,
        .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn:active {
            background: var(--accent);
            backdrop-filter: none;
            -webkit-backdrop-filter: none;
            box-shadow:
                0 1px 2px rgb(0 0 0 / 25%),
                0 4px 12px rgb(0 0 0 / 12%);
        }
        .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn.following,
        .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn.following:hover,
        .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn.following:active {
            color: var(--text);
            background: var(--chrome-glass-fill-media);
            backdrop-filter: var(--chrome-glass-filter-media);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media);
            box-shadow: var(--chrome-glass-shadow-thin);
        }
        /* no-blur fallback (only the photo variant needs one; the flat variant
           is already opaque tonal): solid fill + elevation, rim removed. */
        body.no-blur .profile-header-bg.has-backdrop .p-view-actions .edit-profile-btn {
            background: var(--glass);
            box-shadow:
                0 1px 2px rgb(0 0 0 / 25%),
                0 4px 12px rgb(0 0 0 / 12%);
        }
        body.no-blur .profile-header-bg.has-backdrop .p-view-actions .edit-profile-btn:hover,
        body.no-blur .profile-header-bg.has-backdrop .p-view-actions .edit-profile-btn:active {
            background: var(--glass-bold);
        }
        body.no-blur .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn,
        body.no-blur .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn:hover,
        body.no-blur .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn:active {
            background: var(--accent);
        }
        body.no-blur .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn.following,
        body.no-blur .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn.following:hover,
        body.no-blur .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn.following:active {
            background: var(--glass);
            box-shadow:
                0 1px 2px rgb(0 0 0 / 25%),
                0 4px 12px rgb(0 0 0 / 12%);
        }

        /* In-place editor: the island holds the real inputs. They inherit the
           final typography (the name input sits INSIDE the h2 next to the badges)
           but carry the .m3-field tonal fill so they read as editable -
           fully transparent inputs were indistinguishable from view mode and
           nobody realized the text was tappable. Paddings are cancelled by
           negative margins so the text keeps the exact view-mode geometry. */
        .p-island-name,
        .p-island-bio {
            border: none;
            background: var(--glass-bold);
            border-radius: var(--r-btn, 14px);
            outline: none;
            resize: none;
            text-align: center;
            font: inherit;
            color: inherit;
            caret-color: var(--accent);
        }
        /* Well overhangs are budgeted against the 8px name->bio gap: the pill
           reaches 2px below its text, the bio well 4px above its own (2px
           daylight between them). The v5.103 counter-row removal exposed this -
           the old counter line doubled as the spacing buffer. Horizontal pill
           padding = the h2's 6px gap, so the pill ends exactly where the badges
           begin. fitNameInput's width constant tracks this padding. */
        .p-island-name {
            display: block;
            min-width: 24px;
            max-width: 100%;
            padding: 3px 6px 2px;
            margin: -3px -6px -2px;
        }
        .p-island-bio {
            display: block;
            width: calc(100% + 20px);
            /* padding is cancelled by the negative margins and line-height matches
               .profile-bio (see the shared card rule), so the textarea occupies
               exactly the static bio's box: bottom padding + bottom margin = 12px
               (the static bio's margin) in both modes. Top padding is 4px, not 8:
               a taller well overhang collides with the name pill (see above). */
            padding: 4px 10px 8px;
            font-size: 14px;
            line-height: 1.5;
            color: var(--text2);
            overflow: hidden;
            margin: -4px -10px 4px;
        }
        .p-island-name::placeholder,
        .p-island-bio::placeholder {
            color: var(--text3);
        }
        /* the empty-name hint must not wear the display-title type of the
           name itself; fitNameInput measures with the same reduced style */
        .p-island-name::placeholder {
            font-size: 15px;
            font-weight: 500;
        }

        .p-username-copy {
            /* block-level flex, centred by auto side margins: as an INLINE
               flex box its vertical margins go into the line box instead of
               moving it, and the subtitle rhythm below cannot be dialled in */
            display: flex;
            width: fit-content;
            align-items: center;
            justify-content: center;
            gap: 5px;
            /* subtitle rhythm, one rule with .profile-username in style.css,
               and the arithmetic has to be spelled out because the two cards
               lay out differently. The 34px target leaves 7px of dead box
               above and below the 20px text line, so the bottom margin FIRST
               cancels that 7 and only then adds the real gap: hero -7 + 8px
               flex gap, here -7 + 4 = -3 (this card is a block, it has no
               gap). Measured ink-to-ink at 390px: 10 above, 11 below; the hero
               reads 11 above and 10 down to the tier chip's fill edge. The
               plain +4 that lived here skipped the cancel step and left 18px
               under the handle - the owner spotted the two profiles side by
               side (2026-08-21). Both bio and stats sit 11px below now: the
               empty-bio card moves with the same margin. */
            min-height: 34px;
            max-width: 100%;
            margin: -12px auto -3px;
            padding: 4px 8px;
            border: 0;
            border-radius: var(--r-pill);
            background: transparent;
            color: var(--text2);
            font: inherit;
            font-size: 14px;
            line-height: 20px;
            cursor: pointer;
            transition: color 130ms cubic-bezier(0.4, 0, 0.2, 1), opacity 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .p-username-copy:active {
            opacity: 0.55;
        }
        @media (hover: hover) {
            .p-username-copy:hover { color: var(--text); }
            .p-username-copy:hover .p-username-copy-ic { opacity: 0.7; }
        }
        .p-username-copy:focus-visible {
            outline: none;
            text-decoration: underline;
            text-decoration-color: var(--accent);
            text-decoration-thickness: 2px;
            text-underline-offset: 3px;
        }
        .p-username-copy-ic {
            display: block;
            width: 14px;
            height: 14px;
            flex: 0 0 auto;
            color: var(--text);
            opacity: 0.48;
            transition: opacity 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .p-username-text {
            min-width: 0;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        /* The three editable fields are ONE family: a tonal well the exact size
           of the text it holds, sitting where that text sits when the card is
           read (name pill, handle pill, bio block). The handle used to be a
           260x44 box - a control from another screen dropped onto the card,
           and 43px of card height that appeared the moment edit opened. */
        .p-username-edit {
            width: 100%;
            margin: 0;
        }
        .p-username-field {
            display: flex;
            width: fit-content;
            max-width: 100%;
            align-items: center;
            justify-content: center;
            /* padding cancelled by the negative margins, like .p-island-name:
               the text keeps its view-mode geometry and only the fill is new */
            margin: -7px auto -2px;
            padding: 3px 8px 2px;
            border-radius: var(--r-btn);
            background: var(--glass-bold);
            color: var(--text2);
            transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1);
        }

        /* width is written by fitUsernameInput (the hidden measure span shares
           this type), so the well hugs the handle exactly like the name pill */
        .p-input-username {
            min-width: 12px;
            border: 0;
            outline: 0;
            padding: 0;
            background: transparent;
            color: var(--text);
            caret-color: var(--accent);
            font: inherit;
            /* the same 14px the handle is READ at, so nothing shifts on save */
            font-size: 14px;
            line-height: 20px;
            text-align: center;
        }
        .p-input-username::placeholder { color: var(--text3); }
        /* the one row edit mode has that reading mode does not: the handle's
           format line. It keeps its space at all times (a hint that appears on
           the first keystroke would shove the bio down mid-typing), and it owns
           the gap to the bio well - whose own negative top margin eats 4 of it */
        .p-username-status {
            min-height: 16px;
            margin-bottom: 10px;
            padding: 4px 4px 0;
            color: var(--text3);
            font-size: 11px;
            line-height: 14px;
            text-align: center;
        }
        .p-username-status[data-state="available"] { color: var(--accent-text); }
        .p-username-status[data-state="taken"],
        .p-username-status[data-state="invalid"],
        .p-username-status[data-state="reserved"],
        .p-username-status[data-state="error"] { color: var(--destructive); }
        .profile-header-bg.has-backdrop .p-username-field {
            background: rgba(255, 255, 255, 0.08);
        }

        /* Over the photo the island is glass; the tonal token melts into it,
           so the wells step up with the white-alpha glass idiom instead. */
        .profile-header-bg.has-backdrop .p-island-name,
        .profile-header-bg.has-backdrop .p-island-bio {
            background: rgba(255, 255, 255, 0.08);
        }
        /* display:contents removes the wrapper from layout entirely: the inner
           .user-badges span is a direct flex item of the h2, byte-identical to
           the pre-rework markup (keeps badge alignment and spacing exact). */
        .p-name-badges {
            display: contents;
        }
        .p-name-measure {
            position: absolute;
            visibility: hidden;
            white-space: pre;
            pointer-events: none;
            font: inherit;
        }

        .profile-modal-dynamic .p-zoom {
            margin: 12px 20px 0;
        }

        /* Add-banner / lock actions sit on the poster itself in edit mode */
        .profile-poster-content .pf-backdrop-add,
        .profile-poster-content .pf-backdrop-lock {
            width: auto;
            align-self: center;
            padding: 0 18px;
            margin-bottom: 4px;
        }

        /* Sticky save bar: Cancel/Save always visible, nothing to scroll for.
           Deliberately a solid tonal bar, NOT glass: Firefox does not blur
           scrolled content behind elements inside a scroll container, so the
           glass recipe degraded to mud there (owner call: no blur here at all).
           Separation from the modal bg is one tone step + elevation shadow. */
        .p-edit-footer {
            position: sticky;
            bottom: 0;
            z-index: 20;
            display: none;
            gap: 8px;
            margin-top: 12px;
            padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 0px));
            background: var(--bg-tint);
            /* The second shadow is not decoration: it is the bar's own fill
               painted 2px BELOW its box, and it closes a seam the layout cannot.
               Sticky puts the bar's bottom edge exactly on the scroller's (it
               measures as a clean 0, and 0.08px short of it at the end of the
               list), but at a fractional device pixel ratio the sheet's rounded
               clip rect and the bar's background rect round to DIFFERENT device
               pixels, and the row between them shows the list scrolling
               underneath (owner, Firefox at dpr 1.0714). Geometry cannot fix
               this: at the end of the scroll the last box IS the scrollport
               bottom by construction, so nothing can overshoot it - only paint
               reaches past. Keep the colour equal to the bar's own background.
               Guard: harness profilecardedit-baredge. */
            box-shadow: 0 -6px 16px rgb(0 0 0 / 18%), 0 2px 0 0 var(--bg-tint);
        }
        .p-edit-footer.active { display: flex; }
        .p-edit-footer .form-btn {
            flex: 1;
            min-height: 44px;
            margin: 0;
        }

        .profile-name-large {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 6px;
            max-width: 100%;
            overflow-wrap: anywhere;
            margin: 0 0 8px;
        }

        .profile-actions-row {
            display: flex;
            justify-content: center;
            gap: 8px;
            flex-wrap: wrap;
        }

        /* Poster: square when a real backdrop exists (owner intent: a BACKGROUND
           occupying a big area, not a content-hugging banner). The <img> + scrim come
           from the shared style.css .profile-backdrop-media / -scrim rules.
           Height = max(width, content): a grid with a zero-width ::before spacer
           whose padding-top:100% enforces the 1:1 minimum, while the content shares
           the same cell and can stretch the row past the square when a long bio
           needs it. (aspect-ratio + min-height:max-content does NOT do this on a
           flex container: verified broken in Chromium, content got clipped.) */
        .profile-header-bg.has-backdrop {
            background: var(--bg);
            border-bottom: none;
            display: grid;
            grid-template-columns: 100%;
            min-height: 0;
        }
        .profile-header-bg.has-backdrop::before {
            content: '';
            grid-area: 1 / 1;
            width: 0;
            padding-top: 100%;
        }
        /* Content centered in the square (owner-picked over bottom-anchored) */
        .profile-header-bg.has-backdrop .profile-poster-content {
            grid-area: 1 / 1;
            height: auto;
            min-height: 0;
            justify-content: center;
            padding: 18px 16px 72px;
        }
        .profile-header-bg.has-backdrop .profile-avatar-large {
            border: 3px solid var(--bg-tint);
            box-shadow: none;
        }
        .profile-header-bg.has-backdrop .profile-name-large,
        .profile-header-bg.has-backdrop .profile-bio,
        .profile-header-bg.has-backdrop .profile-stat-value,
        .profile-header-bg.has-backdrop .profile-stat-label {
            color: var(--text);
            text-shadow: 0 1px 10px rgba(0, 0, 0, 0.64);
        }
        .profile-header-bg.has-backdrop .profile-stat-label {
            color: rgba(255, 255, 255, 0.78);
        }
        /* CLEAR-readable glass: the card stands on guaranteed media (the
           banner); same tokens as the app profile hero card */
        .profile-header-bg.has-backdrop .profile-identity-card {
            background: var(--chrome-glass-fill-media-readable);
            backdrop-filter: var(--chrome-glass-filter-media-readable);
            -webkit-backdrop-filter: var(--chrome-glass-filter-media-readable);
            box-shadow: var(--chrome-glass-shadow);
        }

        .profile-header-bg.has-backdrop .profile-identity-card::before {
            content: '';
            position: absolute;
            inset: 0;
            z-index: 0;
            border-radius: inherit;
            background: color-mix(in srgb, var(--bg) 22%, transparent);
            pointer-events: none;
        }

        /* no-blur: opaque tonal card over the photo = elevation only, no rim. */
        body.no-blur .profile-header-bg.has-backdrop .profile-identity-card {
            background: var(--glass);
            box-shadow:
                0 1px 2px rgb(0 0 0 / 25%),
                0 12px 28px rgb(0 0 0 / 26%);
        }

        body.no-blur .profile-header-bg.has-backdrop .profile-identity-card::before {
            display: none;
        }

        .profile-header-bg.has-backdrop .profile-identity-card .profile-name-large,
        .profile-header-bg.has-backdrop .profile-identity-card .profile-stat-value {
            color: var(--text);
            text-shadow: none;
        }

        /* Full bio, no clamp: the island sizes to its content and the poster
           grows to fit (see .has-backdrop above). Clamping here was rejected. */
        .profile-header-bg.has-backdrop .profile-identity-card .profile-bio {
            color: var(--text2);
            text-shadow: none;
        }

        .profile-header-bg.has-backdrop .profile-identity-card .profile-bio:empty {
            display: none;
        }

        .profile-header-bg.has-backdrop .profile-identity-card .profile-stat-label {
            color: var(--text3);
            text-shadow: none;
        }

        .profile-identity-card .profile-name-large {
            color: var(--text);
            /* same numbers as the app profile's .profile-name: 22/700, badges 18,
               gap 6 (one rule for both profiles, owner 2026-08-19) */
            font-size: 22px;
            line-height: 1.16;
            letter-spacing: -0.5px;
        }

        .profile-identity-card .profile-name-large .badge-icon {
            width: 18px;
            height: 18px;
        }
        .profile-identity-card .profile-name-large .user-badges { margin-left: 0; }

        .profile-identity-card .profile-avatar-large,
        .profile-header-bg.has-backdrop .profile-identity-card .profile-avatar-large {
            box-shadow: none;
        }

        /* One rule for BOTH the static bio and its edit-mode textarea: identical
           font metrics + wrapping are what keeps the island the same height in
           both modes (a 300-char single word must break the same way twice) */
        .profile-identity-card .profile-bio,
        .profile-identity-card .p-island-bio {
            font-size: clamp(12px, 3.35vw, 14px);
            line-height: 1.5;
            overflow-wrap: anywhere;
        }

        .profile-identity-card .profile-bio {
            max-width: 100%;
            margin: 0 auto 12px;
            color: var(--text2);
        }

        .profile-identity-card .profile-bio:empty {
            display: none;
        }

        /* Equal-width columns hugging the widest label: a fit-content grid
           equalizes 1fr tracks to the largest max-content, so the middle
           stat stays on the card axis without spreading to full width
           (flex centering shifts it when side labels differ). */
        .profile-identity-card .profile-stats-row {
            display: grid;
            grid-auto-flow: column;
            grid-auto-columns: 1fr;
            width: fit-content;
            gap: clamp(14px, 4.5vw, 20px);
            margin: 0 auto;
        }

        .profile-identity-card .profile-stat-value {
            color: var(--text);
            font-size: clamp(14px, 4.25vw, 18px);
            font-variant-numeric: tabular-nums;
        }

        .profile-identity-card .profile-stat-label {
            color: var(--text3);
            font-size: clamp(10px, 2.75vw, 12px);
            text-transform: none;
            letter-spacing: 0;
        }

        .profile-joined-line {
            font-size: clamp(10px, 2.75vw, 12px);
            color: var(--text3);
            text-align: center;
            margin: 10px 0 0;
        }

        /* Edit-mode counters live in the joined line's slot (same type row), so
           toggling edit never changes the island height */
        .p-edit-counts {
            display: flex;
            justify-content: center;
            gap: 6px;
            font-variant-numeric: tabular-nums;
        }

        .profile-joined-line:empty {
            display: none;
        }

        /* Media actions top-LEFT, close top-right (codex review, v5.103): one
           combined right-side cluster still reached the centered avatar at
           320-360px; split corners clear it structurally and separate the
           media actions from the unrelated close */
        .profile-modal-dynamic .pf-poster-tools {
            top: 12px;
            left: 12px;
            right: auto;
            z-index: 18;
            gap: 8px;
        }

        /* Chips shrink with the poster so the top corners never reach the
           centered avatar; the ::after pad keeps the HIT area at >=44px even
           when the visible chip drops to 34px */
        /* NO position here: .close-modal-btn is already position:absolute from
           its own rule (a blanket relative would knock it out of its corner);
           the chips get relative separately for the ::after hit pad */
        .profile-modal-dynamic .pf-poster-chip,
        .profile-modal-dynamic .close-modal-btn {
            height: clamp(34px, 10vw, 40px);
            min-width: clamp(34px, 10vw, 40px);
        }

        .profile-modal-dynamic .pf-poster-chip {
            position: relative;
        }

        .profile-modal-dynamic .pf-poster-chip::after,
        .profile-modal-dynamic .close-modal-btn::after {
            content: '';
            position: absolute;
            inset: calc((clamp(34px, 10vw, 40px) - 44px) / 2);
            border-radius: inherit;
        }

        /* Bottom-center, not top-left: the top band belongs to the avatar and
           the chips; at 360-430px the old top-left pill lay across the avatar */
        .profile-modal-dynamic .pf-poster-hint {
            top: auto;
            bottom: 14px;
            left: 50%;
            transform: translateX(-50%);
            z-index: 17;
            min-height: clamp(34px, 10vw, 40px);
            max-width: calc(100% - 28px);
            padding: 0 clamp(12px, 4vw, 16px);
            font-size: clamp(12px, 3.35vw, 14px);
        }

        .profile-modal-dynamic .pf-poster-chip {
            color: var(--text);
        }

        .profile-modal-dynamic .pf-poster-chip svg,
        .profile-modal-dynamic .pf-poster-hint svg {
            width: 20px;
            height: 20px;
        }

        /* In-place crop: while editing, the bare photo is the drag surface.
           The island and avatar keep native vertical panning, so the modal
           stays scrollable by grabbing them (gesture split: photo pans the
           photo, furniture scrolls the page). */
        .profile-header-bg.is-editing.has-backdrop {
            touch-action: none;
            cursor: grab;
            user-select: none;
            -webkit-user-select: none;
        }
        .profile-header-bg.is-editing.has-backdrop:active { cursor: grabbing; }
        .profile-header-bg.is-editing .profile-identity-card,
        .profile-header-bg.is-editing .p-ava-wrap {
            touch-action: pan-y;
            cursor: auto;
        }
        /* While a pan/pinch is active the island melts away so the photo being
           framed is visible; restores on release (iOS poster idiom). The avatar
           stays solid on purpose: it is the anchor people align the photo to. */
        .profile-header-bg .profile-identity-card {
            transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .profile-header-bg.pf-cropping .profile-identity-card {
            opacity: 0.3;
        }

        .p-backdrop-input {
            position: absolute;
            width: 1px;
            height: 1px;
            opacity: 0;
            pointer-events: none;
        }

        .profile-modal-dynamic .pf-edit-ava-badge {
            border-color: var(--bg-tint);
        }

        .comment-header {
            display: flex;
            justify-content: space-between;
            align-items: flex-start; /* Align top */
            margin-bottom: 4px;
        }
        
        .comment-author-wrapper {
            display: flex;
            align-items: center;
            flex-wrap: wrap;
            gap: 4px;
        }
        .badge-modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.6);
            z-index: 3000; /* Highest priority */
            display: flex;
            align-items: center;
            justify-content: center;
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.2s ease;
            padding: 20px;
            padding-top: calc(20px + var(--tg-top, 0px));
        }
        
        .badge-modal-overlay.active {
            opacity: 1;
            pointer-events: auto;
        }

        /* centered dialog = M3 territory: opaque tonal surface, elevation
           shadow only. Rim highlights belong to real glass, not here. */
        .badge-info-card {
            background: var(--bg-tint);
            border: none;
            border-radius: var(--r-island, 24px);
            padding: 32px 24px;
            width: 100%;
            max-width: 320px;
            text-align: center;
            transform: scale(0.9);
            transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
            box-shadow: 0 1px 2px rgb(0 0 0 / 30%), 0 8px 12px 6px rgb(0 0 0 / 15%);
        }
        
        .badge-modal-overlay.active .badge-info-card {
            transform: scale(1);
        }

        .badge-info-icon-wrapper {
            width: 64px;
            height: 64px;
            margin: 0 auto 16px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .badge-info-icon-wrapper svg {
            width: 100%;
            height: 100%;
        }

        .badge-info-title {
            color: var(--text);
            font-size: 20px;
            font-weight: 700;
            margin-bottom: 8px;
        }

        .badge-info-desc {
            color: var(--text2);
            font-size: 15px;
            line-height: 1.5;
            margin-bottom: 20px;
        }

        .badge-info-meta {
            background: var(--glass);
            border-radius: var(--r-btn, 14px);
            padding: 12px;
            margin-top: 12px;
            border: none;
        }

        .badge-info-label {
            color: var(--text3);
            font-size: 12px;
            margin-bottom: 4px;
            font-weight: 600;
        }

        .badge-info-value {
            color: var(--text);
            font-size: 14px;
            font-weight: 500;
        }

        /* счётчик вердиктов в бейдже модератора: подпись слева, число справа */
        .badge-mod-row {
            display: flex;
            align-items: baseline;
            justify-content: space-between;
            gap: 12px;
            text-align: left;
        }
        .badge-mod-row + .badge-mod-row {
            margin-top: 6px;
        }
        .badge-mod-row .badge-info-label {
            margin-bottom: 0;
        }
        .badge-mod-row .badge-info-value {
            font-variant-numeric: tabular-nums;
            font-weight: 700;
        }
        .badge-mod-since {
            margin-top: 8px;
            font-size: 11px;
            color: var(--text3);
            text-align: left;
        }
        .badge-mod-since:empty {
            display: none;
        }

        .badge-close-btn {
            margin-top: 24px;
            background: var(--glass-bold);
            color: var(--text);
            border: none;
            min-height: 48px;
            padding: 0 20px;
            border-radius: var(--r-pill, 999px);
            font-family: inherit;
            font-size: 15px;
            font-weight: 600;
            cursor: pointer;
            width: 100%;
            transition: background 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
        }
        @media (hover: hover) {
            .badge-close-btn:hover { background: var(--glass-border); }
        }
        .badge-close-btn:active { background: var(--glass-border); transform: scale(0.97); }
        .badge-close-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        .comment-author-tag {
            background: var(--accent-soft);
            color: var(--accent-text);
            font-size: 10.5px;
            padding: 3px 8px;
            border-radius: var(--r-pill, 999px);
            margin-left: 6px;
            font-weight: 600;
            border: none;
            vertical-align: middle;
            display: inline-block;
        }
        
        .comment-author-name-creator {
            color: var(--text2);
        }
        .banned-title {
            font-size: 24px;
            font-weight: 600;
            color: var(--destructive);
            margin-bottom: 12px;
        }

        .banned-text {
            font-size: 15px;
            color: var(--text2);
            line-height: 1.6;
        }
        .action-btn.favorited svg {
            fill: currentColor;
        }
        .admin-menu-btn,
        .notification-btn {
            width: 48px;
            height: 48px;
            border-radius: var(--r-pill, 999px);
            background: var(--glass);
            cursor: pointer;
            border: none;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 130ms cubic-bezier(.4,0,.2,1), color 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
        }
        .admin-menu-btn:active,
        .notification-btn:active {
            transform: scale(0.94);
        }

        @media (hover: hover) {
            .admin-menu-btn:hover,
            .notification-btn:hover {
                background: var(--glass-bold);
            }
        }

        .admin-menu-btn svg,
        .notification-btn svg {
            width: 20px;
            height: 20px;
            stroke: var(--text2);
            transition: stroke 0.2s ease;
        }
        
        @media (hover: hover) {
            .admin-menu-btn:hover svg,
            .notification-btn:hover svg {
                stroke: var(--text);
            }
        }
        .notification-badge {
            position: absolute;
            top: 6px;
            right: 6px;
            min-width: 18px;
            height: 18px;
            background: var(--destructive);
            color: #ffffff;
            border-radius: 9px;
            font-size: 11px;
            font-weight: 600;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 0 5px;
            border: 2px solid var(--bg);
        }
        .admin-panel-modal {
            z-index: 2000;
        }

        /* ── Moderation panel: characters and images speak ONE row ──
           Cover, what is being reviewed, who sent it and how long it waited,
           status slot, chevron. Only the data differs between the queues. */
        .mod-section + .mod-section {
            margin-top: 28px;
        }

        /* section header: title + optional bulk action, same row in both queues */
        .mod-sec-head {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            min-height: 36px;
            margin-bottom: 12px;
        }
        .admin-sec-title {
            margin: 0;
            font-variant-numeric: tabular-nums;
        }
        .mod-sec-action {
            margin-top: 0;
            flex: none;
        }
        .auto-mod-status {
            margin: -4px 0 10px;
            font-size: 13px;
            color: var(--text2);
            font-variant-numeric: tabular-nums;
        }

        .mod-list {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        .mod-row {
            display: flex;
            align-items: center;
            gap: 12px;
            width: 100%;
            padding: 10px;
            border: none;
            border-radius: var(--r-card, 18px);
            background: var(--glass);
            color: var(--text);
            font-family: inherit;
            text-align: left;
            cursor: pointer;
            transition: background 130ms cubic-bezier(.4,0,.2,1), opacity 130ms cubic-bezier(.4,0,.2,1);
        }
        .mod-row:active {
            background: var(--glass-bold);
        }
        @media (hover: hover) {
            .mod-row:hover {
                background: var(--glass-bold);
            }
        }
        .mod-row:focus-visible {
            outline: 2px solid var(--accent);
            outline-offset: -2px;
        }
        .mod-row.resolved {
            opacity: 0.55;
        }

        .mod-row-media {
            width: 56px;
            height: 56px;
            flex: none;
            border-radius: 14px;
            object-fit: cover;
            background: var(--glass-bold);
        }
        /* a video post's media carries the clip mark: the wrap is the box the
           mark anchors to, the img keeps its own size and radius */
        .mod-row-media-wrap {
            position: relative;
            flex: none;
            display: flex;
        }
        .mod-row-body {
            flex: 1;
            min-width: 0;
        }
        .mod-row-title {
            font-size: 15px;
            font-weight: 600;
            color: var(--text);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .mod-row-meta {
            display: flex;
            align-items: center;
            flex-wrap: wrap;
            gap: 6px;
            margin-top: 3px;
            min-width: 0;
        }
        .mod-row-sub {
            font-size: 12.5px;
            color: var(--text3);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .mod-row-chev {
            width: 16px;
            height: 16px;
            flex: none;
            color: var(--text3);
        }

        /* tapped row resolves the detail before the modal opens: the spinner
           takes the chevron's slot so the row geometry stays put */
        .mod-row.pending {
            pointer-events: none;
        }
        .mod-row.pending .mod-row-media,
        .mod-row.pending .mod-row-body {
            opacity: 0.55;
        }
        .mod-row.pending .mod-row-chev {
            display: none;
        }
        .mod-row.pending::after {
            content: '';
            width: 16px;
            height: 16px;
            flex: none;
            border: 2px solid color-mix(in srgb, var(--text2) 28%, transparent);
            border-top-color: var(--text2);
            border-radius: 50%;
            animation: actionPendingSpin 700ms linear infinite;
        }

        /* Status slot: the auto-moderation verdict on characters, the report
           count on images. Fixed semantic colors are intentional (staff
           semaphore), and they match the destructive red of the decline button. */
        .mod-chip {
            display: inline-flex;
            align-items: center;
            gap: 5px;
            flex: none;
            padding: 3px 8px;
            border-radius: var(--r-pill, 999px);
            background: var(--glass-bold);
            color: var(--text2);
            font-size: 11.5px;
            font-weight: 600;
            white-space: nowrap;
            font-variant-numeric: tabular-nums;
        }
        .mod-chip svg {
            width: 12px;
            height: 12px;
            flex: none;
        }
        .mod-chip.mute {
            background: var(--glass-bold);
            color: var(--text3);
        }
        .mod-chip.ok {
            background: rgba(52, 199, 89, 0.16);
            color: #34c759;
        }
        .mod-chip.bad {
            background: color-mix(in srgb, var(--destructive) 16%, transparent);
            color: var(--destructive);
        }
        .mod-chip.warn {
            background: rgba(255, 179, 64, 0.16);
            color: #ffb340;
        }

        .auto-mod-spinner {
            animation: autoModSpin 0.8s linear infinite;
            transform-origin: center;
        }

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

        .mod-more {
            display: block;
            margin: 4px auto 0;
        }

        /* Section-level empty / error / no-access state: one component for both
           queues, never a bare line of text under a header. */
        .mod-empty {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 8px;
            padding: 28px 20px;
            border-radius: var(--r-card, 18px);
            background: var(--glass);
            text-align: center;
        }
        .mod-empty-icon {
            width: 26px;
            height: 26px;
            color: var(--text);
            opacity: 0.35;
        }
        .mod-empty-icon svg {
            width: 100%;
            height: 100%;
            display: block;
        }
        .mod-empty-text {
            font-size: 14px;
            color: var(--text2);
        }

        .admin-btn-approve {
            background: var(--accent);
            color: #fff;
            border: none;
        }

        .admin-btn-decline {
            background: color-mix(in srgb, var(--destructive) 12%, var(--glass));
            color: var(--destructive);
            border: none;
        }

        @media (hover: hover) {
            .admin-btn-approve:hover:not(:disabled) {
                background: color-mix(in srgb, var(--accent) 88%, white);
            }
            .form-btn.admin-btn-decline:hover:not(:disabled) {
                background: color-mix(in srgb, var(--destructive) 18%, var(--glass));
            }
        }
        .form-btn.admin-btn-decline:active:not(:disabled) {
            background: color-mix(in srgb, var(--destructive) 24%, var(--glass));
        }

        .notification-btn svg {
            width: 20px;
            height: 20px;
            stroke: var(--text2);
        }


        .notification-modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 20px 24px;
            border-bottom: 1px solid var(--sep);
        }
        #notificationSettingsModal .notification-modal-header {
            position: relative;
            min-height: 72px;
            justify-content: center;
            box-sizing: border-box;
            padding: 14px 60px;
        }
        #notificationSettingsModal .notification-modal-header .modal-name {
            margin: 0;
            text-align: center;
            line-height: 1.15;
        }
        #notificationSettingsModal .notification-settings-close {
            position: absolute;
            top: 50%;
            right: 16px;
            margin: 0;
            transform: translateY(-50%);
        }
        .mark-all-read-btn {
            white-space: nowrap;
            min-height: 40px;
            padding: 0 16px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--glass);
            color: var(--text2);
            cursor: pointer;
            font-family: inherit;
            font-size: 14px;
            font-weight: 600;
            transition: background 130ms cubic-bezier(.4,0,.2,1), color 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
        }

        @media (hover: hover) {
            .mark-all-read-btn:hover:not(:disabled) {
                background: var(--glass-bold);
                color: var(--text);
            }
        }
        .mark-all-read-btn:active:not(:disabled) {
            background: var(--glass-bold);
            color: var(--text);
            transform: scale(0.97);
        }
        .mark-all-read-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        .mark-all-read-btn:disabled {
            display: none;
        }
        .mark-all-read-btn svg {
            display: none;
            width: 21px;
            height: 21px;
        }
        @media (max-width: 420px) {
            #notificationModal .notification-modal-header {
                padding: 14px 16px;
            }
            /* на узких вся модалка переходит на 16px-ось вместе с шапкой */
            #notificationModal .notification-nav-island {
                margin-left: 16px;
                margin-right: 16px;
            }
            #notificationModal .notification-item {
                padding: 16px;
            }
            .mark-all-read-btn:not(:disabled) {
                width: 44px;
                height: 44px;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                padding: 0;
            }
            .mark-all-read-btn span {
                display: none;
            }
            .mark-all-read-btn svg {
                display: block;
            }
        }

        .notifications-list {
            max-height: 60vh;
            overflow-y: auto;
        }

        .notification-nav-island {
            /* ось контента этой модалки 24px (шапка и строки), а не островные 16px */
            margin: 14px 24px 6px;
        }
        .notification-nav-row {
            width: 100%;
            border: 0;
            background: transparent;
            color: var(--text);
            font: inherit;
            text-align: left;
        }
        .notif-prefs-body {
            max-height: min(72vh, 680px);
            overflow-y: auto;
            padding: 0 16px 24px;
        }
        .notif-prefs-intro {
            margin: 14px 0 18px;
            color: var(--text2);
            font-size: 14px;
            line-height: 1.45;
        }
        .notif-pref-island {
            overflow: hidden;
        }
        #notificationSettingsModal .notif-pref-island {
            margin-left: 0;
            margin-right: 0;
        }
        .notif-pref-group-head,
        .notif-pref-row {
            display: grid;
            grid-template-columns: minmax(0, 1fr) 68px 68px;
            align-items: center;
        }
        .notif-pref-group-head {
            align-items: end;
            margin: 0 var(--pad);
            padding: 24px 8px 8px 0;
        }
        #notificationSettingsModal .notif-pref-group-head {
            margin-left: 0;
            margin-right: 0;
        }
        /* grouped-list header, identical to .section-lbl in style.css */
        .notif-pref-group-title {
            color: var(--text2);
            font-size: 13px;
            line-height: 18px;
            font-weight: 600;
            letter-spacing: 0.4px;
            text-transform: uppercase;
            text-align: left;
        }
        .notif-pref-group-channel {
            color: var(--text3);
            font-size: 11px;
            font-weight: 500;
            line-height: 1.15;
            text-align: center;
        }
        .notif-pref-row {
            min-height: 58px;
            padding: 6px 8px 6px 16px;
        }
        /* pseudo hairline, not a border: Chromium rounds a 0.5px border up to
           a full device pixel, a 0.5px pseudo stays half */
        .notif-pref-row { position: relative; }
        .notif-pref-row + .notif-pref-row::before {
            content: '';
            position: absolute;
            left: 16px;
            right: 0;
            top: 0;
            height: 1px;
            background: var(--core-separator);
        }
        .notif-pref-name {
            min-width: 0;
            padding-right: 8px;
            color: var(--text);
            font-size: 14px;
            font-weight: 500;
            line-height: 1.3;
        }
        .notif-pref-channel {
            position: relative;
            min-width: 44px;
            min-height: 44px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            border-radius: var(--r-pill);
        }
        /* nothing grows under the finger (iOS switch): the cell is the target */
        .notif-pref-channel.pending {
            pointer-events: none;
        }
        .notif-pref-channel.pending .switch-thumb {
            opacity: 0;
        }
        .notif-pref-channel.pending .switch-track::after {
            content: '';
            position: absolute;
            width: 14px;
            height: 14px;
            left: 50%;
            top: 50%;
            margin: -9px 0 0 -9px;
            border: 2px solid color-mix(in srgb, var(--text) 28%, transparent);
            border-top-color: var(--text);
            border-radius: 50%;
            animation: actionPendingSpin 700ms linear infinite;
        }
        .notif-prefs-error {
            padding: 28px 16px;
            text-align: center;
            color: var(--text2);
            font-size: 14px;
        }

        @media (max-width: 360px) {
            /* the iOS switch is 64px wide: the narrow-screen columns keep the
               base 68px and the event label takes whatever is left */
            .notif-pref-group-head,
            .notif-pref-row {
                grid-template-columns: minmax(0, 1fr) 68px 68px;
            }
            .notif-prefs-body {
                padding-left: 10px;
                padding-right: 10px;
            }
            .notif-pref-name {
                font-size: 13px;
            }
        }

        .follows-count {
            font-size: 15px;
            color: var(--text2);
            font-variant-numeric: tabular-nums;
            flex-shrink: 0;
        }
        .follows-chevron {
            width: 18px;
            height: 18px;
            color: var(--text3);
            flex-shrink: 0;
        }
        .follow-row-btn {
            min-height: 40px;
            padding: 0 16px;
            border: none;
            border-radius: var(--r-pill, 999px);
            background: var(--glass-bold);
            color: var(--text2);
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
            font-family: inherit;
            flex-shrink: 0;
            align-self: center;
            transition: background 130ms cubic-bezier(.4,0,.2,1), color 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
        }
        @media (hover: hover) {
            .follow-row-btn:hover {
                background: var(--glass-border);
                color: var(--text);
            }
        }
        .follow-row-btn:active {
            background: var(--glass-border);
            color: var(--text);
            transform: scale(0.97);
        }
        .follow-row-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
        .follow-row-btn:disabled { opacity: 0.32; pointer-events: none; }
        .follow-row-btn.refollow {
            background: var(--accent-soft);
            color: var(--accent-text);
        }

        .notification-item {
            display: flex;
            gap: 12px;
            padding: 16px 24px;
            border-bottom: 1px solid var(--sep);
            cursor: pointer;
            transition: background 0.2s;
            animation: slideIn 0.3s ease;
        }

        @media (hover: hover) {
            .notification-item:hover {
                background: var(--glass);
            }
        }
        .notification-item:active {
            background: var(--glass);
        }
        .notification-item:last-child {
            border-bottom: none;
        }

        /* tapped row resolves its subject before the viewer opens */
        .notification-item.pending {
            pointer-events: none;
        }
        .notification-item.pending .notification-content {
            opacity: 0.55;
        }
        .notification-item.pending::after {
            content: '';
            width: 18px;
            height: 18px;
            align-self: center;
            flex-shrink: 0;
            border: 2px solid color-mix(in srgb, var(--text2) 28%, transparent);
            border-top-color: var(--text2);
            border-radius: 50%;
            animation: actionPendingSpin 700ms linear infinite;
        }

        #notificationsList .comment-more-replies-btn {
            display: block;
            margin: 12px auto 0;
        }

        .notification-avatar {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            object-fit: cover;
            flex-shrink: 0;
        }
        .notification-subject-avatar {
            border-radius: 10px;
            background: var(--glass);
        }

        .notification-content {
            flex: 1;
            min-width: 0;
        }

        .notification-text {
            font-size: 14px;
            color: #fff;
            margin-bottom: 4px;
            line-height: 1.4;
        }

        .notification-text .char-name {
            color: var(--accent-text);
            font-weight: 600;
        }

        .notification-preview {
            font-size: 13px;
            color: var(--text3);
            line-height: 1.4;
            overflow: hidden;
            text-overflow: ellipsis;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
            margin-top: 6px;
        }

        .notification-context {
            font-size: 12px;
            color: var(--text3);
            font-style: italic;
            margin-top: 4px;
        }

        .notification-time {
            font-size: 11px;
            color: var(--text3);
            margin-top: 6px;
        }

        .notification-empty {
            text-align: center;
            padding: 60px 20px;
            color: var(--text3);
        }

        .notification-empty-icon {
            font-size: 48px;
            margin-bottom: 12px;
            opacity: 0.5;
        }

        @keyframes slideIn {
            from {
                opacity: 0;
                transform: translateX(-20px);
            }
            to {
                opacity: 1;
                transform: translateX(0);
            }
        }

        /* ── Rules section ── */
        .rules-body { font-size: 15px; line-height: 1.7; color: var(--text2); }
        .rules-body p { margin-bottom: 14px; }
        .rules-body h4 {
            font-size: 16px;
            font-weight: 600;
            color: var(--text);
            margin: 28px 0 12px;
        }
        .rules-body ul {
            margin: 0 0 16px 20px;
            line-height: 1.9;
        }
        .rules-callout {
            padding: 14px 16px;
            border-radius: var(--r-btn, 14px);
            margin-bottom: 16px;
            border: none;
            font-size: 14px;
            line-height: 1.6;
        }
        .rules-callout p { margin: 0; }
        .rules-callout ul { margin: 8px 0 0 20px; line-height: 1.8; }
        .rules-callout.info {
            background: var(--accent-soft);
        }
        .rules-callout.warn {
            background: rgba(255,193,7,0.08);
        }
        .rules-callout.danger {
            background: color-mix(in srgb, var(--destructive) 8%, transparent);
            color: color-mix(in srgb, var(--destructive) 50%, #fff);
        }
        .rules-label {
            font-weight: 600;
            font-size: 14px;
            margin-bottom: 6px;
        }
        .rules-label.ok { color: var(--success); }
        .rules-label.no { color: var(--destructive); }

        body.no-blur .filter-dropdown-menu {
            background: var(--bg-tint) !important;
        }

        /* Creator/admin stats panel */
        .stats-section {
            margin-top: 12px;
        }
        .stats-loading,
        .stats-error,
        .stats-empty {
            text-align: center;
            padding: 24px 12px;
            color: var(--text2);
            font-size: 14px;
        }
        .stats-error {
            color: var(--destructive);
        }

        .stats-overview {
            margin-bottom: 12px;
            padding: 4px;
            border-radius: var(--r-card);
            background: var(--glass);
        }
        .stats-overview-main {
            display: flex;
            align-items: flex-end;
            justify-content: space-between;
            gap: 16px;
            min-width: 0;
            padding: 12px 12px 14px;
        }
        .stats-overview-hero,
        .stats-overview-activity,
        .stats-overview-metric {
            display: flex;
            flex-direction: column;
            min-width: 0;
        }
        .stats-overview-hero-value {
            color: var(--text);
            font-size: 30px;
            font-weight: 700;
            font-variant-numeric: tabular-nums;
            letter-spacing: -0.5px;
            line-height: 1.05;
        }
        .stats-overview-label {
            margin-top: 4px;
            color: var(--text2);
            font-size: 12px;
            font-weight: 500;
            line-height: 1.3;
        }
        .stats-overview-activity {
            align-items: flex-end;
            max-width: 50%;
            text-align: right;
        }
        .stats-overview-activity-value {
            margin-top: 4px;
            color: var(--text);
            font-size: 15px;
            font-weight: 600;
            line-height: 1.3;
        }
        .stats-overview-secondary {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: 16px;
            padding: 12px;
            border-radius: var(--r-btn);
            background: var(--glass-bold);
        }
        .stats-overview-metric-value {
            color: var(--text);
            font-size: 21px;
            font-weight: 700;
            font-variant-numeric: tabular-nums;
            line-height: 1.1;
        }
        .stats-overview-sub {
            margin-top: 4px;
            color: var(--text3);
            font-size: 11px;
            line-height: 1.3;
        }

        .stats-block {
            margin-bottom: 12px;
            padding: 16px;
            border-radius: var(--r-card);
            background: var(--glass);
        }
        .stats-block-title {
            margin: 0 0 14px;
            color: var(--text);
            font-size: 16px;
            font-weight: 600;
            line-height: 1.3;
        }
        .stats-subsection + .stats-subsection {
            margin-top: 24px;
        }
        .stats-subsection-title {
            margin: 0 0 10px;
            color: var(--text2);
            font-size: 13px;
            font-weight: 600;
            line-height: 1.35;
        }
        .stats-subsection-caption {
            margin: 12px 0 8px;
            color: var(--text3);
            font-size: 11px;
            font-weight: 500;
            line-height: 1.35;
        }

        .stats-three-stat {
            display: grid;
            grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: 4px;
            margin-bottom: 12px;
            text-align: center;
        }
        .stats-three-stat > div {
            display: flex;
            flex-direction: column;
            gap: 3px;
            min-width: 0;
        }
        .stats-three-num {
            color: var(--text);
            font-size: 20px;
            font-weight: 700;
            font-variant-numeric: tabular-nums;
            line-height: 1.15;
        }
        .stats-three-label {
            color: var(--text3);
            font-size: 11px;
            line-height: 1.25;
        }

        .stats-meter-row {
            display: grid;
            grid-template-columns: minmax(68px, 92px) minmax(0, 1fr);
            align-items: center;
            gap: 10px;
            margin-bottom: 8px;
        }
        .stats-meter-row:last-child {
            margin-bottom: 0;
        }
        .stats-meter-row.is-stacked,
        .stats-meter-row.is-label-hidden {
            display: block;
        }
        .stats-meter-row.is-stacked {
            margin-bottom: 12px;
        }
        .stats-meter-label {
            overflow: hidden;
            color: var(--text2);
            font-size: 12px;
            font-weight: 500;
            line-height: 1.3;
            white-space: nowrap;
            text-overflow: ellipsis;
        }
        .stats-meter-row.is-stacked .stats-meter-label {
            margin-bottom: 6px;
        }
        .stats-meter-track {
            position: relative;
            min-width: 0;
            height: 24px;
            overflow: hidden;
            border-radius: var(--r-btn);
            background: var(--glass-bold);
        }
        .stats-meter-fill {
            position: absolute;
            inset: 0 auto 0 0;
            border-radius: inherit;
            background: var(--accent-soft);
            transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1);
        }
        .stats-meter-row.is-secondary .stats-meter-fill {
            opacity: 0.5;
        }
        .stats-meter-value {
            position: relative;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
            height: 100%;
            padding: 0 10px;
            color: var(--text);
            font-size: 11px;
            font-variant-numeric: tabular-nums;
            font-weight: 500;
            line-height: 1;
            white-space: nowrap;
        }
        .stats-meter-row.is-secondary .stats-meter-value {
            color: var(--text2);
        }
        .stats-meter-pct {
            margin-left: auto;
            color: var(--text2);
            font-weight: 600;
        }

        .stats-dual-row {
            margin-bottom: 14px;
        }
        .stats-dual-row:last-child {
            margin-bottom: 0;
        }
        .stats-dual-name {
            margin-bottom: 6px;
            overflow: hidden;
            color: var(--text2);
            font-size: 12px;
            font-weight: 600;
            line-height: 1.3;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .stats-dual-bars {
            display: flex;
            flex-direction: column;
            gap: 4px;
        }
        .stats-dual-bars .stats-meter-row {
            margin-bottom: 0;
        }

        /* the audience fold nests on the expanded stats card (glass-bold),
           where the shared fold tones vanish (glass-bold on glass-bold) or
           invert. Pin the card to --glass in every state: collapsed reads as
           a well strip, expanded becomes the same full-width well as the
           sibling stats blocks (their tone, radius and 16px inset) */
        .stats-audience-fold,
        .stats-audience-fold:hover,
        .stats-audience-fold.expanded {
            background: var(--glass);
            border-radius: var(--r-card);
        }
        .stats-audience-fold > .fold-pick-head {
            padding: 12px 16px;
        }
        .stats-audience-fold .fold-pick-body-inner {
            padding: 0 16px 16px;
        }
        /* the fold card IS the well, the lone block inside sheds its own
           card chrome (a card-in-card would invert the tone order and
           shrink the meters vs the sibling sections) */
        #statsDetails > .stats-block {
            margin-bottom: 0;
            padding: 0;
            border-radius: 0;
            background: none;
        }
        .stats-context-note {
            margin-bottom: 12px;
            padding: 12px 14px;
            border-radius: var(--r-btn);
            background: var(--accent-soft);
        }
        .stats-context-title {
            margin: 0;
            color: var(--accent-text);
            font-size: 14px;
            font-weight: 600;
            line-height: 1.35;
        }
        .stats-context-body {
            margin: 5px 0 0;
            color: var(--text2);
            font-size: 13px;
            line-height: 1.45;
        }

        .stats-chart-wrap {
            position: relative;
            height: 160px;
            margin: 0 -4px;
        }

        @media (max-width: 360px) {
            .stats-overview-main {
                align-items: flex-start;
                gap: 12px;
            }
            .stats-overview-activity {
                max-width: 48%;
            }
            .stats-block {
                padding: 14px;
            }
            .stats-meter-row {
                grid-template-columns: 72px minmax(0, 1fr);
                gap: 8px;
            }
        }

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

        /* Admin moderation mode: hide community-facing sections for pending chars */
        #characterModal.modal-admin-mode .modal-stats,
        #characterModal.modal-admin-mode .modal-actions,
        #characterModal.modal-admin-mode #statsSection,
        #characterModal.modal-admin-mode #openBotBtn,
        #characterModal.modal-admin-mode #deleteBtn,
        #characterModal.modal-admin-mode #similarSection,
        #characterModal.modal-admin-mode .comments-section { display: none !important; }

/* ═══════════════════════════════════════════
   DESKTOP LAYOUT
   Gated on html.layout-desktop (set by JS in app.js from width OR the manual
   Settings override), in sync with style.css. Uses CSS nesting.
═══════════════════════════════════════════ */
html.layout-desktop {

  /* Community overlay sits to the right of the sidebar */
  #community-root {
    left: calc(var(--sidebar-w) + var(--sidebar-gap) * 2) !important;
    /* toolbar floats 12px down + 60px tall, content clears the bottom edge */
    padding-top: 72px !important;
  }

  /* .c-toolbar is position:fixed (viewport-relative), so it can't inherit the
     overlay's left offset — set it here. The bar itself is invisible (separate
     floating chips), only its children render. */
  #community-root .c-toolbar {
    top: 12px;
    left: calc(var(--sidebar-w) + var(--sidebar-gap) * 2 + 12px);
    right: 12px;
  }

  /* Fade spans the full column (covers the island's 12px side + top gaps).
     Taller than mobile so the % gradient clears the island that floats 12px down. */
  #community-root .c-toolbar-fade {
    left: calc(var(--sidebar-w) + var(--sidebar-gap) * 2);
    height: 112px;
  }

  /* Modals: from bottom-sheet to a centered floating card.
     .modal-content is already max-width:600px and centered; restore all-corner
     radius + bottom border and slide-from-bottom becomes a small lift. */
  .modal.active { align-items: center; }
  .modal-content {
    border-radius: var(--r-island);
    border-bottom: 1px solid var(--glass-border);
    max-height: 86vh;
    transform: translateY(24px);
  }
  .modal.active .modal-content { transform: translateY(0); }
  /* десктопная пара к @starting-style выше: здесь подъём короткий */
  @starting-style {
    .modal.active .modal-content { transform: translateY(24px); }
  }
  /* keep the post viewer's definite height in step with the desktop card cap
     so the pager height chain resolves without overflowing the centered dialog */
  #postModal > .modal-content { height: 86vh; }

  /* Fullscreen image lightbox: centre the photo in the free column right of the
     sidebar (same idea as the modal padding shim) while the dim+blur backdrop
     still covers everything. Drop it below the sidebar (z 4000; community modals
     cap at 3800) so the sidebar draws over it like it does over modals, instead
     of the lightbox covering it. The slides span the viewport, so padding-left on
     each slide re-centres its image into the content column; the prev arrow and
     counter follow so they don't sit over the sidebar gutter. */
  .char-lightbox { z-index: 3900; }
  /* Летящий кадр обязан лежать на том же слое, что и просмотрщик, в который он
     летит: иначе на десктопе он проходит ПОВЕРХ боковой панели, а приехав,
     ныряет под неё. Ступень выше модалок (3800) и та же, что у просмотрщика. */
  .post-zoom { z-index: 3900; }
  .char-lightbox-slide { padding-left: calc(var(--sidebar-w) + var(--sidebar-gap) * 2); }
  .char-lightbox-arrow-prev { left: calc(var(--sidebar-w) + var(--sidebar-gap) * 2 + 10px); }
  .char-lightbox-tools { left: calc(50% + (var(--sidebar-w) + var(--sidebar-gap) * 2) / 2); }
  .char-lightbox-counter { left: calc(50% + (var(--sidebar-w) + var(--sidebar-gap) * 2) / 2); }

  /* Overlays deliberately stay full-bleed (base .modal): the dim+blur must
     cover EVERYTHING except the sidebar, which draws above them (z 4000,
     style.css) and stays clickable — same convention as the app-side
     .modal-overlay sheets. The card is shifted into the free column by the
     index.html padding shim. The old z race (the +10 ratchet crossing the
     sidebar after ~10 opens, which v5.36.0 dodged by stopping the overlay at
     the sidebar edge = a visible hard blur boundary) is closed by capping
     _nextCommunityZIndex below the sidebar. */

  /* ── Community desktop pass (v5.36.0) ──────────────────────────────
     Catalog density, toolbar rework and the character modal two-pane.
     COMMUNITY_ONLY mode never gets html.layout-desktop, so none of this
     touches /app?mode=community. */

  /* Catalog: column count follows the viewport (~300px cards) instead of
     capping at 4 giant columns on wide screens */
  #community-root .cards-container {
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 16px;
  }

  /* Search: a centered glass pill inside the fixed top chip strip
     (moved there by _communityApplyLayout in community.js). Verbatim
     glass recipe; no-blur fallback right after this block. */
  #community-root .c-toolbar .search-container {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: min(520px, 42vw);
    height: 40px;
  }
  #community-root .c-toolbar .search-input {
    height: 40px;
    padding-top: 0;
    padding-bottom: 0;
    font-size: 14px;
    background: var(--chrome-glass-fill-readable);
    backdrop-filter: var(--chrome-glass-filter-readable);
    -webkit-backdrop-filter: var(--chrome-glass-filter-readable);
    box-shadow: var(--chrome-glass-shadow);
  }
  /* focus never swaps the material (ADR-0011): the field is shell chrome */
  #community-root .c-toolbar .search-input:focus {
    background: var(--chrome-glass-fill-readable);
  }
  #community-root .c-toolbar .search-clear { top: 0; }
  #community-root .c-toolbar .search-progress { top: 13px; }

  /* Controls row: pager centered, sort + filter right, all on ONE line.
     The 1fr/auto/1fr grid keeps the pager truly centered while there is
     room; when the window gets narrow the left 1fr collapses first, so the
     pager drifts left instead of overlapping the right cluster (graceful,
     nothing breaks). _communityApplyLayout moves #topPagination in here and
     groups sort + filter into .controls-right. */
  #community-root .controls-wrapper {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 12px;
  }
  #community-root .controls-wrapper .pagination {
    grid-column: 2;
    margin: 0;
  }
  #community-root .controls-right {
    grid-column: 3;
    justify-self: end;
    display: flex;
    align-items: center;
    gap: 12px;
  }
  #community-root .sort-btn { flex: 0 0 auto; }
  #community-root .filter-dropdown-btn { width: 44px; height: 44px; }
  /* art gallery: the mode switch lives in the grid wrapper; pin it left
     (auto-placement drifted it into the right column). In gallery mode every
     row sits on ONE centered 1024px axis: switch, sort row and the grid. */
  #community-root .c-mode-switch { grid-column: 1; grid-row: 1; justify-self: start; margin: 0; }
  #community-root.gallery-mode .c-mode-switch {
    grid-column: 1 / -1;
    justify-self: center;
    width: 100%;
    max-width: 1024px;
  }
  /* gallery sort mirrors the catalog's desktop controls row: the hug-width
     strip sits at the right, on the line the catalog puts its sort into */
  #gallerySection .g-tabs { margin-left: auto; }

  /* Author/own profile modal: the characters grid adapts to the modal
     width instead of the hardcoded 2 columns */
  .profile-chars-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  }

}

/* No-blur fallback for the desktop toolbar search pill (glass elements
   must degrade to a solid tonal fill) */
html.layout-desktop body.no-blur #community-root .c-toolbar .search-input {
  background: var(--bg-tint);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: 0 1px 2px rgb(0 0 0 / 30%), 0 4px 8px 3px rgb(0 0 0 / 15%);
}
html.layout-desktop body.no-blur #community-root .c-toolbar .search-input:focus {
  background: var(--glass-bold);
}

/* ═══════════════════════ Art gallery (ADR-0009) ═══════════════════════
   Community root mode 'art'. Content follows M3 tonal rules (tone steps,
   no border strokes, accent only for selection); over-art chips are real
   glass (verbatim recipe) with body.no-blur solid fallbacks. */

.c-mode-switch { margin-bottom: 4px; }
/* Full app: characters/gallery switching lives in the bottom nav (two root
   tabs over one overlay), so the in-page switch renders only in
   /app?mode=community where there is no navbar. In gallery mode the wrapper
   would hold nothing but hidden rows - drop it so the feed starts on the
   catalog's top rhythm. */
body:not(.community-only) #cModeSwitch { display: none; }
body:not(.community-only) #community-root.gallery-mode .controls-wrapper { display: none; }
body:not(.community-only) #gallerySection { margin-top: 0; padding-top: 12px; }

/* controls-wrapper keeps its 24px catalog rhythm; in art mode only the mode
   switch is visible there, so pull the gallery up to the standard 12px gap.
   The 12px side padding mirrors .controls-wrapper/.cards-container (the
   index.html shim) so every gallery row sits on the catalog's left axis. */
#gallerySection { margin-top: -12px; padding: 0 12px; }

/* Sort tabs: the chat-list folder idiom (34px text tab + accent-soft pill
   inside a capsule strip, mirrors .folder-bar), shared by the gallery
   (#gSortGroup) and the catalog (#charSortContainer). The pill is positioned
   imperatively (_layoutTabsInd) since labels are localized; anchor for the
   period pull-down. */
.g-tabs {
    position: relative;
    /* The strip itself must outrank the grid: tiles are position:relative and
       their chips reach z-5; fixed top fades start at z-15. Keep the glass on
       ::before instead of this ancestor: a backdrop-filter ancestor becomes a
       Backdrop Root and prevents the period menu from sampling the cards below. */
    z-index: 10;
    display: flex;
    align-items: center;
    gap: 3px;
    height: 42px;
    padding: 4px;
    margin-bottom: 8px;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    border-radius: var(--r-pill);
}
.g-tabs::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    border-radius: inherit;
    /* same glass capsule as the chats folder bar (owner-ratified strip) */
    background: var(--chrome-glass-fill);
    backdrop-filter: var(--chrome-glass-filter);
    -webkit-backdrop-filter: var(--chrome-glass-filter);
    box-shadow: var(--chrome-glass-shadow);
}
/* no-blur: solid fill, shadows (rims incl.) stay - the app-wide island rule */
body.no-blur .g-tabs::before {
    background: var(--bg-tint);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}
/* Inner scroller, folder-bar semantics: few tabs stretch to fill, many
   enable a horizontal scroll (RU labels overflow 4 tabs under ~400px).
   Deliberately NOT overflow on .g-tabs itself: the period reveal lives
   there and a scroll container would clip it. Positioned so the sliding
   pill and the tabs share one scrolled coordinate space. */
.g-tabs-scroll {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: 3px;
    flex: 1;
    min-width: 0;
    height: 100%;
    overflow-x: auto;
    scrollbar-width: none;
}
.g-tabs-scroll::-webkit-scrollbar { display: none; }
/* catalog placement: .controls-wrapper's own gap (and .controls-right's
   center alignment on desktop) handle the spacing */
.controls-wrapper .g-tabs { margin-bottom: 0; }
/* desktop rows hug their content (catalog: controls-right next to search;
   gallery: right-aligned above the grid) - a full-width strip only makes
   sense on the narrow column */
html.layout-desktop #community-root .g-tabs { width: fit-content; }
.g-tab {
    position: relative;
    z-index: 1;
    flex: 1 1 0;
    min-width: max-content;
    justify-content: center;
    height: 34px;
    padding: 0 16px;
    display: inline-flex;
    align-items: center;
    border: none;
    background: transparent;
    border-radius: var(--r-pill);
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    color: var(--text2);
    white-space: nowrap;
    cursor: pointer;
    transition: color 130ms cubic-bezier(0.4, 0, 0.2, 1);
}
.g-tab:active { color: var(--text); }
.g-tab.active { color: var(--accent-text); }
/* caret morphs in on the active Top tab (same pull-down affordance as the
   catalog's Popular segment; base .sort-caret is width:0/opacity:0) */
.g-tab .sort-caret { height: 14px; }
.g-tab.active .sort-caret { width: 14px; margin-left: 3px; opacity: 1; }
.g-tabs-ind {
    position: absolute;
    /* inside .g-tabs-scroll (34px content box), not the padded 42px strip */
    top: 0;
    left: 0;
    width: 0;
    height: 34px;
    border-radius: var(--r-pill);
    /* the shell's selection lens: a neutral capsule, the tint goes on the tab
       label (--accent-text), same as the folder strip */
    background: var(--chrome-lens-fill);
    pointer-events: none;
    visibility: hidden;
    /* matches the main .nav-indicator's timing (style.css) - same kind of
       element, sliding pill under tabs, should move at the same speed */
    transition: left 0.3s cubic-bezier(.32,.72,.35,1.1), width 0.3s cubic-bezier(.32,.72,.35,1.1);
}
/* view prefs chip lives in the fixed toolbar, gallery mode only. The extra
   .c-tool-btn is load-bearing: index.html's inline styles set the same
   specificity and load after this file, so a bare rule loses the tie. */
#community-root .c-tool-btn.g-eye-tool { display: none; }
#community-root.gallery-mode .c-tool-btn.g-eye-tool { display: flex; }

/* funnel chips (BOTH surfaces): they share ONE toolbar slot - the catalog
   funnel shows in catalog mode, the gallery funnel in gallery mode - so the
   control never moves between the sibling screens. Badge positioned for the
   40px toolbar chip; active filters tint the chip tonally (the badge alone
   undersells the state). */
#community-root .c-tool-btn .filter-badge { top: -2px; right: -2px; }
#community-root .c-tool-btn.c-funnel.active { background: var(--accent-soft-solid); }
#community-root .c-tool-btn.c-funnel.active svg { stroke: var(--accent-text); }
/* catalog funnel: catalog mode only (the mirror gate of .g-filter-anchor) */
#community-root .c-funnel-anchor { display: block; position: relative; }
#community-root.gallery-mode .c-funnel-anchor { display: none; }
.c-funnel-anchor .filter-menu-reveal {
    position: fixed;
    top: calc(56px + var(--tg-top, 0px));
    right: 12px;
    left: auto;
    width: min(340px, calc(100vw - 24px));
}
html.layout-desktop #community-root .c-funnel-anchor .filter-menu-reveal { top: 68px; }

/* search chip: fades in once the in-flow search field has scrolled away
   (hdr-scrolled is flipped by a scroll threshold in community.js). Catalog
   only - the gallery has no search; desktop keeps the always-on toolbar
   field. */
#community-root .c-tool-btn.c-search-chip { opacity: 0; visibility: hidden; transition: opacity 150ms cubic-bezier(0.4,0,0.2,1), visibility 150ms; }
#community-root.hdr-scrolled:not(.gallery-mode) .c-tool-btn.c-search-chip { opacity: 1; visibility: visible; }
html.layout-desktop #community-root .c-tool-btn.c-search-chip { display: none; }
/* desktop toolbar floats 12px down, so the fixed popover drops with it */
html.layout-desktop #community-root .g-filter-anchor .g-filter-reveal { top: 68px; }

/* Masonry: JS-balanced columns; tiles reserve height from stored aspect
   ratios so the feed never shifts while thumbs load */
.g-grid {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 0 4px 24px;
}
.g-col {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.g-tile {
    position: relative;
    width: 100%;
    border-radius: var(--r-card, 18px);
    overflow: hidden;
    background: var(--glass);
    cursor: pointer;
    transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
}
.g-tile:active { transform: scale(0.98); }
.g-tile-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0;
    transition: opacity 250ms cubic-bezier(0.05, 0.7, 0.1, 1), filter 200ms ease, transform 200ms ease;
}
.g-tile.loaded .g-tile-img { opacity: 1; }
.g-tile.g-blur .g-tile-img { filter: blur(24px) saturate(1.1); transform: scale(1.12); }

.g-blur-ov {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    background: rgba(0, 0, 0, 0.25);
    transition: opacity 200ms ease;
}
.g-rating-chip {
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.4px;
    padding: 4px 10px;
    border-radius: 999px;
}
.g-blur-hint { font-size: 12px; color: rgba(255, 255, 255, 0.85); }

/* Over-art like counter: placement only; material comes from .stat-chip */
.g-like-chip {
    position: absolute;
    left: 8px;
    bottom: 8px;
}
/* Clip mark: play glyph + length, the media archive's .ci-badge recipe in the
   free corner (the like chip and the author chip own the left ones) */
.g-vid-badge {
    position: absolute;
    right: 8px;
    bottom: 8px;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 3px;
    padding: 2px 6px 2px 4px;
    border-radius: var(--r-pill, 999px);
    background: rgba(0, 0, 0, 0.55);
    font-size: 11px;
    color: #fff;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
}
.g-vid-badge svg { width: 9px; height: 9px; display: block; }
.g-vid-badge.bare { padding: 4px 5px; }
/* the same mark on a small thumbnail (56px mod row media, 44px bell thumb):
   glyph only, tucked 4px into the corner */
.g-vid-badge.mini { right: 4px; bottom: 4px; padding: 3px 4px; }
.g-vid-badge.mini svg { width: 8px; height: 8px; }
.notification-post-thumb-wrap { position: relative; display: inline-flex; margin-top: 6px; }
.notification-post-thumb-wrap .notification-post-thumb { margin-top: 0; }

/* Moderation status chip, SHARED between gallery tiles and catalog cards:
   one neutral scrim pill, never a colored banner (bright pills + colored
   borders are banned; the old .card-status-overlay was both) */
.g-status-chip {
    position: absolute;
    top: 8px;
    left: 8px;
    right: 8px;
    width: fit-content;
    max-width: calc(100% - 16px);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    background: var(--chrome-glass-fill-media);
    backdrop-filter: var(--chrome-glass-filter-media);
    -webkit-backdrop-filter: var(--chrome-glass-filter-media);
    box-shadow: var(--chrome-glass-shadow-thin);
    color: #fff;
    font-size: 11px;
    font-weight: 600;
    padding: 3px 8px;
    border-radius: 999px;
    z-index: 5;
}
/* on catalog cards the top-left corner belongs to the author chip */
.card-status-chip {
    top: 8px;
    right: 8px;
    left: auto;
}

.g-skel {
    background: linear-gradient(100deg, var(--glass) 40%, var(--glass-bold) 50%, var(--glass) 60%);
    background-size: 200% 100%;
    animation: gShimmer 1.4s ease infinite;
}
@keyframes gShimmer {
    from { background-position: 120% 0; }
    to { background-position: -80% 0; }
}

.g-empty {
    text-align: center;
    color: var(--text3);
    font-size: 14px;
    padding: 48px 24px;
    line-height: 1.5;
}
/* filtered dead-end reuses the catalog empty-state anatomy; the reset button
   is the visible exit (a filter trap with no exit reads as a broken feed) */
.g-empty .empty-state { padding: 0; }
/* shared by the gallery AND catalog filtered-empty reset actions */
.g-empty-reset { margin: 16px auto 0; display: block; min-width: 180px; }
.g-loading { display: flex; justify-content: center; padding: 32px; }
.g-sentinel { height: 1px; }

/* ── Gallery feed filters (toolbar funnel popover + picker sheets) ── */

/* anchor: a filter-free direct toolbar child; the glass chip and the glass
   popover are SIBLINGS inside it (ADR-0014: no filtered ancestor over the
   reveal). Gated per mode like the eye chip. */
#community-root .g-filter-anchor { display: none; }
#community-root.gallery-mode .g-filter-anchor { display: block; position: relative; }
/* the funnel is not the LAST toolbar chip, so right-aligning to it would walk
   a 320px panel off the left edge; both the toolbar and the panel are fixed,
   so pin the panel to the viewport pad under the chip row instead */
.g-filter-anchor .g-filter-reveal {
    position: fixed;
    top: calc(56px + var(--tg-top, 0px));
    right: 12px;
    left: auto;
    width: min(320px, calc(100vw - 24px));
}
.g-filter-popover { max-height: 70vh; }
/* the shared shell pads its inner box for the catalog's chip cloud; this
   popover speaks the pull-down dialect and its rows need the raw edge */
.g-filter-popover .filter-dropdown-menu-inner { padding: 0; }

/* Popover content = the canonical iOS glass-menu dialect (chat-dropdown /
   sort-period grammar): full-bleed rows on ONE 16px text axis, hairlines
   between neighbours, thick .chat-dropdown-sep bands between groups. The
   M3 seg-group that carried orientation until v5.205 read as an opaque
   slab over live blur - the boundary rule keeps seg-groups on tonal
   opaque surfaces; on glass a choose-one is check rows (the sort-period
   pull-down precedent). */

/* summary row: label + value; the trailing slot is the nav chevron (no value)
   XOR the adjacent clear X (value set) - never both, and no invisible
   reserved ghost (rejected in owner review as a layout hole) */
.gf-row-wrap {
    position: relative;
    display: flex;
    align-items: center;
    min-height: 48px;
}
.gf-row-wrap + .gf-row-wrap::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 1px;
    background: rgba(255, 255, 255, 0.06);
}
.gf-row {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px;
    min-height: 48px;
    border: none;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 15px;
    line-height: 1.2;
    text-align: left;
    cursor: pointer;
}
.gf-row:active { background: rgba(255, 255, 255, 0.07); }
.gf-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.gf-row-lbl { flex: none; }
.gf-row-val {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    color: var(--text2);
}
.gf-row-val.is-any { color: var(--text3); }
.gf-row-val-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gf-row-ava {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    flex: none;
    object-fit: cover;
    display: block;
}
.gf-row-chev { width: 16px; height: 16px; flex: none; stroke: var(--text); opacity: 0.45; }
.gf-x {
    width: 44px;
    height: 44px;
    flex: none;
    /* centers the glyph on the chevron slot (16px pad + half a 16px glyph) */
    margin-right: 2px;
    border: none;
    background: transparent;
    color: var(--text);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    border-radius: var(--r-pill, 999px);
    transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* X glyph contract: solid path color dimmed by whole-SVG opacity - an
   alpha-bearing stroke doubles where the two lines cross (slop checklist) */
.gf-x svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.2; stroke-linecap: round; opacity: 0.55; }
.gf-x:active { background: rgba(255, 255, 255, 0.10); transform: scale(0.92); }
.gf-x:active svg { opacity: 1; }
.gf-x:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* orientation: check rows (leading check slot + label), grouped under a
   small section label. Section-title carries its own padding - zero it so
   the label sits on the shared 16px axis. */
.gf-orient-lbl { margin: 10px 16px 2px; padding: 0; }
.gf-check-row {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 12px 16px;
    min-height: 44px;
    border: none;
    background: none;
    font: inherit;
    font-size: 15px;
    font-weight: 400;
    line-height: 1.2;
    color: var(--text);
    text-align: left;
    cursor: pointer;
}
.gf-check-row::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: rgba(255, 255, 255, 0.06);
}
.gf-orient-group .gf-check-row:last-child::after { display: none; }
/* the bookmark toggle: a summary-shaped row whose trailing slot holds the
   check (same morph as the orientation rows) where the facet rows keep their
   chevron */
.gf-saved-row.active .gf-check { opacity: 1; transform: none; }
.gf-check-row:active { background: rgba(255, 255, 255, 0.07); }
.gf-check-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* the check morphs in (opacity+scale, standard 130ms): sort-period swaps
   its check instantly because that menu closes on pick, but this popover
   stays open for live composition, so the state change must visibly settle */
.gf-check {
    width: 18px;
    height: 18px;
    flex: none;
    opacity: 0;
    transform: scale(0.6);
    transition: opacity 130ms cubic-bezier(0.4, 0, 0.2, 1),
                transform 130ms cubic-bezier(0.4, 0, 0.2, 1);
}
.gf-check-row.active .gf-check { opacity: 1; transform: none; }

/* reset: a full-bleed iOS action row after its own group band, not a
   centered 2016-dialog text button */
.gf-reset {
    width: 100%;
    border: none;
    background: transparent;
    color: var(--text2);
    font: inherit;
    font-size: 15px;
    line-height: 1.2;
    min-height: 44px;
    cursor: pointer;
    text-align: left;
    padding: 12px 16px;
}
.gf-reset:active { background: rgba(255, 255, 255, 0.07); }
.gf-reset:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* in the catalog panel the same action sits inset on THAT panel's padded
   14px axis, rounded like its neighbours */
#filterDropdownMenu .gf-reset { padding: 0 6px; font-size: 14px; border-radius: var(--r-btn, 14px); }
@media (hover: hover) {
    .gf-row:hover, .gf-check-row:hover, .gf-reset:hover { background: rgba(255, 255, 255, 0.07); }
    .gf-x:hover { background: rgba(255, 255, 255, 0.07); }
    .gf-x:hover svg { opacity: 1; }
}

/* style picker rows: preview + name + m3 radio (radio-list grammar; the
   preview well reserves its box so image loads never shift rows) */
.gstyle-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 6px;
    border-radius: var(--r-btn, 14px);
    cursor: pointer;
    width: 100%;
    border: none;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: left;
}
@media (hover: hover) {
    .gstyle-row:hover { background: var(--glass); }
}
.gstyle-row:active { background: var(--glass-bold); }
.gstyle-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.gstyle-preview {
    width: 44px;
    height: 44px;
    border-radius: 10px;
    flex: none;
    object-fit: cover;
    display: block;
    background: var(--glass-bold);
}
.gstyle-preview-any {
    display: flex;
    align-items: center;
    justify-content: center;
}
.gstyle-preview-any svg { width: 20px; height: 20px; stroke: var(--text); opacity: 0.55; fill: none; }
.gstyle-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 15px;
    color: var(--text);
}
.gstyle-row .m3-radio { flex: none; }
.gstyle-pick-list { height: min(60vh, 520px); }

/* picker sheets sit at the bottom like the rest of the sheet family
   (mirrors .char-share-modal.active) */
.gchar-pick-modal.active {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding: 0 12px calc(20px + var(--safe-bottom, 0px));
}
.gchar-pick-modal .modal-sheet { display: flex; flex-direction: column; }
.gchar-search { margin: 12px 0 4px; flex: none; }
.gchar-pick-list {
    overflow-y: auto;
    /* FIXED height, not max: the sheet is bottom-anchored, so a list that
       shrinks with the result count would teleport the search field down
       ~340px between keystrokes (ADR-0011: loading must not reflow the row) */
    height: min(52vh, 460px);
    scrollbar-width: none;
    margin: 0 -4px;
    padding: 4px;
}
.gchar-pick-list::-webkit-scrollbar { display: none; }
.gchar-pick-list.soft-loading { opacity: 0.5; pointer-events: none; }
.gchar-pick-list { transition: opacity 130ms cubic-bezier(0.4, 0, 0.2, 1); }
.gf-pick-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 6px;
    border-radius: var(--r-btn, 14px);
    cursor: pointer;
    width: 100%;
    border: none;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: left;
}
@media (hover: hover) {
    .gf-pick-row:hover { background: var(--glass); }
}
.gf-pick-row:active { background: var(--glass-bold); }
.gf-pick-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.gf-pick-row .gf-ava {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    flex: none;
    object-fit: cover;
    display: block;
}
.gf-pick-row .gf-nm {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 15px;
    color: var(--text);
}
.gf-pick-row .gf-cnt {
    color: var(--text3);
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    flex: none;
}
.gf-pick-row .m3-radio { flex: none; }
.gchar-pick-empty {
    text-align: center;
    color: var(--text3);
    font-size: 14px;
    padding: 32px 16px 24px;
    line-height: 1.5;
    overflow-wrap: anywhere;
}

/* character modal → gallery entry: a nav row in the fold-pick card anatomy
   (same glass, radius, 12/14 head padding, 14/500 title, 12/500 tabular
   value) - it sits stacked with the Промпт/Статистика folds and must read as
   one family. No leading icon: no row in this modal carries one. The chevron
   points right, not down (it leaves the modal instead of expanding). The
   grid wrapper is the house auto-height arrival: the detail payload lands
   async, a row popping in would shift the whole modal body */
.char-gallery-reveal {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 250ms cubic-bezier(0.05, 0.7, 0.1, 1);
}
.char-gallery-reveal.open { grid-template-rows: 1fr; }
.char-gallery-reveal-in { overflow: hidden; min-height: 0; }
@media (prefers-reduced-motion: reduce) {
    .char-gallery-reveal { transition: none; }
}
.char-gallery-row {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--glass);
    border: none;
    border-radius: var(--r-btn, 14px);
    padding: 12px 14px;
    min-height: 44px;
    box-sizing: border-box;
    color: var(--text);
    font: inherit;
    font-size: 14px;
    cursor: pointer;
    text-align: left;
    transition: background 130ms cubic-bezier(0.4, 0, 0.2, 1);
}
@media (hover: hover) {
    .char-gallery-row:hover { background: var(--glass-bold); }
}
.char-gallery-row:active { background: var(--glass-bold); }
.char-gallery-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.char-gallery-row .char-gallery-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; line-height: 1.35; }
.char-gallery-row .char-gallery-count { color: var(--text3); flex: none; font-size: 12px; font-weight: 500; white-space: nowrap; font-variant-numeric: tabular-nums; }
.char-gallery-row .char-gallery-chev { width: 16px; height: 16px; stroke: var(--text); opacity: 0.45; flex: none; }

@media (min-width: 900px) {
    /* wide viewports without the desktop layout: align every gallery row to
       one centered 1024px column (segments stretched edge-to-edge look broken) */
    #community-root .c-mode-switch { width: 100%; max-width: 1024px; margin-left: auto; margin-right: auto; }
    /* the grid fills the viewport like the catalog does; the column count
       adapts in JS (_galleryColTarget), so no width cap here */
    .g-grid { gap: 12px; }
    .g-col { gap: 12px; }
}

/* ── Post viewer ── */

/* Horizontal doomscroll pager: 3-pane track (prev · center · next). The center
   pane is the real interactive post (all its handlers untouched); the sides are
   throwaway ghosts filled on drag start. Mirrors the lightbox carousel model. */
.post-pager {
    position: relative;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}
.pager-track {
    display: flex;
    height: 100%;
    width: 300%;
    transform: translate3d(-33.3333%, 0, 0);
    will-change: transform;
}
.pager-track.animating {
    transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}
.pager-track > .post-pane {
    flex: 0 0 33.3333%;
    height: 100%;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    touch-action: pan-y;
    /* hide the scrollbar: a ghost (no comments) is not scrollable but the
       settled centre is, so a visible bar would pop in on every swipe. Touch
       drag / wheel still scroll. */
    scrollbar-width: none;
}
.pager-track > .post-pane::-webkit-scrollbar { display: none; }
.post-pane-side {
    overflow: hidden;
    pointer-events: none;
}
/* The centre pane is one persistent DOM node. Entity adoption happens while
   the committed neighbour ghost is visible, so state changes must be atomic
   before the track snaps home. */
.post-pane-center.post-entity-swap .action-btn,
.post-pane-center.post-entity-swap .post-hero-img,
.post-pane-center.post-entity-swap .post-hero-video,
.post-pane-center.post-entity-swap .fold-pick,
.post-pane-center.post-entity-swap .fold-pick-chevron,
.post-pane-center.post-entity-swap .fold-pick-body-wrap {
    transition: none !important;
}
@media (prefers-reduced-motion: reduce) {
    .pager-track.animating { transition: transform 0.01s linear; }
}

/* Pager controls belong to the overlay, not to the post's content. Touch and
   narrow windows page by swipe; wide pointer layouts get external controls
   only when there is enough room beside the 600px card. */
.post-pager-arrow {
    display: none;
    position: fixed;
    top: 50%;
    transform: translateY(-50%);
    width: 40px;
    height: 40px;
    z-index: 10;
    border: none;
    border-radius: var(--r-pill, 999px);
    background: var(--chrome-glass-fill);
    backdrop-filter: var(--chrome-glass-filter);
    -webkit-backdrop-filter: var(--chrome-glass-filter);
    box-shadow: var(--chrome-glass-shadow-thin);
    color: #fff;
    cursor: pointer;
    align-items: center;
    justify-content: center;
    transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1), background 130ms cubic-bezier(0.4, 0, 0.2, 1), scale 120ms ease-out;
}
.post-pager-arrow.prev { left: var(--post-pager-prev-left, -100px); }
.post-pager-arrow.next { left: var(--post-pager-next-left, -100px); }
.post-pager-arrow svg { width: 22px; height: 22px; }
body.no-blur .post-pager-arrow:active { transform: translateY(-50%) scale(0.92); }
.post-pager-arrow:disabled { opacity: 0.32; pointer-events: none; }
.post-pager-arrow:focus-visible {
    outline: 2px solid var(--accent-text);
    outline-offset: 3px;
}
@media (hover: hover) {
    #postModal.has-external-pager .post-pager-arrow.is-available { display: flex; }
    .post-pager-arrow:hover { background: color-mix(in srgb, var(--glass) 82%, transparent); }
}
#postModal.is-closing .post-pager-arrow { display: none; }
body.no-blur .post-pager-arrow {
    background: var(--glass);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    box-shadow: 0 1px 2px rgb(0 0 0 / 25%), 0 4px 12px rgb(0 0 0 / 12%);
}

/* Отложенный индикатор догрузки крупного кадра (герой поста, карусель карточки).
   Экран открывается сразу на засеянной ступени - той, что уже показывала плитка,
   - поэтому индикатор живёт В УГЛУ и на стеклянном диске, а не по центру и не
   поверх затемнения: загораживать уже показанный кадр нельзя (это ровно та
   претензия, которую собрал Discord). Центрированный .lb-loader в style.css - для
   ДРУГОГО случая: пустой слайд лайтбокса, где показывать нечего.
   Появляется только если целевая ступень не приехала за 500мс, чтобы на быстрой
   сети не мигать. Правило: .claude/rules/frontend.md. */
.media-loader {
    position: absolute;
    right: 10px;
    bottom: 10px;
    z-index: 4;
    width: 28px;
    height: 28px;
    display: grid;
    place-items: center;
    border-radius: var(--r-pill, 999px);
    background: var(--chrome-glass-fill-media);
    backdrop-filter: var(--chrome-glass-filter-media);
    -webkit-backdrop-filter: var(--chrome-glass-filter-media);
    box-shadow: var(--chrome-glass-shadow-thin);
    pointer-events: none;
    animation: mediaLoaderIn 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* Засеять было нечем, место показа пустое: тогда индикатор идёт в центр, а не
   в угол. Центруется полями, а не трансформой: трансформу забирает себе
   появление (mediaLoaderIn), и кольцо бы дёрнулось из угла. */
.media-loader.center {
    inset: 0;
    margin: auto;
}
.media-loader::after {
    content: '';
    width: 14px;
    height: 14px;
    border: 2px solid color-mix(in srgb, var(--text) 30%, transparent);
    border-top-color: var(--text);
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
}
body.no-blur .media-loader {
    background: var(--glass-bold);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}
@keyframes mediaLoaderIn {
    from { opacity: 0; transform: scale(0.8); }
    to { opacity: 1; transform: scale(1); }
}
/* гасится только ПОЯВЛЕНИЕ: само вращение это и есть индикация, без него
   остаётся немое кольцо, то есть ровно та немота, ради которой всё делалось */
@media (prefers-reduced-motion: reduce) {
    .media-loader { animation: none; }
}

.post-hero {
    position: relative;
    width: 100%;
    max-height: 64vh;
    overflow: hidden;
    background: var(--glass);
    cursor: pointer;
}
.post-hero-img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
    transition: opacity 250ms ease, filter 200ms ease, transform 200ms ease;
}
.post-hero.blurred .post-hero-img { filter: blur(40px) saturate(1.1); transform: scale(1.1); }
/* video post: the clip plays over its poster (the poster stays underneath as
   the already-decoded seed and as the frame the .post-zoom flight takes) */
.post-hero-video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: none;
}
.post-hero.has-video .post-hero-video { display: block; }
.post-hero.blurred .post-hero-video { filter: blur(40px) saturate(1.1); transform: scale(1.1); }
/* sound toggle over the clip: a 32px disc in the over-art chip material of
   the tile chips (.g-status-chip), the chat .act-icon's size and hit pad */
.post-hero-sound {
    position: absolute;
    right: 12px;
    bottom: 12px;
    z-index: 4;
    width: 32px;
    height: 32px;
    border: none;
    border-radius: var(--r-pill, 999px);
    background: var(--chrome-glass-fill-media);
    backdrop-filter: var(--chrome-glass-filter-media);
    -webkit-backdrop-filter: var(--chrome-glass-filter-media);
    box-shadow: var(--chrome-glass-shadow-thin);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    font-family: inherit;
    transition: opacity 130ms cubic-bezier(0.4, 0, 0.2, 1);
}
.post-hero-sound::before { content: ''; position: absolute; inset: -6px; border-radius: inherit; }
.post-hero-sound svg { width: 16px; height: 16px; display: block; opacity: 0.85; }
@media (hover: hover) {
    .post-hero-sound:hover { opacity: 0.88; }
}
.post-hero-sound:active { opacity: 0.62; }
.post-hero-sound:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.post-hero.blurred .post-hero-sound { visibility: hidden; }
.post-hero-reveal {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    background: rgba(0, 0, 0, 0.25);
}
.post-hero-reveal-hint { font-size: 13px; color: rgba(255, 255, 255, 0.85); }

.post-like-burst {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    opacity: 0;
    color: #fff;
}
.post-like-burst svg {
    width: 96px;
    height: 96px;
    filter: drop-shadow(0 4px 16px rgba(0, 0, 0, 0.45));
}
.post-like-burst.burst { animation: postHeartBurst 750ms cubic-bezier(0.17, 0.89, 0.32, 1.12); }
@keyframes postHeartBurst {
    0% { opacity: 0; transform: scale(0.4); }
    18% { opacity: 0.95; transform: scale(1.08); }
    34% { transform: scale(0.94); }
    50% { transform: scale(1); }
    78% { opacity: 0.95; }
    100% { opacity: 0; transform: scale(1); }
}

/* Zoom-open. На фото поста живут два жеста на одних пикселях (двойной тап
   лайкает, одиночный открывает фуллскрин), а различить их можно только выждав
   окно в 300мс - это платформенное число, iOS и Android оба держат
   doubleTapTimeout на 300, короче нельзя. Раньше окно было мёртвым временем:
   нажатие не отвечало ничем, и паузу читали как скачивание файла, хотя сети
   здесь нет вообще. Теперь окно закрывает переход, а не индикатор: обе системы
   отвечают на это одним и тем же (zoom transition в iOS 26, container transform
   в M3 Expressive) - движение начинается в кадре нажатия и обязано
   разворачиваться на лету. Кадр берётся с живого героя (currentSrc), поэтому
   слой не трогает сеть. */
.post-zoom {
    position: fixed;
    inset: 0;
    /* Зажат между боковой панелью (4000) и тостами (4500): ниже панели кадр
       нырял бы под неё и выпрыгивал обратно в момент коммита, потому что сам
       просмотрщик стоит на 5000 и панель перекрывает; выше тостов слой закрыл
       бы собой объяснение отказа, которое как раз и вызывается вторым тапом
       по своему же посту. */
    z-index: 4200;
    touch-action: none;
    cursor: pointer;
}
.post-zoom-scrim {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.92);
    opacity: 0;
    transition: opacity 310ms cubic-bezier(0.32, 0.72, 0, 1);
}
.post-zoom.zooming .post-zoom-scrim { opacity: 1; }
.post-zoom-img {
    position: absolute;
    transform-origin: top left;
    object-fit: contain;
    will-change: transform;
    transition: transform 310ms cubic-bezier(0.32, 0.72, 0, 1),
                border-radius 310ms cubic-bezier(0.32, 0.72, 0, 1);
}
/* Брошен обратно вторым тапом: путь короче и на кривой прямого ответа, а сам
   кадр растворяется в своём же близнеце под ним, чтобы не закрывать сердце
   лайка, которое играет на герое под слоем. */
.post-zoom.reverting .post-zoom-scrim {
    opacity: 0;
    transition-duration: 200ms;
    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.post-zoom.reverting .post-zoom-img {
    opacity: 0;
    transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1),
                border-radius 200ms cubic-bezier(0.4, 0, 0.2, 1),
                opacity 200ms linear;
}
/* При сниженной подвижности кадр никуда не едет (это решает JS, не назначая
   transform), остаётся одно затемнение - ответ есть, движения нет. */
@media (prefers-reduced-motion: reduce) {
    .post-zoom-img,
    .post-zoom.reverting .post-zoom-img { transition: opacity 200ms linear; }
}

.post-body { padding: 16px 20px 28px; }
.post-author-slot { min-width: 0; }
.post-author-slot .author-tag { margin: 0; }
.post-anon-note { font-size: 11px; color: var(--text3); margin-left: 6px; }
.post-anon { cursor: default; }
.action-btn.reported { opacity: 0.45; pointer-events: none; }
/* liked fill state comes from the shared .action-btn.liked rules;
   the actions row itself is the character modal's .modal-actions */
.post-char-tag { cursor: pointer; }
.post-char-tag img {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    margin-right: 5px;
    vertical-align: -3px;
}

/* Own-post moderation state: tonal panel, tone steps only */
.post-status-panel {
    background: var(--glass);
    border-radius: var(--r-card, 18px);
    padding: 12px 14px;
    margin-bottom: 14px;
}
.post-status-title { font-size: 14px; font-weight: 600; color: var(--text); }
.post-status-flags, .post-status-reason { font-size: 13px; color: var(--text2); margin-top: 6px; }
.post-status-hint { font-size: 12.5px; color: var(--text3); margin-top: 6px; line-height: 1.45; }
/* the author's way back after a decline: same button the character card uses */
.post-status-panel .form-btn { display: block; width: 100%; margin-top: 12px; }
.post-appeal-btn { margin-top: 12px; width: 100%; }

.post-prompt-sec { margin-bottom: 14px; }
.post-prompt-text {
    white-space: pre-wrap;
    word-break: break-word;
    font-size: 13px;
    color: var(--text2);
    line-height: 1.5;
    user-select: text;
    -webkit-user-select: text;
}
.post-prompt-actions { display: flex; gap: 8px; margin-top: 2px; }
/* на раскрытой карточке (--glass-bold) тональная кнопка сливается с фоном:
   тот же полупрозрачный белый слой, что у вторичной кнопки сценария */
.post-prompt-actions .form-btn {
    flex: 1;
    background: rgba(255, 255, 255, 0.10);
}
@media (hover: hover) {
    .post-prompt-actions .form-btn:hover {
        background: rgba(255, 255, 255, 0.15);
    }
}
.post-prompt-actions .form-btn:active { background: rgba(255, 255, 255, 0.15); }


/* ── Small centered sheets (report reasons, content settings): M3 dialog,
      solid tonal fill + elevation, no glass rim ── */
.modal .post-small-sheet {
    width: min(400px, calc(100% - 32px));
    height: auto;
    max-height: 86vh;
    margin: auto;
    border-radius: var(--r-island, 24px);
    background: var(--bg-tint);
    padding: 20px;
    overflow-y: auto;
    /* a classic (non-overlay) desktop scrollbar sits inside the padding box and
       eats the right padding, so content reads as touching the right edge while
       the left keeps 20px. Hide it (wheel/touch still scroll) so the padding
       stays symmetric on every platform. */
    scrollbar-width: none;
    box-shadow: 0 8px 40px rgb(0 0 0 / 45%);
}
.post-small-sheet::-webkit-scrollbar { display: none; }
.post-small-sheet .section-title { margin-bottom: 4px; }
.post-small-sheet .form-buttons { display: flex; gap: 8px; margin-top: 8px; }
.post-small-sheet .form-btn { flex: 1; }

/* Publish/edit are one gallery form family. The editor keeps its chrome and
   actions fixed while only the form body scrolls. */
.modal .post-edit-sheet {
    width: 100%;
    max-width: 520px;
    max-height: min(760px, 100%) !important;
    margin: auto;
    padding: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    background: var(--bg-tint);
    border: none;
    border-radius: var(--r-island, 24px);
    box-shadow: 0 8px 40px rgb(0 0 0 / 45%);
}
.post-edit-sheet:focus { outline: none; }
.post-edit-head {
    flex: 0 0 auto;
    padding: 14px 20px 8px;
}
.post-edit-head .modal-handle { margin-bottom: 12px; }
.post-edit-head .modal-title { margin-bottom: 0; }
.post-edit-scroll {
    flex: 1 1 auto;
    min-height: 0;
    position: relative;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--bg);
}
.post-edit-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
    padding: 12px 20px 16px;
    scroll-padding-bottom: 24px;
}
.post-edit-scroll.is-scrollable .post-edit-body {
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%);
    mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%);
}
.post-edit-body::-webkit-scrollbar { display: none; }
.post-edit-scrollbar {
    position: absolute;
    z-index: 2;
    inset-block: 2px;
    inset-inline-end: 0;
    width: 4px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 130ms cubic-bezier(0.4, 0, 0.2, 1);
}
.post-edit-scrollbar.visible { opacity: 1; }
.post-edit-scrollbar-thumb {
    position: absolute;
    inset-block-start: 0;
    inset-inline-end: 0;
    width: 4px;
    min-height: 24px;
    border-radius: var(--r-pill);
    background: var(--text2);
    opacity: 0.7;
    transform: translate3d(0, 0, 0);
    will-change: transform, height;
}
@media (prefers-reduced-motion: reduce) {
    .post-edit-scrollbar { transition: none; }
}
.post-edit-loading {
    min-height: 220px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.post-edit-loading .spinner { width: 32px; height: 32px; margin: 0; border-width: 3px; }
.post-edit-preview-wrap {
    display: flex;
    justify-content: center;
    margin-bottom: 14px;
}
.post-edit-preview {
    display: block;
    max-width: 60%;
    max-height: 120px;
    object-fit: contain;
    border-radius: var(--r-card, 18px);
    background: var(--glass);
}
.post-edit-sheet .post-edit-actions {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 0;
    padding: 10px 20px 16px;
    background: var(--bg-tint);
}
.post-edit-actions .form-btn { width: 100%; flex: none; }
/* pending state: the shared .form-btn.pending spinner (form section) */

.post-report-list { display: flex; flex-direction: column; margin: 10px 0 8px; }
.post-report-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    margin: 0 -16px;
    width: calc(100% + 32px);
    background: none;
    border: none;
    /* list row: full-bleed highlight, no own radius (its cookbook rule) */
    border-radius: 0;
    color: var(--text);
    font-size: 15px;
    text-align: left;
    cursor: pointer;
}
.post-report-row:active { background: var(--glass); }
/* the selection mark is TRAILING (iOS list), the label takes the rest */
.post-report-row > span:not(.m3-radio) { flex: 1; min-width: 0; }
.post-report-row .m3-radio { flex: none; order: 2; }

.g-prefs-hint { font-size: 13px; color: var(--text3); margin: 6px 0 12px; line-height: 1.45; }
.g-pref-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 9px 0;
}
.g-pref-label { font-size: 15px; color: var(--text); }
.g-pref-seg { flex: none; }

@media (prefers-reduced-motion: reduce) {
    .notification-item.pending::after,
    .mod-row.pending::after { animation: none; }
}

/* ── Profile: author art strip ── */
.p-art-strip {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    padding: 0 20px 20px;
    scrollbar-width: none;
}
.p-art-strip::-webkit-scrollbar { display: none; }
.p-art-sentinel { flex: none; width: 1px; }
.p-art-cell {
    position: relative;
    flex: none;
    width: 96px;
    height: 128px;
    border-radius: 14px;
    overflow: hidden;
    background: var(--glass);
    cursor: pointer;
}
.p-art-cell img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: filter 200ms ease, transform 200ms ease;
}
.p-art-cell.g-blur img { filter: blur(18px); transform: scale(1.15); }

/* Bell: new-post thumbnail preview */
.notification-post-thumb {
    width: 44px;
    height: 56px;
    border-radius: 10px;
    object-fit: cover;
    margin-top: 6px;
    background: var(--glass);
}

/* ── Gallery viewer: caption, style line, edit sheet (feedback round 2) ── */
.post-caption {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.post-edit-lbl {
    font-size: 13.5px;
    font-weight: 500;
    color: var(--text2);
    margin: 16px 0 6px;
}
.post-edit-caption { width: 100%; margin-bottom: 0; }
/* toggle stack spacing, mirrors the publish sheet's 12px row gaps */
.post-edit-switches { display: flex; flex-direction: column; gap: 12px; }
.form-btn:focus-visible,
#characterModal .action-btn:focus-visible,
#postModal .action-btn:focus-visible {
    outline: 2px solid var(--accent-text);
    outline-offset: 2px;
}
.post-edit-styles {
    max-height: 216px;
    overflow-y: auto;
    margin-bottom: 4px;
}

/* gallery mode: the create FAB belongs to the character catalog (its
   visibility is driven async by rules/ban checks, so a class wins the race) */
#community-root.gallery-mode .create-fab { display: none !important; }

/* ── Profile-scoped character collections ─────────────────────────────── */
/* Two families, matching the pre-existing modal split:
   - picker / editor / share are BOTTOM SHEETS in the char-share anatomy:
     community .modal stack + the style.css .modal-sheet material (all-corner
     island radius, glass mix + no-blur fallback), .modal-handle grabber,
     centered 17px .modal-title, NO X chip (handle + swipe + backdrop close).
   - detail is a standard community modal (character-modal family): base
     centering, .modal-content geometry, glass-chip X like the profile modal. */
.collection-picker-modal.active,
.collection-editor-modal.active,
.collection-share-modal.active {
    align-items: flex-end;
    padding: 0 12px calc(20px + var(--safe-bottom, 0px));
}
.collection-modal .collection-sheet {
    max-width: 520px;
    max-height: calc(88vh - var(--tg-top, 0px));
    border-radius: var(--r-island, 24px);
    padding: 14px var(--pad, 16px) 16px;
    background: var(--chrome-glass-fill-readable);
    backdrop-filter: var(--chrome-glass-filter-readable);
    -webkit-backdrop-filter: var(--chrome-glass-filter-readable);
    box-shadow: var(--chrome-glass-shadow);
}
body.no-blur .collection-modal .collection-sheet {
    background: var(--bg-tint);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}
/* the sheet container takes programmatic focus (a11y anchor); controls keep
   their own rings, the container itself must not draw the UA outline */
.collection-modal .modal-content:focus,
.collection-modal .modal-content:focus-visible { outline: none; }
.collection-detail-modal .collection-modal-close {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 18;
    color: var(--text);
}
.collection-detail-modal .collection-modal-close svg { width: 20px; height: 20px; }
.collection-sheet-hint {
    margin: 0 0 14px;
    color: var(--text2);
    font-size: 13px;
    line-height: 1.5;
    text-align: center;
}
.collection-loading {
    min-height: 112px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.collection-loading .spinner { width: 32px; height: 32px; margin: 0; border-width: 3px; }

.p-collections-strip {
    display: flex;
    align-items: stretch;
    gap: 10px;
    overflow-x: auto;
    padding: 0 20px 20px;
    scrollbar-width: none;
}
.p-collections-strip::-webkit-scrollbar { display: none; }
/* a collection card IS the catalog .card with a cover mosaic in the image
   slot: same radius, fills, press, cascade. Only the content box differs
   (fixed like the catalog's, sized for name + 2-line desc + meta). */
.collection-card {
    flex: 0 0 148px;
    min-width: 0;
    padding: 0;
    border: 0;
    color: var(--text);
    font: inherit;
    text-align: left;
}
.collection-card .card-content { height: 100px; padding: 10px 12px 12px; }
.collection-card .card-name { margin-bottom: 0; }
.collection-card-desc {
    display: -webkit-box;
    overflow: hidden;
    height: 34px;
    margin-top: 4px;
    color: var(--text2);
    font-size: 13px;
    line-height: 17px;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
}
.collection-card:focus-visible,
.collection-picker-row:focus-visible,
.collection-author-chip:focus-visible,
.collection-detail-action:focus-visible,
.collection-member-handle:focus-visible,
.collection-member-remove:focus-visible {
    outline: 2px solid var(--accent-text);
    outline-offset: 2px;
}
.collection-mosaic {
    display: grid;
    grid-template-columns: 1.35fr 1fr;
    grid-template-rows: 1fr 1fr;
    gap: 2px;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: var(--bg);
}
.collection-mosaic img,
.collection-mosaic-empty {
    width: 100%;
    height: 100%;
    min-width: 0;
    min-height: 0;
    object-fit: cover;
}
.collection-mosaic img:first-child { grid-row: 1 / 3; }
.collection-mosaic.count-1 img:first-child { grid-column: 1 / 3; }
.collection-mosaic.count-2 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr; }
.collection-mosaic.count-2 img:first-child { grid-row: 1; }
.collection-mosaic-empty {
    grid-column: 1 / 3;
    grid-row: 1 / 3;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--glass-bold);
    color: var(--text2);
    font-size: 34px;
    font-weight: 800;
}
/* system Favorites keeps one identity on every surface: the bookmark glyph
   on the NEUTRAL well (accent-soft is a selection fill, not an identity) */
.collection-mosaic-empty svg { width: 34px; height: 34px; }
/* create tile: one uniform tonal well, centered plus + label (no card bands) */
.collection-card-add {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 150px;
    padding: 16px;
    color: var(--text2);
    text-align: center;
    animation: none;
}
.collection-card-add svg { width: 24px; height: 24px; flex: none; color: var(--text); opacity: 0.55; }
.collection-card-add span { font-size: 13px; font-weight: 500; line-height: 1.3; }
.collection-card-sub {
    display: flex;
    align-items: center;
    gap: 7px;
    overflow: hidden;
    margin-top: 4px;
    color: var(--text3);
    font-size: 11px;
    line-height: 16px;
    white-space: nowrap;
}
.collection-card-sub > span { overflow: hidden; text-overflow: ellipsis; }
/* the .chat-pin-ico recipe: the glyph is BASELINE-anchored inline content,
   never flex-centered - geometric centering drifts with font metrics, a
   baseline anchor rides the text in any font and at any OS text scale
   (1em, not px, so inflation grows the lock with its word) */
.collection-private-badge {
    display: inline;
    flex: none;
    white-space: nowrap;
    color: var(--text3);
    font-size: 11px;
    line-height: 16px;
}
.collection-private-badge svg {
    display: inline-flex;
    width: 1em;
    height: 1em;
    vertical-align: -0.1em;
    margin-right: 4px;
    color: var(--text);
    opacity: 0.3;
}
/* strip error card (isMe load failure); creation lives on the add tile */
.collection-profile-empty {
    flex: 0 0 auto;
    align-self: center;
    max-width: 240px;
    padding: 16px;
    border-radius: var(--r-card, 18px);
    background: var(--glass);
    color: var(--text3);
    font-size: 13px;
    line-height: 1.45;
}

/* picker rows follow the folder-assign sheet grammar (.assign-row/.assign-check
   in style.css): transparent rest, --glass press, trailing 22px check circle */
.collection-picker-list {
    max-height: min(50vh, 430px);
    overflow-y: auto;
    overscroll-behavior: contain;
    margin: 0 0 12px;
}
.collection-picker-row {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 6px;
    border: 0;
    border-radius: var(--r-btn, 14px);
    background: transparent;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition: background 130ms cubic-bezier(.4,0,.2,1);
}
.collection-picker-row:active { background: var(--glass); }
@media (hover: hover) {
    .collection-picker-row:hover { background: var(--glass); }
}
.collection-mosaic.compact {
    flex: 0 0 52px;
    width: 52px;
    height: 52px;
    aspect-ratio: 1;
    border-radius: 11px;
}
.collection-mosaic.compact .collection-mosaic-empty { font-size: 20px; }
.collection-mosaic.compact .collection-mosaic-empty svg { width: 20px; height: 20px; }
.collection-picker-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.collection-picker-name-row { display: flex; align-items: center; gap: 5px; min-width: 0; }
.collection-picker-name {
    overflow: hidden;
    font-size: 15px;
    font-weight: 600;
    white-space: nowrap;
    text-overflow: ellipsis;
}
.collection-picker-meta {
    display: flex;
    align-items: center;
    gap: 7px;
    overflow: hidden;
    color: var(--text3);
    font-size: 12px;
    white-space: nowrap;
}
.collection-picker-meta .collection-private-badge { font-size: 12px; line-height: inherit; }
.collection-picker-empty { padding: 28px 16px; color: var(--text3); text-align: center; font-size: 13px; line-height: 1.45; }
.collection-picker-new { margin-bottom: 8px; }

/* the editor pins its footer: only the field/member well scrolls (the
   gallery post-editor pattern) so the CTA can never leave the sheet */
.collection-editor-sheet {
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.collection-editor-sheet .modal-handle,
.collection-editor-sheet .collection-sheet-title { flex: none; }
.collection-editor-form {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    margin-top: 2px;
}
.collection-editor-scroll {
    display: flex;
    flex-direction: column;
    gap: 20px;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-bottom: 4px;
}
/* the well's children must OVERFLOW into its scroll, never compress: an
   explicit min-height (44px on .switch-row) replaces the min-content floor,
   so a shrinkable row would clip its own wrapped description under text
   inflation before the well ever starts scrolling */
.collection-editor-scroll > * { flex-shrink: 0; }
.collection-editor-scroll .delete-btn { margin-top: 0; flex: none; }
.collection-field { display: block; }
.collection-field .form-label { margin-bottom: 7px; }
/* .char-limit is right-aligned by text-align, which needs a block box */
.collection-field .char-limit { display: block; }
.collection-description-input { min-height: 88px; }
.collection-members-section { margin-top: 2px; }
.collection-members-section[hidden] { display: none; }
.collection-members-heading {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
    margin: 0 0 8px;
    color: var(--text2);
    font-size: 13px;
    line-height: 18px;
    font-weight: 600;
    letter-spacing: 0.4px;
    text-transform: uppercase;
}
/* the trailing VALUE of a grouped header stays sentence case */
.collection-members-count {
    color: var(--text3);
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
}
.collection-members-list { display: flex; flex-direction: column; gap: 8px; }
.collection-members-section.pending .collection-member-row { opacity: .62; }
.collection-member-row {
    min-height: 62px;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 7px 4px 7px 0;
    border-radius: var(--r-btn, 14px);
    background: var(--glass);
}
.collection-member-row.dragging { z-index: 6; box-shadow: 0 12px 28px rgba(0,0,0,.38); }
.collection-member-row.unavailable { opacity: .72; }
.collection-member-handle,
.collection-member-remove {
    flex: 0 0 44px;
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--r-pill, 999px);
    background: transparent;
    color: var(--text3);
    cursor: grab;
    touch-action: none;
    transition: background 130ms cubic-bezier(.4,0,.2,1), transform 130ms cubic-bezier(.4,0,.2,1);
}
.collection-member-remove { color: var(--text2); cursor: pointer; touch-action: manipulation; }
.collection-member-handle svg { width: 18px; height: 18px; }
.collection-member-remove svg { width: 18px; height: 18px; color: var(--text); opacity: 0.55; }
.collection-member-remove:active { background: var(--glass-bold); transform: scale(.92); }
.collection-member-remove:active svg { opacity: 1; }
@media (hover: hover) {
    .collection-member-remove:hover { background: var(--glass-bold); }
    .collection-member-remove:hover svg { opacity: 1; }
}
.collection-member-avatar { flex: 0 0 44px; width: 44px; height: 44px; border-radius: 12px; object-fit: cover; background: var(--glass-bold); }
.collection-member-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.collection-member-name { overflow: hidden; color: var(--text); font-size: 14px; font-weight: 600; white-space: nowrap; text-overflow: ellipsis; }
.collection-member-hint { color: var(--text3); font-size: 11px; line-height: 1.25; }
.collection-members-empty { padding: 20px 12px; border-radius: var(--r-btn, 14px); background: var(--glass); color: var(--text3); text-align: center; font-size: 13px; }
.collection-editor-actions { flex: none; margin-top: 0; padding: 12px 0 2px; }

.collection-detail-sheet {
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.collection-detail-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 58px 16px 22px;
}
.collection-detail-hero { display: flex; align-items: flex-start; gap: 14px; }
.collection-detail-hero > .collection-mosaic {
    flex: 0 0 112px;
    width: 112px;
    border-radius: 17px;
    aspect-ratio: 1;
}
.collection-detail-copy { flex: 1; min-width: 0; padding-top: 2px; }
.collection-detail-title {
    margin: 0;
    color: var(--text);
    font-size: clamp(21px, 6vw, 26px);
    line-height: 1.12;
    overflow-wrap: anywhere;
}
/* hero author byline: the house .author-tag, hugged into the hero flow */
.collection-detail-copy .author-tag {
    margin: 6px 0 0;
    border: 0;
    font: inherit;
}
/* strip context: compact content box (name + meta only, no description) */
.p-collections-strip .collection-card .card-content { height: 62px; }
.collection-detail-description { margin: 6px 0 0; color: var(--text2); font-size: 13px; line-height: 1.4; white-space: pre-wrap; overflow-wrap: anywhere; }
.collection-detail-meta { display: flex; align-items: center; gap: 8px; margin-top: 8px; color: var(--text3); font-size: 12px; }
/* detail actions are the community form-btn row; only icon plumbing is local */
.collection-detail-actions { display: flex; gap: 10px; margin: 16px 0 20px; }
.collection-detail-action { display: inline-flex; align-items: center; justify-content: center; gap: 7px; }
.collection-detail-action svg { width: 17px; height: 17px; }
.collection-detail-members-title { margin: 20px 2px 10px; color: var(--text2); font-size: 13px; line-height: 18px; font-weight: 600; letter-spacing: 0.4px; text-transform: uppercase; }
.collection-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.collection-detail-grid .collection-character-card { min-width: 0; padding: 0; border: 0; color: inherit; font: inherit; text-align: left; }
.collection-detail-empty { grid-column: 1 / -1; padding: 36px 12px; color: var(--text3); text-align: center; font-size: 14px; }
.collection-share-sheet .share-link-chip { width: 100%; border: 0; color: var(--text); font: inherit; text-align: left; }

/* bookmark hold (picker gesture): pre-trigger acknowledgment mirrors
   .msg-hold-press - the transition delay keeps plain taps from flashing it;
   callout/selection off so the iOS long-press menu never races the hold */
#characterModal .save-action-btn {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}
#characterModal .save-action-btn.hold-arming {
    transform: scale(0.94);
    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.12s;
}

html.layout-desktop {
    .collection-picker-modal.active,
    .collection-editor-modal.active,
    .collection-share-modal.active { align-items: center; padding: 20px; }
    .collection-card { flex-basis: 196px; }
}

/* same widening step as .cards-container */
@media (min-width: 768px) {
    .collection-detail-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 360px) {
    .collection-picker-modal.active,
    .collection-editor-modal.active,
    .collection-share-modal.active { padding-left: 8px; padding-right: 8px; }
    .collection-modal .collection-sheet { padding-left: 12px; padding-right: 12px; }
    .collection-detail-hero > .collection-mosaic { flex-basis: 92px; width: 92px; }
    .collection-detail-actions { flex-direction: column; }
}

@media (prefers-reduced-motion: reduce) {
    .collection-modal { animation: none !important; }
    .collection-modal .modal-content { transition: none !important; }
    .collection-card,
    .collection-picker-row,
    .collection-author-chip,
    .collection-detail-action,
    .collection-member-remove { transition: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   DENSE SHELL (body.no-blur) — one block, after every surface rule
   Twin of the block at the end of style.css: every floating community surface
   goes opaque, loses the rim and the radial press light, and keeps the two
   static hairlines plus elevation. Same geometry, same timing.
   ═══════════════════════════════════════════════════════════════════════════ */
body.no-blur .search-input,
body.no-blur .filter-dropdown-menu,
body.no-blur .sort-period-menu,
body.no-blur .char-menu,
body.no-blur .g-tabs::before,
body.no-blur .collection-modal .collection-sheet {
  background: var(--chrome-glass-fill-solid);
  box-shadow: var(--chrome-glass-shadow-solid);
}
body.no-blur .search-input:focus { background: var(--chrome-glass-fill-solid); }
body.no-blur .profile-header-bg.has-backdrop .p-view-actions .p-follow-btn.following,
body.no-blur .stat-chip,
body.no-blur .card-author-chip,
body.no-blur .g-status-chip,
body.no-blur .post-hero-sound,
body.no-blur .close-modal,
body.no-blur .post-pager-arrow,
body.no-blur .modal-image-carousel .carousel-arrow,
body.no-blur .modal-image-carousel .carousel-dots,
body.no-blur .profile-header-bg.has-backdrop .p-view-actions .edit-profile-btn {
  background: var(--chrome-glass-fill-solid);
  box-shadow: var(--chrome-glass-shadow-solid-thin);
}
/* the character viewer keeps its own neutral (non-theme) tone */
body.no-blur .char-lightbox-btn,
body.no-blur .char-lightbox-arrow,
body.no-blur .char-lightbox-tools {
  background: var(--chrome-glass-fill-neutral-solid);
  box-shadow: var(--chrome-glass-shadow-solid-thin);
}

/* ═══════════════════════════════════════════
   ПЕРВАЯ РЕПЛИКА ПЕРСОНАЖА (графа + кнопки форматирования)
═══════════════════════════════════════════ */
/* Поле - тот же материал, что у .form-textarea, но contenteditable: автор
   пишет реплику и сразу видит её так, как её увидит собеседник. Отдельного
   блока превью нет намеренно - он был бы костылём под видимые теги.
   Высоту ставит JS явно (_greetFit): auto-высота не переходит, и модалка
   прыгала бы на каждой новой строке; overflow:hidden прячет строку на время
   роста, как композер iMessage. */
.greet-field {
  position: relative;
  display: block;
  min-height: 96px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  overflow: hidden;
  cursor: text;
  transition: background-color 0.2s ease,
              height 160ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* :empty не годится: очищенное contenteditable хранит <br> и перестаёт быть
   пустым. Класс ставит сериализатор, он единственный знает настоящую длину.
   Плейсхолдер ВНЕ потока: в потоке он занимает место в строке, и каретка
   пустого поля мигает после него. */
.greet-field.is-empty::before {
  content: attr(data-ph);
  position: absolute;
  left: 12px;
  top: 12px;
  right: 12px;
  color: var(--text3);
  pointer-events: none;
}
/* Действие: ЖИРНЫЙ + звёздочки, и звёздочки нарисованы, а не набраны. Каретка
   в сгенерированное содержимое не встаёт и backspace его не съедает, поэтому
   пара никогда не рвётся пополам, а в хранилище они настоящие символы
   (их дописывает сериализация в community.js).
   Оба класса на body ставит app.js по настройкам пользователя. */
.greet-field b {
  font-weight: 700;
  font-style: normal;
}
.greet-field b::before,
.greet-field b::after { content: '*'; }
body.rp-nostars .greet-field b::before,
body.rp-nostars .greet-field b::after { content: none; }
body.rp-dim .greet-field b {
  font-weight: 400;
  font-style: italic;
  color: var(--text2);
}
.greet-field i { font-style: italic; }

/* Строка под графой это ИНСТРУКЦИЯ, а не сопроводительная мелочь: без неё
   кнопки форматирования не найти. .char-hint (text3 под opacity .75) для
   такого слишком тих, поэтому ступень выше. */
.greet-hint {
  font-size: 12px;
  color: var(--text2);
  opacity: 1;
}
.char-limit.over { color: var(--destructive); }

/* Кнопки форматирования: одни и те же .greet-fmt в двух рядах.
   Мышь (hover:hover, pointer:fine) - строка прямо под полем: клавиатуры нет,
   и полка у нижней кромки окна была бы оторвана от поля на пол-экрана.
   Палец - стеклянная полка над клавиатурой: нативную выноску выделения
   ("Формат" в iOS) страница расширить не может ни на одной платформе, а
   геометрия та же, что у .chat-search-panel (bottom + --kb-h из app.js). */
.greet-bar {
  display: none;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
}
@media (hover: hover) and (pointer: fine) {
  .greet-bar { display: flex; }
  .greet-shelf { display: none; }
}
.greet-shelf {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(8px + var(--safe-bottom, 0px));
  z-index: 9998;
  display: flex;
  align-items: center;
  padding: 0 var(--pad, 16px);
  pointer-events: none;
  /* вход/выход как у .scroll-bottom-btn: тот же плавающий чип в чате */
  opacity: 0;
  visibility: hidden;
  transform: translateY(6px) scale(0.9);
  transform-origin: left bottom;
  transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1),
              transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),
              bottom 0.15s cubic-bezier(0.4, 0, 0.2, 1),
              visibility 0s linear 0.15s;
}
.greet-shelf.open {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}
body.kb-open .greet-shelf { bottom: calc(8px + var(--kb-h, 0px)); }
.greet-shelf-island {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 6px;
  height: 50px;
  padding: 5px;
  border-radius: var(--r-pill);
  background: var(--chrome-glass-fill);
  backdrop-filter: var(--chrome-glass-filter);
  -webkit-backdrop-filter: var(--chrome-glass-filter);
  box-shadow: var(--chrome-glass-shadow);
}
.greet-fmt {
  width: 40px;
  height: 40px;
  flex: none;
  border: none;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--text2);
  font-family: inherit;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: transform 130ms cubic-bezier(0.4, 0, 0.2, 1),
              color 130ms cubic-bezier(0.4, 0, 0.2, 1),
              background-color 130ms cubic-bezier(0.4, 0, 0.2, 1),
              opacity 130ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* в строке у поля кнопка тональная: это контент, а не плавающая хромировка */
.greet-bar .greet-fmt { background: var(--glass); }
.greet-fmt:active { transform: scale(0.92); }
.greet-fmt.on {
  background: var(--accent-soft);
  color: var(--accent-text);
}
.greet-fmt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Глиф показывает РЕЗУЛЬТАТ, а не выдуманный символ: у "действия" своего
   значка не существует, зато буква в его начертании читается без подписи.
   Звёздочки на кнопке стоят ВСЕГДА, даже когда пользователь их скрыл: это
   название команды, а не образец его личного отображения. */
.greet-g-act { font-size: 17px; font-weight: 700; letter-spacing: -0.5px; }
.greet-g-act .st { opacity: 0.5; }
.greet-g-em { font-size: 19px; font-style: italic; font-weight: 500; }

body.no-blur .greet-shelf-island {
  background: var(--chrome-glass-fill-solid);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: var(--chrome-glass-shadow-solid);
}
@media (prefers-reduced-motion: reduce) {
  .greet-field { transition: background-color 0.2s ease; }
  .greet-shelf { transition: opacity 0.15s ease, visibility 0s linear 0.15s; transform: none; }
}
