/* =============================================================================
   ATRIUM · 06 · PRODUCT OBJECTS
   =============================================================================
   The things Bherda is actually made of. Each is designed once here and
   reused by every page that shows it — a post card is the same object in
   the feed, in History, in Saved and in Hidden Content.

   Built on the five laws in atrium.components.css. Where a product object
   needs a primitive (a button, an avatar, a row), it uses the primitive
   rather than restating it.

     1. Post card            7. Chat
     2. Comment              8. Notifications
     3. Profile card         9. Search results
     4. Market & NFT        10. Legacy adoption (Bootstrap contract classes)
     5. Gem picker          11. Passcode lock & splash
     6. Wallet & ledger     12. Retained engine styles

   JS CONTRACTS HONOURED (custom.js / post.js / pjax.js / the market engine):
     .like_btn .unlike_btn .save_btn .unsave_btn .hide_btn .unhide_btn
     .repost_btn .followbtn .unfollowbtn .add-comment .comment-input .nce
     .delete-comment .mention-scope .post-img .video-element .post-media
     .is-veiled .chat-bubble[data-message-id] .chatlist_item .btn-tab
     .content-section #likecount{id} [data-comment-count] .stars-overlay
     .centered-item .lazy-load .spin .blurred-media
   ========================================================================== */

/* =============================================================================
   1 · POST CARD  (components/post_card.php)
   -----------------------------------------------------------------------------
   The unit of the feed. A quiet plaster card whose only saturated element is
   the author's work; everything else is ink and space.
   ========================================================================== */
.v-post { margin-bottom: var(--at-s4); }

/* Arrival: cards come into the light top-down, like pages being dealt.
   Suppressed during a PJAX swap so page-light and card-light never read as
   a double bounce. */
#pjax-root .v-post { animation: v-rise var(--at-t-slower) var(--at-ease-out); }
#pjax-root .v-post:nth-child(2) { animation-delay: 40ms; animation-fill-mode: backwards; }
#pjax-root .v-post:nth-child(3) { animation-delay: 80ms; animation-fill-mode: backwards; }
#pjax-root .v-post:nth-child(n+4) { animation-delay: 120ms; animation-fill-mode: backwards; }
#pjax-root.is-swapping .v-post { animation: none; }

.v-post__header {
    display: flex;
    align-items: center;
    gap: var(--at-s3);
    padding: var(--at-s4) var(--at-s4) var(--at-s3);
}

.v-post__author {
    display: flex;
    align-items: center;
    gap: var(--at-s3);
    flex: 1 1 auto;
    min-width: 0;
    color: inherit;
}

.v-post__author:hover { color: inherit; }
.v-post__author:hover .v-post__name > .v-truncate {
    text-decoration: none;
    text-underline-offset: 2px;
}

.v-post__name {
    display: flex;
    align-items: center;
    gap: var(--at-s1);
    min-width: 0;
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-semibold);
    line-height: 1.35;
    color: var(--at-ink);
}

.v-post__meta {
    display: flex;
    align-items: center;
    gap: var(--at-s1);
    font-size: var(--at-text-xs);
    letter-spacing: var(--at-tracking-meta);
    line-height: 1.35;
    color: var(--at-ink-muted);
}

.v-post__meta-dot::before { content: "\00B7"; margin: 0 2px; }

.v-post__body { padding: 0 var(--at-s4) var(--at-s3); }

.v-post__text {
    margin: 0;
    font-size: var(--at-text-md);
    line-height: var(--at-leading-relaxed);
    color: var(--at-ink);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.v-post__text a { font-weight: var(--at-weight-medium); }

/* The work sits in its case, matted by the card's own gutter. */
.v-post .v-vitrine { margin: 0 var(--at-s2) var(--at-s2); }

/* post.js reveals media by removing .is-veiled from .post-media. */
.v-vitrine.is-veiled > img,
.v-vitrine.is-veiled > video {
    filter: blur(28px);
    transform: scale(1.06);
    pointer-events: none;
}

/* --- Actions ---------------------------------------------------------------
   Quiet ink at rest; a voice only when the state is on. */
.v-post__actions {
    display: flex;
    align-items: center;
    gap: var(--at-s0);
    /* Horizontal inset is s1, not s3: the buttons carry their own padding, so
       a 12px row inset pushed the leading glyph to 26px — 10px past the 16px
       content column every other row of the card sits on. At s1 the glyph
       lands ~18px, optically on the column (round marks read aligned a hair
       inboard). */
    padding: var(--at-s1) var(--at-s1) var(--at-s2);
}

.v-post__action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--at-s2);
    /* Square targets: glyph widths differ, so the box sets the size, not the
       ink — otherwise the row's rhythm wanders by a few pixels per icon. */
    min-width: calc(var(--at-hit) - var(--at-s2));
    min-height: calc(var(--at-hit) - var(--at-s2));
    padding: var(--at-s2);
    border: 0;
    border-radius: var(--at-r-pill);
    background: transparent;
    color: var(--at-ink-secondary);
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-medium);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                color var(--at-t-fast) var(--at-ease-out);
}

.v-post__action:hover {
    background-color: var(--at-surface-hover);
    color: var(--at-ink);
}

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

.v-post__action .fi,
.v-post__action .v-icon {
    font-size: var(--at-icon-md);
    transition: transform var(--at-t-base) var(--at-ease-spring);
}

.v-post__action:active .fi {
    transform: scale(0.85);
    transition-duration: var(--at-t-instant);
}

/* On-states. custom.js toggles these as sibling show/hide pairs. */
.v-post__action.unlike_btn { color: var(--at-like); }
.v-post__action.unlike_btn .fi { animation: v-bloom 420ms var(--at-ease-spring); }
.v-post__action.unsave_btn { color: var(--at-accent-ink); }
.v-post__action.unsave_btn .fi { animation: v-pop var(--at-t-slow) var(--at-ease-spring); }

.v-post__count {
    min-width: 1ch;
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-semibold);
    font-variant-numeric: tabular-nums;
    color: var(--at-ink-secondary);
}

/* Trash and hidden views carry their primary action on the card itself. */
.v-post__actions--stacked {
    flex-direction: column;
    align-items: stretch;
    gap: var(--at-s2);
    padding: var(--at-s3) var(--at-s4) var(--at-s4);
}

.v-post--removed .v-post__body {
    color: var(--at-ink-muted);
    font-size: var(--at-text-sm);
}

/* --- Comments panel -------------------------------------------------------- */
.v-post__comments {
    padding: var(--at-s3) var(--at-s4) var(--at-s4);
    border-top: 1px solid var(--at-hairline);
}

.v-post__nocomments {
    margin: var(--at-s3) 0;
    text-align: center;
    font-size: var(--at-text-sm);
    color: var(--at-ink-muted);
}

/* Composer row — custom.js: .comment-input plus its adjacent .add-comment. */
.v-composer {
    display: flex;
    align-items: center;
    gap: var(--at-s2);
    width: 100%;
    margin-top: var(--at-s3);
}

.v-composer .comment-input {
    flex: 1 1 auto;
    min-width: 0;
    min-height: calc(var(--at-control-h) - var(--at-s1));
    padding: 0 var(--at-s4);
    border: 1px solid transparent;
    border-radius: var(--at-r-pill);
    background-color: var(--at-surface-sunken);
    color: var(--at-ink);
    font-family: inherit;
    font-size: var(--at-text-sm);
    transition: background-color var(--at-t-base) var(--at-ease-out),
                border-color var(--at-t-base) var(--at-ease-out),
                box-shadow var(--at-t-base) var(--at-ease-out);
}

.v-composer .comment-input:focus {
    outline: none;
    background-color: var(--at-surface-raised);
    border-color: var(--at-accent);
    box-shadow: 0 0 0 3px var(--at-focus-ring);
}

.v-composer .add-comment {
    flex: 0 0 auto;
    min-height: calc(var(--at-control-h) - var(--at-s1));
    padding: 0 var(--at-s3);
    border: 0;
    border-radius: var(--at-r-pill);
    background: transparent;
    color: var(--at-accent-ink);
    font-family: inherit;
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-semibold);
    cursor: pointer;
    transition: background-color var(--at-t-fast) var(--at-ease-out);
}

.v-composer .add-comment:hover { background-color: var(--at-accent-soft); }
.v-composer .add-comment:disabled { color: var(--at-ink-muted); cursor: not-allowed; }

/* =============================================================================
   2 · COMMENT  (components/comment.php)
   ========================================================================== */
.v-comment {
    display: flex;
    align-items: flex-start;
    gap: var(--at-s3);
    padding: var(--at-s2) 0;
}

.v-comment__bubble {
    flex: 1 1 auto;
    min-width: 0;
    padding: var(--at-s2) var(--at-s3);
    /* Concentric inside the card's comment panel. */
    border-radius: calc(var(--at-r-lg) - var(--at-s2));
    background-color: var(--at-surface-sunken);
}

.v-comment__author {
    font-size: var(--at-text-xs);
    font-weight: var(--at-weight-semibold);
    color: var(--at-ink);
}

.v-comment__text {
    margin: var(--at-s0) 0 0;
    font-size: var(--at-text-sm);
    line-height: var(--at-leading-normal);
    color: var(--at-ink);
    overflow-wrap: anywhere;
}

.v-comment .delete-comment {
    padding: 0 var(--at-s1);
    border: 0;
    background: none;
    color: var(--at-ink-faint);
    font-size: var(--at-text-sm);
    line-height: 1;
    cursor: pointer;
    transition: color var(--at-t-fast) var(--at-ease-out);
}

.v-comment .delete-comment:hover  { color: var(--at-danger); }
.v-comment .delete-comment:active { translate: 0 1px; }

/* =============================================================================
   3 · PROFILE CARD  (components/profile_header.php)
   -----------------------------------------------------------------------------
   Someone's doorway. The banner is the lintel, the avatar is seated into
   the canvas, and the name is the one place the Inscription speaks loudest.
   ========================================================================== */
.v-profile__banner {
    position: relative;
    height: clamp(9rem, 24vw, 13rem);
    border-radius: var(--at-r-lg);
    background-color: var(--at-surface-sunken);
}

/* ---- Profile media stacking (predictable, documented) ---------------------
   The profile header is a small set of layers with fixed order:
     banner image            z 0   (base plate)
     avatar / portrait        z 1   (rises over the banner, below its menu)
     banner action chips      z 2   (sit over the banner image)
     dropdown menus           (own high layer via the overlay controller)
   The avatar deliberately stays BELOW the banner's own dropdown so an opening
   banner menu is never trapped behind the portrait; the two never collide in
   layout, so 1-below-2 costs nothing visually and keeps menus reliably on top. */

/* The clipping layer: it — not the cover — carries overflow:hidden, so the
   image and its skeleton stay rounded while the self-service dropdown, which
   lives beside it, can open past the cover's edge without being clipped. */
.v-profile__banner-media {
    position: absolute;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    border-radius: inherit;
}

.v-profile__banner img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.v-profile__banner-actions {
    position: absolute;
    top: var(--at-s3);
    inset-inline-end: var(--at-s3);
    z-index: 2;
    display: flex;
    gap: var(--at-s2);
}

/* A square icon-only chip (QR) keeps the same height as its labelled siblings. */
.v-chipbtn--icon {
    padding: 0;
    width: var(--at-control-h-sm);
    justify-content: center;
}

/* A destructive chip (remove banner) — neutral until intent, red on hover. */
.v-chipbtn--danger .fi { color: #ff9a9a; }
.v-chipbtn--danger:hover { background: var(--at-danger); color: #fff; }
.v-chipbtn--danger:hover .fi { color: #fff; }

/* A chip laid over arbitrary media: it cannot know what is beneath it, so
   its ink and scrim are absolute by design. */
.v-chipbtn {
    display: inline-flex;
    align-items: center;
    gap: var(--at-s2);
    min-height: var(--at-control-h-sm);
    padding: 0 var(--at-s4);
    border: 0;
    border-radius: var(--at-r-pill);
    background: var(--at-scrim);
    color: #fff;
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-semibold);
    text-decoration: none;
    cursor: pointer;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    transition: background-color var(--at-t-fast) var(--at-ease-out);
}

.v-chipbtn:hover  { background: var(--at-scrim-strong); color: #fff; }
.v-chipbtn:active { translate: 0 1px; }

/* =============================================================================
   PROFILE — flagship two-pane (profile.php + components/profile_header.php)
   -----------------------------------------------------------------------------
   A person's page is a room with two sides: a sticky identity RAIL and the wide
   column of their work. The cover spans both; the portrait seats into its lower
   edge at the head of the rail. From the Floor-Plan posture up the two panes
   open; below it the room folds to one native centred column. The rail reuses
   the identity primitives (name, handle, bio, facts, stats) so there is one
   identity language across the profile and the edit page, not two.
   ========================================================================== */

/* The cover meets the rail — the avatar seats into its lower edge, no gap. */
.v-pf__cover { margin-bottom: 0; }

.v-pf__grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--at-s6);
    align-items: start;
}

.v-pf__identity {
    display: flex;
    flex-direction: column;
    gap: var(--at-s3);
    padding-inline: var(--at-s2);
}

/* The avatar is the only thing that rises into the cover — a fixed amount, never
   shifting with the name. .v-profile__seat is shared with the edit page; the
   --at-avatar-seat fallback (4.125rem) supplies the lift with no extra rule. */
.v-profile__seat {
    position: relative;
    z-index: 1;                 /* avatar layer — above the banner image */
    align-self: flex-start;
    margin-top: calc(var(--at-avatar-seat, 4.125rem) * -1);
    margin-bottom: var(--at-s6);
}
.v-avatar--2xl { display: block; }

.v-pf__titles { min-width: 0; }

/* The rail is narrower than a hero, so the name speaks at section size rather
   than display size — still the Inscription, still the one identity moment.
   Qualified to win over the base .v-profile__name display size. */
.v-pf__identity .v-pf__name { font-size: var(--at-text-2xl); margin-bottom: 0; }

.v-pf__bio { margin-top: 0; max-width: none; }
.v-pf__facts { margin-top: 0; }

/* The two primary acts share the rail's width; the more-menu holds its own. */
.v-pf__actions { flex-wrap: wrap; gap: var(--at-s2); }
.v-pf__cta { flex: 1 1 auto; }

.v-pf__stats { margin-top: var(--at-s1); gap: var(--at-s1); flex-wrap: wrap; }

/* The content column never lets a wide grid push the page sideways. */
.v-pf__main { min-width: 0; }

/* --- Floor Plan and up: the two panes open, the identity stays with you ---- */
@media (min-width: 64em) {
    .v-pf__grid { grid-template-columns: 20rem minmax(0, 1fr); gap: var(--at-s7); }
    .v-pf__aside {
        position: sticky;
        top: calc(var(--at-topbar-h) + env(safe-area-inset-top, 0px) + var(--at-s4));
    }
}

/* --- below Floor Plan: one native centred column --------------------------- */
@media (max-width: 63.98em) {
    .v-pf__identity { align-items: center; text-align: center; }
    .v-pf__seat { align-self: center; }
    .v-pf__name { justify-content: center; }
    .v-pf__bio { text-align: start; width: 100%; max-width: var(--at-measure); margin-inline: auto; }
    .v-pf__facts { justify-content: center; }
    .v-pf__actions { width: 100%; max-width: 26rem; margin-inline: auto; justify-content: center; }
    .v-pf__stats { justify-content: center; }
}

/* The about button is cut into the avatar's lower edge. */
.v-profile__about {
    position: absolute;
    bottom: 0;
    inset-inline-end: 0;
    background: var(--at-surface-raised);
    box-shadow: var(--at-edge), var(--at-shadow-sm);
}

/* --- self portrait: the same media controls as everything else -------------
   The portrait is the trigger (tap opens the shared viewer); a camera veil
   invites the change on hover, and a camera control cut into the lower edge
   opens the anchored menu (change / view / remove). */
.v-pf__portrait { position: relative; }

/* The veil sits over the image; it never eats the tap — pointer-events pass
   through to the image so the portrait still opens the viewer. Hover-capable
   pointers only; on touch the camera control is the affordance. */
.v-pf__portrait-veil {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--at-scrim);
    color: #fff;
    font-size: var(--at-icon-lg);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--at-t-fast) var(--at-ease-out);
}
@media (hover: hover) {
    .v-pf__portrait:hover .v-pf__portrait-veil { opacity: 1; }
}

/* The camera control is anchored to the portrait's lower edge, matching the
   about button's seat so self and others read as the same shape. */
.v-pf__seataction {
    position: absolute;
    bottom: 0;
    inset-inline-end: 0;
}
.v-pf__seatbtn {
    background: var(--at-surface-raised);
    box-shadow: var(--at-edge), var(--at-shadow-sm);
}
.v-pf__seatbtn[aria-expanded="true"] {
    background: var(--at-accent);
    color: var(--at-accent-ink);
}

.v-profile__titles {
    grid-area: titles;
    align-self: end;
    min-width: 0;
}

.v-profile__name {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--at-s2);
    margin: 0 0 var(--at-s1);
    /* A person's name at their own door — the Inscription. */
    font-family: var(--at-font-inscription);
    font-size: var(--at-display);
    font-weight: var(--at-weight-display);
    letter-spacing: var(--at-tracking-display);
    line-height: 1.12;
    color: var(--at-ink);
    overflow-wrap: anywhere;
}

.v-profile__handle {
    margin: 0;
    font-size: var(--at-text-md);
    letter-spacing: var(--at-tracking-meta);
    color: var(--at-ink-muted);
}

/* The name must survive a very long name AND a full cluster of seals: the
   text wraps, the seals stay together on the line they land on. */
.v-profile__nametext { min-width: 0; overflow-wrap: anywhere; }

.v-profile__seals {
    display: inline-flex;
    align-items: center;
    gap: var(--at-s1);
    flex-wrap: wrap;
    /* Seals ride the name's optical centre, not its baseline. */
    align-self: center;
}

/* --- the bio ---------------------------------------------------------------
   What someone wrote about themselves used to live behind an "About" button.
   It reads at body size on a comfortable measure, clamped to three lines with
   a reveal that only exists when the text actually overflows. */
.v-profile__bio {
    margin-top: var(--at-s4);
    max-width: var(--at-measure);
}

.v-profile__biotext {
    margin: 0;
    font-size: var(--at-text-md);
    line-height: var(--at-leading-relaxed);
    color: var(--at-ink-secondary);
    overflow-wrap: anywhere;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.v-profile__bio.is-open .v-profile__biotext {
    -webkit-line-clamp: unset;
    overflow: visible;
}

.v-profile__biomore {
    margin-top: var(--at-s1);
    padding: 0;
    border: 0;
    background: none;
    color: var(--at-accent-ink);
    font: inherit;
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-medium);
    cursor: pointer;
}

.v-profile__biomore:hover { text-decoration: underline; text-underline-offset: 2px; }

/* --- facts -----------------------------------------------------------------
   Where they are, what they are here for, when they arrived. A quiet inline
   row, never a table: these are asides to the name, not data. */
.v-profile__facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--at-s2) var(--at-s4);
    margin: var(--at-s3) 0 0;
    padding: 0;
    list-style: none;
}

.v-profile__facts li {
    display: inline-flex;
    align-items: center;
    gap: var(--at-s2);
    font-size: var(--at-text-sm);
    color: var(--at-ink-muted);
}

.v-profile__facts i { font-size: var(--at-icon-xs); color: var(--at-ink-faint); }

.v-profile__stats {
    display: flex;
    flex-wrap: wrap;
    gap: var(--at-s2);
    margin-top: var(--at-s5);
}

/* The edit page's media block: the portrait is seated into the banner's
   lower edge exactly as it is on the profile, so what you edit looks like
   what you get. */
.v-editmedia {
    display: flex;
    align-items: flex-end;
    gap: var(--at-s4);
    /* Consistent with the profile hero: only the seat rises into the banner
       (its negative margin comes from the shared .v-profile__seat), so the row
       itself carries no negative margin — otherwise the avatar, which reuses
       .v-profile__seat, would be pulled up twice. Top padding keeps the change
       controls and the live name preview below the banner edge. */
    padding: var(--at-s4) var(--at-s5) var(--at-s5);
}

.v-editmedia .v-avatar--2xl { --at-avatar-seat: 4.125rem; display: block; }

.v-editmedia__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--at-s2);
    padding-bottom: var(--at-s2);
}

/* A quiet section label sits before its action buttons so the two direct
   controls read as "Banner: [Change] [Remove]" — no dropdown, no ambiguity. */
.v-editmedia__label {
    font-size: var(--at-text-xs);
    font-weight: var(--at-weight-semibold);
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--at-ink-muted);
    margin-inline-end: var(--at-s1);
}

/* The banner's own action row, beneath the cover. */
.v-editmedia__row {
    display: flex;
    align-items: center;
    gap: var(--at-s2);
    flex-wrap: wrap;
    padding: var(--at-s3) var(--at-s5) 0;
}
.v-editmedia__buttons { display: flex; gap: var(--at-s2); flex-wrap: wrap; }

/* The "pending — save to apply" badge. It rides the cover / seat while a draft
   change is staged, so the user always knows the preview is not yet committed. */
/* Draft "pending" chip. NOTE: --at-accent-ink is the SAME orange as --at-accent,
   so text coloured with it on an accent background is invisible — which turned
   this chip into a solid orange blob (the "stretched orange shape" bug). The
   ink must be --at-accent-contrast (white). The chip is compact and capped so
   it reads as a small badge at the media's edge, never a bar over the image. */
.v-editmedia__pending {
    position: absolute;
    inset-block-start: var(--at-s3);
    inset-inline-start: var(--at-s3);
    z-index: 3;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    max-width: calc(100% - var(--at-s5));
    padding: 0.12rem 0.5rem;
    border-radius: var(--at-r-pill);
    background: var(--at-accent);
    color: var(--at-accent-contrast);
    font-size: var(--at-text-xs);
    font-weight: var(--at-weight-semibold);
    line-height: 1.5;
    white-space: nowrap;
    box-shadow: var(--at-shadow-sm);
}
.v-editmedia__pending i { font-size: 0.72em; color: inherit; }
.v-profile__seat .v-editmedia__pending {
    inset-block-start: auto;
    inset-block-end: -0.55rem;
    inset-inline-start: 50%;
    inset-inline-end: auto;
    transform: translateX(-50%);
}

/* Save Changes gains a subtle emphasis while there are unsaved drafts. */
.v-editmedia__dirtyhint { display: none; }
#updateProfileForm.is-dirty .v-editmedia__dirtyhint { display: inline; }

/* --- Discard-changes dialog (leaving Edit Profile with pending drafts) ----- */
.v-leavedialog {
    position: fixed;
    inset: 0;
    z-index: calc(var(--at-z-modal) + 20);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--at-s5);
    background: var(--at-scrim-strong);
    opacity: 0;
    transition: opacity var(--at-t-fast) var(--at-ease-out);
}
.v-leavedialog.is-open { opacity: 1; }
.v-leavedialog__box {
    width: min(26rem, 100%);
    padding: var(--at-s5);
    border-radius: var(--at-r-lg);
    background: var(--at-surface-raised);
    box-shadow: var(--at-edge), var(--at-shadow-lg);
    transform: translateY(8px) scale(0.98);
    transition: transform var(--at-t-fast) var(--at-ease-out);
}
.v-leavedialog.is-open .v-leavedialog__box { transform: none; }
.v-leavedialog__title { margin: 0 0 var(--at-s2); font-size: var(--at-text-lg); font-weight: var(--at-weight-bold); color: var(--at-ink); }
.v-leavedialog__body { margin: 0 0 var(--at-s5); color: var(--at-ink-secondary); }
.v-leavedialog__actions { display: flex; justify-content: flex-end; gap: var(--at-s2); }

@media (prefers-reduced-motion: reduce) {
    .v-leavedialog, .v-leavedialog__box { transition: none; }
}

/* --- the workshop's form furniture -----------------------------------------
   Every section of the edit form used to carry the same inline margin. One
   class states the rhythm once. */
.v-formsection { margin-bottom: var(--at-s6); }

/* Two fields that belong together sit side by side when there is room, and
   stack when there is not — without borrowing a grid framework's columns. */
.v-formgrid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: var(--at-s4);
}

/* A live counter is calmer than an error that appears at the cliff edge: it
   is always present, and only changes colour as the budget runs out. */
.v-counter {
    margin: var(--at-s1) 0 0;
    font-size: var(--at-text-xs);
    font-variant-numeric: tabular-nums;
    color: var(--at-ink-muted);
    text-align: end;
}

.v-counter.is-low  { color: var(--at-warning); }
.v-counter.is-over { color: var(--at-danger); font-weight: var(--at-weight-semibold); }

/* Drag & drop. The whole media block is the target on a pointer device; the
   ring only appears while something is actually being dragged over it. */
.v-dropzone { position: relative; }

.v-dropzone.is-dragover::after {
    content: attr(data-drop-label);
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--at-s4);
    border: 2px dashed var(--at-accent);
    border-radius: var(--at-r-lg);
    background: color-mix(in srgb, var(--at-surface-raised) 88%, transparent);
    color: var(--at-accent-ink);
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-semibold);
    text-align: center;
    pointer-events: none;
}

/* While a picked image is being read, the preview settles rather than snapping. */
.v-editmedia img.is-swapping,
.v-profile__banner img.is-swapping { animation: v-pop var(--at-t-slow) var(--at-ease-out); }

@media (prefers-reduced-motion: reduce) {
    .v-editmedia img.is-swapping,
    .v-profile__banner img.is-swapping { animation: none; }
}

@media (max-width: 47.98em) {
    .v-editmedia {
        flex-direction: column;
        align-items: center;
        padding-inline: var(--at-s4);
    }
    .v-editmedia__actions { justify-content: center; padding-bottom: 0; }
}

/* The old single-column mobile identity block lived here. The two-pane layout
   folds to a centred column through the .v-pf responsive rules above, so this
   is now handled in one place rather than two. */

/* A stat reads like a flagship's: a bold number over a quiet label, tappable,
   with a hover tint rather than a raised plate. The old plate treatment
   (surface-raised + shadow on every stat) made a row of heavy chips that
   competed with the identity above them — the awkwardness people noticed. */
.v-statbtn {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    min-width: 4.5rem;
    min-height: var(--at-hit);
    padding: var(--at-s2) var(--at-s3);
    border: 0;
    border-radius: var(--at-r-md);
    background: transparent;
    color: var(--at-ink);
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                translate var(--at-t-instant) var(--at-ease-out);
}

.v-statbtn:hover { background: var(--at-surface-hover); color: var(--at-ink); }
.v-statbtn:active { translate: 0 1px; }

.v-statbtn b {
    font-size: var(--at-text-xl);
    font-weight: var(--at-weight-semibold);
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
    letter-spacing: var(--at-tracking-title);
}

.v-statbtn span {
    font-size: var(--at-text-xs);
    letter-spacing: var(--at-tracking-meta);
    color: var(--at-ink-muted);
}

.v-statbtn.disabled { opacity: 0.55; pointer-events: none; }

/* Verdigris marks the wallet stat — the economy's one seat on a profile. */
.v-statbtn--gilt b { color: var(--at-gilt-ink); }

/* Held at the far end of the identity line on wide postures. */
.v-profile__actions {
    grid-area: actions;
    align-self: end;
    justify-self: end;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: var(--at-s2);
}

.v-profile__actions .v-btn--block { flex: 1 1 auto; }
.v-profile__actions .v-alert { padding: var(--at-s2) var(--at-s3); }

/* Content tabs. profile.php toggles the literal `active` class on .btn-tab
   and .content-section — that contract is kept exactly. */
.v-profile__tabs {
    position: sticky;
    /* Held just under the fixed lintel, so the tabs stay reachable however far
       the content column runs. */
    top: calc(var(--at-topbar-h) + env(safe-area-inset-top, 0px));
    z-index: var(--at-z-sticky);
    display: flex;
    /* Left-aligned: the tabs open the wide content column, they do not float in
       the middle of it. */
    justify-content: flex-start;
    gap: var(--at-s1);
    margin: 0 0 var(--at-s4);
    border-bottom: 1px solid var(--at-hairline);
    background: var(--at-bg);
}

/* Below Floor Plan the tabs centre with the folded single column. */
@media (max-width: 63.98em) {
    .v-profile__tabs { justify-content: center; }
}

.btn-tab {
    position: relative;
    min-height: var(--at-hit);
    padding: var(--at-s3) var(--at-s5);
    border: 0;
    background: none;
    color: var(--at-ink-muted);
    font-family: inherit;
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-semibold);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color var(--at-t-base) var(--at-ease-out);
}

.btn-tab:hover  { color: var(--at-ink); background: none; }
.btn-tab.active { color: var(--at-ink); background: none; }

/* ONE rail travels between the tabs rather than a border appearing under each
   in turn — the eye follows a moving object and merely notices a blinking one.
   profile.php measures the active tab and writes the two custom properties. */
.v-profile__tabrail {
    position: absolute;
    bottom: -1px;
    left: 0;
    height: 2px;
    width: var(--tabrail-w, 0px);
    border-radius: var(--at-r-pill);
    background: var(--at-accent);
    transform: translateX(var(--tabrail-x, 0px));
    transition: transform var(--at-t-base) var(--at-ease-inout),
                width var(--at-t-base) var(--at-ease-inout);
}

@media (prefers-reduced-motion: reduce) {
    .v-profile__tabrail { transition: none; }
}

/* =============================================================================
   4 · MARKET & NFT CARDS
   -----------------------------------------------------------------------------
   Objects under glass. The artwork is the only saturated thing on the
   shelf; the card is a quiet stage and Verdigris marks price and ownership.
   The legacy class names are the market markup's contract, adopted here.
   ========================================================================== */
.product-card,
.product-card-nft,
.v-market-card {
    border: 0;
    border-radius: var(--at-r-lg);
    background-color: var(--at-surface);
    box-shadow: var(--at-edge), var(--at-shadow-sm);
    color: var(--at-ink);
    overflow: hidden;
    transition: box-shadow var(--at-t-base) var(--at-ease-out),
                translate var(--at-t-base) var(--at-ease-out);
}

.product-card:hover,
.product-card-nft:hover,
.v-market-card:hover {
    translate: 0 -2px;
    box-shadow: var(--at-edge), var(--at-shadow-md);
}

.product-card:active,
.product-card-nft:active,
.v-market-card:active { translate: 0 0; box-shadow: var(--at-edge), var(--at-shadow-xs); }

.product-img,
.nft-img {
    border-radius: calc(var(--at-r-lg) - var(--at-s2)) calc(var(--at-r-lg) - var(--at-s2)) 0 0;
    object-fit: cover;
    background-color: var(--at-surface-sunken);
}

.product-title {
    margin-bottom: var(--at-s1);
    font-size: var(--at-text-md);
    font-weight: var(--at-weight-semibold);
    letter-spacing: var(--at-tracking-title);
}

/* Price is the economy speaking. */
.price {
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-semibold);
    font-variant-numeric: tabular-nums;
    color: var(--at-gilt-ink);
}

.author {
    font-size: var(--at-text-xs);
    font-weight: var(--at-weight-medium);
    color: var(--at-ink-muted);
}

/* Filter chips (market.php contract classes) — the primitive chip, wearing
   the market's class names. */
.market-chip,
.filter-button {
    min-height: var(--at-control-h-sm);
    padding: var(--at-s1) var(--at-s4);
    border: 0;
    border-radius: var(--at-r-pill);
    background-color: var(--at-surface);
    box-shadow: var(--at-edge), var(--at-shadow-xs);
    color: var(--at-ink-secondary);
    font-family: inherit;
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-medium);
    white-space: nowrap;
    cursor: pointer;
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                color var(--at-t-fast) var(--at-ease-out),
                box-shadow var(--at-t-fast) var(--at-ease-out),
                translate var(--at-t-instant) var(--at-ease-out);
}

.market-chip:hover,
.filter-button:hover {
    background-color: var(--at-surface-hover);
    color: var(--at-ink);
}

.market-chip:active,
.filter-button:active { translate: 0 1px; box-shadow: none; }

.filter-button.active {
    background-color: var(--at-surface-raised);
    color: var(--at-ink);
    font-weight: var(--at-weight-semibold);
    box-shadow: var(--at-edge), var(--at-shadow-sm);
}

/* The shelf's own furniture. These were inline styles on every element in
   market.php; a card's padding is not a page's business. */
/* --- the hero band ---------------------------------------------------------
   A storefront states itself once. The banner art sits behind a legibility
   wash and the words carry the Inscription, so the shelf opens with intent
   rather than a bare image. */
.v-market__hero {
    position: relative;
    display: flex;
    align-items: flex-end;
    min-height: clamp(9rem, 22vw, 12rem);
    margin-bottom: var(--at-s5);
    padding: var(--at-s6);
    border-radius: var(--at-r-lg);
    overflow: hidden;
    background: var(--at-surface-sunken);
    box-shadow: var(--at-edge), var(--at-shadow-sm);
}

.v-market__hero > img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 0;
}

/* The wash: dark at the foot where the words sit, clear at the top. */
.v-market__hero::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background: linear-gradient(to top, var(--at-scrim-strong), transparent 70%);
}

.v-market__hero-body { position: relative; z-index: 2; color: #fff; }
.v-market__hero-body .v-eyebrow { color: rgba(255, 255, 255, 0.82); margin-bottom: var(--at-s1); }

.v-market__hero-title {
    margin: 0;
    font-family: var(--at-font-inscription);
    font-size: var(--at-display);
    font-weight: var(--at-weight-display);
    letter-spacing: var(--at-tracking-display);
    line-height: var(--at-leading-tight);
    color: #fff;
}

.v-market__hero-sub {
    margin: var(--at-s1) 0 0;
    max-width: 34ch;
    font-size: var(--at-text-sm);
    color: rgba(255, 255, 255, 0.9);
}

/* --- toolbar: search beside the sort scroller ------------------------------ */
.v-market__toolbar {
    display: flex;
    align-items: center;
    gap: var(--at-s3);
    margin-bottom: var(--at-s5);
}

.v-market__search {
    position: relative;
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    width: clamp(11rem, 26vw, 18rem);
}

.v-market__search > i {
    position: absolute;
    inset-inline-start: var(--at-s3);
    font-size: var(--at-icon-sm);
    color: var(--at-ink-muted);
    pointer-events: none;
    line-height: 1;
}

.v-market__search-input {
    width: 100%;
    min-height: var(--at-control-h);
    padding: var(--at-s2) var(--at-s3) var(--at-s2) calc(var(--at-s5) + var(--at-s3));
    border: 0;
    border-radius: var(--at-r-pill);
    background: var(--at-surface-sunken);
    color: var(--at-ink);
    font: inherit;
    font-size: var(--at-text-md);
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                box-shadow var(--at-t-fast) var(--at-ease-out);
}

.v-market__search-input::placeholder { color: var(--at-ink-muted); }
.v-market__search-input:focus {
    outline: none;
    background: var(--at-surface-raised);
    box-shadow: 0 0 0 3px var(--at-focus-ring);
}
.v-market__search-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

/* The sort scroller sits to the right of search and takes the remaining width. */
.v-market__sorts { flex: 1 1 auto; min-width: 0; margin-bottom: 0; padding-bottom: var(--at-s2); }
.v-market__sorts .market-chip { text-decoration: none; }

/* --- the shelf: a touch roomier than the generic --3 for premium artwork.
   Declared BEFORE the phone override below so that override wins at equal
   specificity. */
.v-market__grid { --grid-min: 15rem; }

/* On a phone the toolbar stacks (full-width search over the sort scroller) and
   the shelf goes two-up so the artwork stays large and you browse more at once,
   like a shopping app rather than a one-item-per-screen list. */
@media (max-width: 47.98em) {
    .v-market__toolbar { flex-direction: column; align-items: stretch; gap: var(--at-s3); }
    .v-market__search { width: 100%; }
    .v-market__hero { padding: var(--at-s5); }
    .v-market__hero-title { font-size: var(--at-text-2xl); }
    .v-market__grid { --grid-min: 9.5rem; gap: var(--at-s3); }
    .v-market__body { padding: var(--at-s2) var(--at-s3) var(--at-s3); gap: var(--at-s1); }
    .v-market__badge { display: none; }   /* room is tight; the kind is secondary */
}

/* --- the card body --------------------------------------------------------- */
.v-market__body {
    display: flex;
    flex-direction: column;
    gap: var(--at-s2);
    padding: var(--at-s3) var(--at-s4) var(--at-s4);
}

.v-market__head {
    display: flex;
    align-items: center;
    gap: var(--at-s2);
    min-width: 0;
}

.v-market__head .product-title { margin: 0; flex: 1 1 auto; min-width: 0; }

.v-market__badge {
    flex: 0 0 auto;
    padding: 2px var(--at-s2);
    border-radius: var(--at-r-pill);
    background: var(--at-surface-sunken);
    color: var(--at-ink-muted);
    font-size: var(--at-text-xs);
    font-weight: var(--at-weight-semibold);
    letter-spacing: var(--at-tracking-caps);
    text-transform: uppercase;
}

.v-market__seller {
    display: inline-flex;
    align-items: center;
    gap: var(--at-s2);
    min-width: 0;
    text-decoration: none;
    color: var(--at-ink-muted);
}
.v-market__seller:hover .author { color: var(--at-ink); }
.v-market__seller .author { margin: 0; }

/* Price is the loudest thing in the body — Verdigris, tabular, prominent. */
.v-market__pricing {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--at-s2);
    margin-top: var(--at-s1);
}

.v-market__price {
    display: inline-flex;
    align-items: center;
    gap: var(--at-s1);
    font-size: var(--at-text-lg);
    font-weight: var(--at-weight-semibold);
    font-variant-numeric: tabular-nums;
    color: var(--at-gilt-ink);
}
.v-market__price i { font-size: var(--at-icon-sm); }

.v-market__stock { margin: 0; white-space: nowrap; }
.v-market__body .v-progress { margin: 0; }
.v-market__body > .v-btn { margin-top: var(--at-s1); }

/* The wallet chip: the one place on a non-economy screen where Verdigris
   is allowed, because it *is* the economy. */
.v-wallet-chip .v-icon,
.v-wallet-chip .v-num {
    color: var(--at-gilt-ink);
    font-weight: var(--at-weight-semibold);
}

/* An empty slot in the NFT builder — a recessed tray waiting for a layer. */
.square-item {
    aspect-ratio: 1 / 1;
    border: 1px dashed var(--at-hairline-strong);
    border-radius: var(--at-r-md);
    background-color: var(--at-surface-sunken);
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                border-color var(--at-t-fast) var(--at-ease-out);
}

.square-item:hover {
    background-color: var(--at-surface-hover);
    border-color: var(--at-ink-faint);
}

/* Value is calm: the old animated gold gradient is still Verdigris. */
.animated-gold-button {
    padding: var(--at-s3) var(--at-s5);
    border: 0;
    border-radius: var(--at-r-md);
    background: var(--at-gilt);
    color: var(--at-gilt-contrast);
    font-size: var(--at-text-md);
    font-weight: var(--at-weight-semibold);
    animation: none;
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                translate var(--at-t-instant) var(--at-ease-out);
}

.animated-gold-button:hover  { background: var(--at-gilt-bright); color: var(--at-gilt-contrast); }
.animated-gold-button:active { translate: 0 1px; }

#special-glow-card {
    animation: none;
    box-shadow: 0 0 0 1px var(--at-gilt-soft), var(--at-shadow-md);
}

.btn-purple {
    border: 0;
    background: var(--at-gilt);
    color: var(--at-gilt-contrast);
    font-weight: var(--at-weight-semibold);
}

.btn-purple:hover { background: var(--at-gilt-bright); color: var(--at-gilt-contrast); }

/* --- The pack builder ------------------------------------------------------ */
.v-builder__heading { margin-bottom: var(--at-s2); }

.v-builder__tray {
    height: 15rem;
    margin-top: var(--at-s3);
    overflow-y: auto;
    overscroll-behavior: contain;
}

.v-builder__totals .v-label { margin: 0; }
.v-num--gilt { color: var(--at-gilt-ink); font-weight: var(--at-weight-semibold); }

/* --- The purchase dialog ---------------------------------------------------
   The artifact gets a lit stage at the top of the sheet; the facts sit
   beneath it in the quiet. */
.v-nftdialog { overflow: hidden; }

/* Specificity note: .modal-header and .centered-square are re-dressed later
   in this file (§10 and §12), so a bare .v-nftdialog__* class would lose to
   them. The stage is qualified rather than the file reordered — the legacy
   sections must stay last, since they exist to catch unmigrated markup. */
.v-nftdialog .v-nftdialog__stage {
    position: relative;
    height: 15.625rem;
    padding: 0;
    border: 0;
}

.v-nftdialog__case {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
    /* The stage is deliberately absolute: an NFT is lit against its own
       darkness, not against whichever theme the viewer happens to use. */
    background: #0B0B0A;
}

.v-nftdialog__bg {
    display: none;
    z-index: 1;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
}

.v-nftdialog .v-nftdialog__layers {
    width: 11.25rem;
    height: 11.25rem;
}

.v-nftdialog__close {
    position: absolute;
    top: var(--at-s3);
    inset-inline-end: var(--at-s3);
    z-index: 20;
    width: 2.5rem;
    padding: 0;
    justify-content: center;
}

.v-nftdialog__name {
    margin-bottom: var(--at-s2);
    font-family: var(--at-font-inscription);
    font-size: var(--at-text-xl);
    font-weight: var(--at-weight-display);
    letter-spacing: var(--at-tracking-display);
}

.v-nftdialog__price {
    display: flex;
    align-items: center;
    gap: var(--at-s2);
    margin-bottom: var(--at-s4);
    color: var(--at-gilt-ink);
    font-weight: var(--at-weight-semibold);
}

.v-nftdialog__facts { margin-bottom: 0; }

/* NFT detail panels (the profile modal's contract classes). */
.nft-details,
.nft-stats {
    margin-bottom: var(--at-s4);
    padding: var(--at-s4);
    border: 0;
    border-radius: calc(var(--at-r-lg) - var(--at-s2));
    background-color: var(--at-surface-sunken);
}

.detail-row { font-size: var(--at-text-sm); }
.detail-row > :first-child { color: var(--at-ink-muted); }
.detail-row > :last-child {
    color: var(--at-ink);
    font-weight: var(--at-weight-medium);
    font-variant-numeric: tabular-nums;
}

/* =============================================================================
   5 · GEM PICKER  (.icon-viewport — gem_picker.php + the chat picker)
   ========================================================================== */
.icon-viewport {
    padding: var(--at-s4);
    border: 0;
    border-radius: calc(var(--at-r-lg) - var(--at-s2));
    background-color: var(--at-surface-sunken);
    gap: var(--at-s2);
}

.category-label {
    margin: var(--at-s3) 0 var(--at-s2);
    font-size: var(--at-text-xs);
    font-weight: var(--at-weight-semibold);
    letter-spacing: var(--at-tracking-caps);
    text-transform: uppercase;
    color: var(--at-ink-faint);
}

/* The migrated picker markup carries .icon-row with no flex definition of
   its own, so the emoji stacked in a single column. */
.icon-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--at-s1);
}

.icon-item {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    height: 3rem;
    border: 0;
    border-radius: var(--at-r-md);
    background: transparent;
    cursor: pointer;
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                box-shadow var(--at-t-fast) var(--at-ease-out);
}

.icon-item:hover  { background-color: var(--at-surface-hover); }
.icon-item:active { translate: 0 1px; }

.icon-item.selected-icon {
    background-color: var(--at-accent-soft);
    box-shadow: inset 0 0 0 2px var(--at-accent);
}

/* =============================================================================
   6 · WALLET & LEDGER
   -----------------------------------------------------------------------------
   The dial: a balance inscribed in Verdigris, and a ledger that reads like
   a bank book. The most deliberate surface in the product.
   ========================================================================== */
.v-wallet {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--at-s1);
    padding: var(--at-s6) var(--at-s5);
    text-align: center;
}

.v-wallet__label {
    font-size: var(--at-text-xs);
    font-weight: var(--at-weight-semibold);
    letter-spacing: var(--at-tracking-caps);
    text-transform: uppercase;
    color: var(--at-ink-muted);
}

.v-wallet__balance {
    font-family: var(--at-font-inscription);
    font-size: var(--at-display-lg);
    font-weight: var(--at-weight-display);
    letter-spacing: var(--at-tracking-display);
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
    color: var(--at-gilt-ink);
}

.v-wallet__unit {
    font-size: var(--at-text-sm);
    color: var(--at-ink-muted);
}

.v-ledger { display: flex; flex-direction: column; }

.v-ledger__row {
    display: flex;
    align-items: center;
    gap: var(--at-s3);
    min-height: var(--at-hit);
    padding: var(--at-s3) var(--at-s2);
    border-radius: var(--at-r-sm);
    transition: background-color var(--at-t-fast) var(--at-ease-out);
}

.v-ledger__row:hover { background-color: var(--at-surface-hover); }
.v-ledger__row + .v-ledger__row { border-top: 1px solid var(--at-hairline); }

.v-ledger__amount {
    margin-inline-start: auto;
    font-weight: var(--at-weight-semibold);
    font-variant-numeric: tabular-nums;
}

/* Money in is stated; money out is merely recorded. */
.v-ledger__amount.is-in  { color: var(--at-success); }
.v-ledger__amount.is-out { color: var(--at-ink-secondary); }

/* Legacy wallet-modal contract classes. */
.wallet-buttons .btn-wallet {
    border-radius: var(--at-r-md);
    background-color: var(--at-surface-raised);
    box-shadow: var(--at-edge), var(--at-shadow-xs);
    color: var(--at-ink);
    font-weight: var(--at-weight-semibold);
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                translate var(--at-t-instant) var(--at-ease-out);
}

.wallet-buttons .btn-wallet:hover  { background-color: var(--at-surface-hover); color: var(--at-ink); }
.wallet-buttons .btn-wallet:active { translate: 0 1px; }

.wallet-buttons .btn-wallet.disabled {
    background-color: var(--at-surface-sunken);
    color: var(--at-ink-faint);
}

.modal-button-back,
.btn-credit {
    min-width: 0;
    border: 0;
    border-radius: var(--at-r-md);
    background-color: var(--at-surface-sunken);
    color: var(--at-ink-secondary);
    transition: background-color var(--at-t-fast) var(--at-ease-out);
}

.modal-button-back:hover,
.btn-credit:hover { background-color: var(--at-surface-hover); color: var(--at-ink); }

.transaction-item {
    border-radius: var(--at-r-sm);
    transition: background-color var(--at-t-fast) var(--at-ease-out);
}

.transaction-item:hover { background-color: var(--at-surface-hover); }

/* =============================================================================
   7 · POST VIEWER MEDIA
   -----------------------------------------------------------------------------
   Direct Messages moved out of this file entirely. The conversation is a
   surface of its own now (assets/css/atrium/atrium.messenger.css) rather than
   a bubble skin bolted onto a modal, so the ~314 lines that dressed the old
   docked panel, its composer and its list rows are gone. What remains here is
   the post viewer's media pane, which only ever shared the section by accident.
   ========================================================================== */

/* The post viewer's media pane sits flush against the dialog frame, so it
   inherits the frame's rounding on the edges it owns. */
[id^="postview"] .post-media { border-radius: 0; }

@media (min-width: 48em) {
    [id^="postview"] .post-media {
        border-start-start-radius: var(--at-r-lg);
        border-end-start-radius: var(--at-r-lg);
    }
}

@media (max-width: 47.98em) {
    [id^="postview"] .post-media {
        border-start-start-radius: var(--at-r-lg);
        border-start-end-radius: var(--at-r-lg);
    }
}

/* =============================================================================
   THE POST VIEWER — .pv  (profile.php #postview<id>)
   -----------------------------------------------------------------------------
   Rebuilt 2026-07-29 as a flagship viewer. Desktop: a centred dialog, media lit
   on a dark stage beside a conversation column (sticky header · scrolling body ·
   sticky action-bar + composer). Phone: a full-screen, page-style surface —
   media-first header, scrolling conversation, a composer pinned above the safe
   area. It wears .modal .telegram-modal .v-pv-modal so the overlay controller
   owns open/close, the blurred backdrop, body-lock, ESC and scrim-dismiss; every
   pixel inside is this stylesheet's. Contract classes (.post-media, .post-img/
   .video-element, .v-veil, .like_btn/.save_btn, .comment-input/.add-comment,
   #comment-section/#likecount, [data-post-actions]) are load-bearing for JS.
   ========================================================================== */
.pv {
    display: flex;
    flex-direction: row;
    height: 100%;
    min-height: 0;
    padding: 0;
    overflow: hidden;
    background: var(--at-surface-raised);
    isolation: isolate;
}

/* ---- media stage --------------------------------------------------------- */
.pv__stage {
    position: relative;
    flex: 1 1 58%;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 0;
    min-height: 0;
    margin: 0;                    /* the stage is not a feed card — drop .v-vitrine/.post-media spacing */
    overflow: hidden;
    /* A quiet, lit stage so the media is the light in the room. */
    background:
        radial-gradient(120% 120% at 50% 0%, #17181c 0%, #0b0b0d 60%, #060607 100%);
}

/* Loading placeholder — a dark shimmer that the media paints over. Sits behind
   the media (z 0), so if the enhancer never runs the media still shows; only the
   letterbox margins keep the placeholder, which the JS fades on load. */
.pv__skeleton {
    position: absolute;
    inset: 0;
    z-index: 0;
    background:
        linear-gradient(100deg, rgba(255,255,255,0) 30%, rgba(255,255,255,.06) 50%, rgba(255,255,255,0) 70%),
        #0e0f12;
    background-size: 220% 100%, 100% 100%;
    animation: pv-shimmer 1.4s var(--at-ease-inout) infinite;
    transition: opacity var(--at-t-base) var(--at-ease-out);
}
.pv__stage.is-media-ready .pv__skeleton { opacity: 0; animation: none; }

@keyframes pv-shimmer {
    0%   { background-position: 120% 0, 0 0; }
    100% { background-position: -120% 0, 0 0; }
}

.pv__media {
    position: relative;
    z-index: 1;
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
    display: block;
}
.pv__media.post-img { cursor: pointer; }

/* ---- conversation panel -------------------------------------------------- */
.pv__panel {
    flex: 1 1 42%;
    min-width: 21rem;
    max-width: 28rem;
    display: flex;
    flex-direction: column;
    min-height: 0;
    background: var(--at-surface-raised);
    position: relative;
    z-index: 1;
}

/* Sticky header (flex-none — only the body between scrolls). */
.pv__head {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--at-s2);
    padding: var(--at-s3) var(--at-s3) var(--at-s3) var(--at-s4);
    border-bottom: 1px solid var(--at-hairline);
    min-height: 3.75rem;
    background: var(--at-surface-raised);
}

.pv__author {
    display: flex;
    align-items: center;
    gap: var(--at-s3);
    min-width: 0;
    text-decoration: none;
    color: inherit;
    border-radius: var(--at-r-md);
}
.pv__avatar { flex: 0 0 auto; }
.pv__ident { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.pv__name {
    display: inline-flex;
    align-items: center;
    gap: var(--at-s1);
    min-width: 0;
    font-size: var(--at-text-md);
    font-weight: var(--at-weight-semibold);
    letter-spacing: var(--at-tracking-title);
    color: var(--at-ink);
    line-height: var(--at-leading-snug);
}
.pv__author:hover .pv__name { color: var(--at-accent-ink); }
.pv__time {
    font-size: var(--at-text-xs);
    color: var(--at-ink-muted);
    letter-spacing: var(--at-tracking-meta);
}

/* Header icon buttons (overflow menu, close). */
.pv__iconbtn {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border: 0;
    border-radius: var(--at-r-pill);
    background: transparent;
    color: var(--at-ink-secondary);
    font-size: 1.05rem;
    cursor: pointer;
    transition: background var(--at-t-fast) var(--at-ease-out),
                color var(--at-t-fast) var(--at-ease-out);
}
.pv__iconbtn:hover { background: var(--at-surface-hover); color: var(--at-ink); }
.pv__iconbtn:active { transform: scale(.94); }

/* Scrolling body. */
.pv__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-gutter: stable;
    display: flex;
    flex-direction: column;
}

.pv__caption {
    margin: 0;
    padding: var(--at-s4) var(--at-s4) 0 var(--at-s4);
    font-size: var(--at-text-md);
    line-height: var(--at-leading-relaxed);
    color: var(--at-ink);
    overflow-wrap: anywhere;
}

/* Liked-by summary row + anchored likers list. */
.pv__likes { margin:  var(--at-s4) var(--at-s2); }
.pv__likechip {
    display: inline-flex;
    align-items: center;
    gap: var(--at-s2);
    padding: var(--at-s2) var(--at-s3);
    border: 1px solid var(--at-hairline);
    border-radius: var(--at-r-pill);
    background: var(--at-surface);
    color: var(--at-ink-secondary);
    font-size: var(--at-text-xs);
    font-weight: var(--at-weight-medium);
    letter-spacing: var(--at-tracking-meta);
    cursor: pointer;
    transition: background var(--at-t-fast) var(--at-ease-out),
                border-color var(--at-t-fast) var(--at-ease-out);
}
.pv__likechip:not(.is-empty):hover { background: var(--at-surface-hover); border-color: var(--at-hairline-strong); }
.pv__likechip .fi-sr-heart { color: #ec4863; font-size: .95rem; }
.pv__likechip .v-num { color: var(--at-ink); font-weight: var(--at-weight-semibold); font-variant-numeric: tabular-nums; }
.pv__likechip.is-empty { opacity: .6; cursor: default; }

.pv__likers { min-width: 15rem; max-height: 18rem; overflow-y: auto; }
.pv__liker { display: flex; align-items: center; gap: var(--at-s3); }
.pv__liker-body { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.pv__liker-name { font-size: var(--at-text-sm); font-weight: var(--at-weight-medium); color: var(--at-ink); }
.pv__liker-handle { font-size: var(--at-text-xs); color: var(--at-ink-muted); }

/* Comments list — the conversation. */
.pv__comments {
    display: flex;
    flex-direction: column;
    gap: var(--at-s1);
    padding: var(--at-s2) var(--at-s4) var(--at-s5);
    border-top: 1px solid var(--at-hairline);
}

/* Premium comment presentation, scoped to the viewer (the shared component's
   markup is untouched — this only re-dresses it here). */
.pv__comments .v-comment {
    display: flex;
    gap: var(--at-s3);
    padding: var(--at-s2) 0;
    align-items: flex-start;
}
.pv__comments .v-comment .v-avatar {
    flex: 0 0 auto;
    width: 2rem;
    height: 2rem;
    margin-top: 2px;
}
.pv__comments .v-comment__bubble {
    flex: 1 1 auto;
    min-width: 0;
    background: var(--at-surface);
    border: 1px solid var(--at-hairline);
    border-radius: var(--at-r-md) var(--at-r-md) var(--at-r-md) var(--at-r-xs);
    padding: var(--at-s2) var(--at-s3);
}
.pv__comments .v-comment__author {
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-semibold);
    color: var(--at-ink);
    text-decoration: none;
    letter-spacing: var(--at-tracking-title);
}
.pv__comments .v-comment__author:hover { color: var(--at-accent-ink); }
.pv__comments .v-comment .v-meta {
    font-size: var(--at-text-xs);
    color: var(--at-ink-muted);
    letter-spacing: var(--at-tracking-meta);
}
.pv__comments .v-comment__text {
    margin: 2px 0 0;
    font-size: var(--at-text-sm);
    line-height: var(--at-leading-relaxed);
    color: var(--at-ink-secondary);
    overflow-wrap: anywhere;
}
.pv__comments .delete-comment {
    border: 0;
    background: transparent;
    color: var(--at-ink-faint);
    cursor: pointer;
    padding: 2px;
    border-radius: var(--at-r-sm);
    opacity: 0;
    transition: opacity var(--at-t-fast) var(--at-ease-out), color var(--at-t-fast) var(--at-ease-out);
}
.pv__comments .v-comment:hover .delete-comment,
.pv__comments .delete-comment:focus-visible { opacity: 1; }
.pv__comments .delete-comment:hover { color: var(--at-danger, #e5484d); }
/* A freshly posted comment lands with a soft rise. Transform-only and no `both`
   fill, so the resting state is always the element's natural (fully visible) one
   — a paused animation clock can never strand a comment at opacity 0. The `.pv`
   prefix lifts specificity to (0,3,0) so it also overrides the global opacity
   entrance polish.css puts on `[id^="comment-section"] > :first-child`, which
   would otherwise leave the top comment stranded at opacity 0 in a paused tab. */
.pv .pv__comments .v-comment { animation: pv-comment-in var(--at-t-base) var(--at-ease-out); }

@keyframes pv-comment-in {
    from { transform: translateY(6px); }
    to   { transform: none; }
}

/* Empty state. */
.pv__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--at-s2);
    padding: var(--at-s8) var(--at-s4);
    text-align: center;
}
.pv__empty-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 3.25rem;
    height: 3.25rem;
    border-radius: var(--at-r-pill);
    background: var(--at-surface-sunken);
    color: var(--at-ink-muted);
    font-size: 1.4rem;
    margin-bottom: var(--at-s1);
}
.pv__empty-text {
    margin: 0;
    max-width: 18rem;
    font-size: var(--at-text-sm);
    line-height: var(--at-leading-relaxed);
    color: var(--at-ink-muted);
}

/* Sticky footer: action bar + composer. */
.pv__foot {
    flex: 0 0 auto;
    border-top: 1px solid var(--at-hairline);
    background: var(--at-surface-raised);
}

.pv__actions {
    display: flex;
    align-items: center;
    gap: var(--at-s1);
    padding: var(--at-s2) var(--at-s3);
}
.pv__actbtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border: 0;
    border-radius: var(--at-r-pill);
    background: transparent;
    color: var(--at-ink);
    font-size: 1.2rem;
    cursor: pointer;
    transition: background var(--at-t-fast) var(--at-ease-out),
                color var(--at-t-fast) var(--at-ease-out),
                transform var(--at-t-fast) var(--at-ease-spring);
}
.pv__actbtn:hover { background: var(--at-surface-hover); }
.pv__actbtn:active { transform: scale(.88); }
.pv__actbtn.is-on.pv__actbtn--like,
.pv__actbtn.unlike_btn { color: #ec4863; }
.pv__actbtn.unsave_btn { color: var(--at-accent); }
/* The heart pops when it turns on. */
.pv__actbtn.unlike_btn { animation: pv-pop var(--at-t-base) var(--at-ease-spring); }

@keyframes pv-pop {
    0% { transform: scale(.6); }
    60% { transform: scale(1.18); }
    100% { transform: scale(1); }
}

.pv__composer {
    display: flex;
    align-items: center;
    gap: var(--at-s2);
    padding: var(--at-s2) var(--at-s3) var(--at-s3);
}
.pv__composer-avatar { flex: 0 0 auto; }
.pv__input {
    flex: 1 1 auto;
    min-width: 0;
    height: 2.6rem;
    padding: 0 var(--at-s4);
    border: 1px solid var(--at-hairline);
    border-radius: var(--at-r-pill);
    background: var(--at-surface);
    color: var(--at-ink);
    font-size: var(--at-text-sm);
    transition: border-color var(--at-t-fast) var(--at-ease-out),
                box-shadow var(--at-t-fast) var(--at-ease-out),
                background var(--at-t-fast) var(--at-ease-out);
}
.pv__input::placeholder { color: var(--at-ink-faint); }
.pv__input:focus {
    outline: 0;
    border-color: var(--at-accent);
    background: var(--at-surface-raised);
    box-shadow: 0 0 0 3px var(--at-accent-soft);
}
.pv__send {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.6rem;
    height: 2.6rem;
    border: 0;
    border-radius: var(--at-r-pill);
    background: var(--at-accent);
    color: var(--at-accent-contrast);
    font-size: 1.05rem;
    cursor: pointer;
    transform: scale(.6);
    opacity: 0;
    pointer-events: none;
    transition: transform var(--at-t-base) var(--at-ease-spring),
                opacity var(--at-t-fast) var(--at-ease-out),
                background var(--at-t-fast) var(--at-ease-out);
}
/* The send button is revealed only when there is something to send (post-viewer.js
   toggles .is-active on the composer as the field fills). */
.pv__composer.is-active .pv__send { transform: none; opacity: 1; pointer-events: auto; }
.pv__composer.is-active .pv__send:hover { background: var(--at-accent-hover); }
.pv__composer.is-active .pv__send:active { transform: scale(.9); }

/* The floating over-media close is phone-only; desktop uses the header close. */
.pv__x--float { display: none; }

/* ---- desktop: a wide centred immersive dialog ---------------------------- */
@media (min-width: 48em) {
    .telegram-modal.v-pv-modal .modal-dialog { max-width: min(1200px, 96vw); }
    .telegram-modal.v-pv-modal .modal-content {
        height: min(88dvh, 48rem);
        max-height: 88dvh;
        overflow: hidden;
    }

    /* A soft entrance for the media once the dialog is up (the dialog scale/fade
       is owned by the overlay controller). Transform-only and NEVER opacity: a
       paused animation clock (a backgrounded tab) holds the FROM state forever,
       so an opacity-0 start would strand the content invisible — the panel
       content therefore carries no entrance of its own. It always fails open. */
    .v-pv-modal.show .pv__media { animation: pv-media-in var(--at-t-slow) var(--at-ease-out) both; }
}

@keyframes pv-media-in {
    from { transform: scale(1.04); }
    to   { transform: none; }
}

/* ---- phone: a full-screen, page-style surface --------------------------- */
/* [id^="postview"] in atrium.responsive.css already makes the dialog full-screen
   (100dvh, radius 0) and neutralises the sheet grip; here we stack the panes,
   size the media generously (media-first), float the close over it and keep the
   composer above the safe area. */
@media (max-width: 47.98em) {
    /* Pin the surface to exactly one screen so the internal flex owns the layout:
       a fixed media frame, a scrolling conversation, a composer at the bottom
       edge. atrium.responsive.css sets max-height:none on #postview content (it
       lets sheets grow); .modal-content.pv outranks it so the body — not the
       whole dialog — is what scrolls, keeping the composer on-screen. */
    .v-pv-modal .modal-content.pv {
        height: 100dvh;
        max-height: 100dvh;
        padding: 0;                /* the composer owns the bottom safe-area inset */
        overflow: hidden;
    }

    .pv { flex-direction: column; height: 100%; }

    /* Media-first: a stable, generous frame that never collapses while the media
       loads (so the placeholder always has a surface), letterboxing any aspect
       ratio on the dark stage. */
    .pv__stage {
        flex: 0 0 auto;
        width: 100%;
        height: 46dvh;
        padding-top: env(safe-area-inset-top, 0px);
        background:
            radial-gradient(120% 100% at 50% 0%, #17181c 0%, #0b0b0d 70%, #060607 100%);
    }
    .pv__media { width: auto; max-height: 46dvh; }
    [id^="postview"] .pv__stage .pv__media { max-height: 46dvh !important; }

    .pv__panel {
        flex: 1 1 auto;
        min-width: 0;
        max-width: none;
        min-height: 0;
        /* Sits flush below the media (stage 46dvh + panel 54dvh = the screen, no
           gap). Rounded top corners against the dark stage read as a sheet rising
           off the media — no negative-margin overlap, which stole flex height. */
        border-top-left-radius: var(--at-r-xl);
        border-top-right-radius: var(--at-r-xl);
        box-shadow: 0 -10px 30px -20px rgba(0,0,0,.5);
    }

    .pv__head {
        min-height: 3.25rem;
        padding-top: var(--at-s3);
    }
    /* The … menu stays in the header; the header close is redundant on a phone
       (the floating back button over the media handles dismissal). */
    .pv__x--head { display: none; }

    .pv__x--float {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        position: absolute;
        top: calc(var(--at-s3) + env(safe-area-inset-top, 0px));
        inset-inline-start: var(--at-s3);
        z-index: 4;
        width: var(--at-hit);
        height: var(--at-hit);
        border: 0;
        border-radius: var(--at-r-pill);
        background: rgba(10,10,12,.42);
        color: #fff;
        font-size: 1.15rem;
        -webkit-backdrop-filter: blur(8px);
        backdrop-filter: blur(8px);
        cursor: pointer;
    }
    .pv__x--float:active { transform: scale(.92); }

    /* Bigger touch targets on the action bar + composer. */
    .pv__actbtn { width: 2.75rem; height: 2.75rem; font-size: 1.3rem; }
    .pv__input { height: 2.85rem; }
    .pv__send { width: 2.85rem; height: 2.85rem; }

    /* Composer clears the home indicator. */
    .pv__composer { padding-bottom: calc(var(--at-s3) + env(safe-area-inset-bottom, 0px)); }
}

/* Motion opt-out honours the system setting. Every viewer animation is already
   transform-only (never opacity), so content is visible with or without motion;
   this simply stills them. */
@media (prefers-reduced-motion: reduce) {
    .pv__skeleton,
    .pv__comments .v-comment,
    .pv__actbtn.unlike_btn,
    .v-pv-modal.show .pv__media { animation: none !important; }
    .pv__media { transform: none !important; }
}


/* =============================================================================
   8 · NOTIFICATIONS  (footer.php)
   ========================================================================== */
.v-notif {
    display: flex;
    align-items: flex-start;
    gap: var(--at-s3);
    /* 8 here + the panel body's 12 = the 20px header gutter, so avatars sit
       flush under the panel title. */
    padding: var(--at-s3) var(--at-s2);
    border-radius: var(--at-r-md);
    transition: background-color var(--at-t-fast) var(--at-ease-out);
}

.v-notif:hover { background-color: var(--at-surface-hover); }

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

.v-notif__name {
    display: inline-flex;
    align-items: center;
    gap: var(--at-s1);
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-semibold);
    color: var(--at-ink);
}

.v-notif__name:hover {
    color: var(--at-ink);
    text-decoration: none;
    text-underline-offset: 2px;
}

.v-notif__text {
    margin: 0;
    font-size: var(--at-text-sm);
    line-height: var(--at-leading-snug);
    color: var(--at-ink-secondary);
    overflow-wrap: anywhere;
}

.v-notif__time {
    font-size: var(--at-text-xs);
    letter-spacing: var(--at-tracking-meta);
    color: var(--at-ink-muted);
}

.v-notif__state { flex: 0 0 auto; align-self: center; }

/* =============================================================================
   9 · SEARCH RESULTS
   -----------------------------------------------------------------------------
   ajax.php now renders results as .v-person rows — the same object the feed
   suggestions, follower lists and like lists use. The scoped re-dressing of
   Bootstrap utility markup that used to live here is gone with the markup
   that needed it.
   ========================================================================== */
#sra .v-person + .v-person { margin-top: var(--at-s0); }

#sra img { background-color: var(--at-surface-sunken); }

/* =============================================================================
   10 · LEGACY ADOPTION
   -----------------------------------------------------------------------------
   Contract-class re-dressing so unmigrated markup (Bootstrap dialogs, the
   .form-* family, .btn-*, edit.php) renders as ATRIUM without being
   touched. This section shrinks as pages migrate; nothing new may reference it.
   ========================================================================== */
/* --- Bootstrap-hosted dialogs, app-wide ----------------------------------- */
.modal-content {
    border: 0;
    border-radius: var(--at-r-lg);
    background-color: var(--at-surface-raised);
    color: var(--at-ink);
    box-shadow: var(--at-edge), var(--at-shadow-lg);
}

.modal-header {
    padding: var(--at-s4) var(--at-s5);
    border-bottom: 1px solid var(--at-hairline);
    background: transparent;
}

.modal-title {
    font-size: var(--at-text-lg);
    font-weight: var(--at-weight-semibold);
    letter-spacing: var(--at-tracking-title);
    color: var(--at-ink);
}

/* One gutter for all three regions: Bootstrap's 1rem body inset left dialog
   content 4px off the header and footer. */
.modal-body {
    padding: var(--at-s4) var(--at-s5);
    color: var(--at-ink);
}

.modal-footer {
    padding: var(--at-s3) var(--at-s5);
    border-top: 1px solid var(--at-hairline);
    background: transparent;
}

.modal-bg        { background-color: var(--at-surface-raised) !important; color: var(--at-ink) !important; }
.modal-header-bg { background-color: var(--at-surface-sunken) !important; color: var(--at-ink) !important; }
.custom-modal    { background: var(--at-surface-raised); color: var(--at-ink); box-shadow: var(--at-edge), var(--at-shadow-lg); }
.custom-modal .modal-header,
.custom-modal .modal-footer { background: transparent; }

/* =============================================================================
   THE BOTTOM SHEET — one geometry for every large dialog on a phone
   -----------------------------------------------------------------------------
   Two families carry sheets: .v-actions (the post action sheet) and
   .telegram-modal (compose, wallet, withdraw, transaction, new item). They
   share ONE definition so a sheet is the same object wherever it appears —
   full width, anchored to the bottom edge, rounded only where it meets the
   screen, and never taller than most of the viewport, because the page showing
   above it is what tells you this is a sheet and not a new page.

   Small confirmations deliberately do NOT get this: they stay centred dialogs
   (see post_modals.php), which is the native behaviour for a short question.
   The slide itself lives in atrium.polish.css §2; the drag is in the overlay
   controller. This is only the shape. */
@media (max-width: 47.98em) {
    .v-actions .modal-dialog,
    .telegram-modal .modal-dialog {
        position: fixed;
        inset-inline: 0;
        bottom: 0;
        margin: 0;
        max-width: none;
    }

    .v-actions .modal-content,
    .telegram-modal .modal-content {
        border-radius: var(--at-r-xl) var(--at-r-xl) 0 0;
        max-height: 90vh;
        max-height: 90dvh;      /* the honest viewport once the URL bar moves */
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }

    /* Only the sheet's body scrolls — its header stays put, and a scroll that
       reaches the end never drags the page behind it. */
    .v-actions .modal-body,
    .telegram-modal .modal-body {
        overflow-y: auto;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
    }

    /* Every sheet carries the same grab handle, so what can be dragged looks
       draggable. .v-actions ships a real .v-sheet__grip element (it sits in
       the flow above a menu); the .telegram-modal family draws the identical
       bar as a pseudo-element, which gives all of its sheets the handle from
       one rule instead of thirteen edits — and it costs no layout, sitting
       inside the 16px of whitespace the header already holds above its text. */
    .telegram-modal .modal-content { position: relative; }

    .telegram-modal .modal-content::before {
        content: "";
        position: absolute;
        top: var(--at-s2);
        left: 50%;
        transform: translateX(-50%);
        width: 2.25rem;
        height: 0.25rem;
        border-radius: var(--at-r-pill);
        background: var(--at-hairline-strong);
        pointer-events: none;
        z-index: 2;
    }
}

@media (min-width: 48em) {
    .v-actions .modal-dialog { max-width: 24rem; margin: 1.75rem auto; }
    .v-actions .v-sheet__grip,
    .telegram-modal .v-sheet__grip { display: none; }

    /* DESKTOP: the sheet becomes a premium CENTRED dialog. Bootstrap's default
       `margin: 1.75rem auto` is top-aligned, which left short dialogs (a
       follower list, the QR card) stranded at the top of the screen. This
       centres them vertically and horizontally, caps their height, and lets
       only the body scroll — so nothing is ever attached to the top edge. */
    .telegram-modal .modal-dialog {
        display: flex;
        align-items: center;
        min-height: calc(100dvh - 3.5rem);
        max-width: 32rem;
        margin: 1.75rem auto;
    }
    .telegram-modal .modal-content {
        width: 100%;
        max-height: calc(100dvh - 5rem);
    }
    .telegram-modal .modal-body {
        overflow-y: auto;
        overscroll-behavior: contain;
    }
}

.v-actions .v-menu {
    min-width: 0;
    background: transparent;
    box-shadow: none;
}

.v-actions.show .v-menu .v-menu__item { animation: v-rise var(--at-t-slow) var(--at-ease-out) backwards; }
.v-actions.show .v-menu .v-menu__item:nth-child(2) { animation-delay: 30ms; }
.v-actions.show .v-menu .v-menu__item:nth-child(3) { animation-delay: 60ms; }
.v-actions.show .v-menu .v-menu__item:nth-child(n+4) { animation-delay: 90ms; }

/* --- Forms (legacy .form-* markup) ---------------------------------------- */
.form-label {
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-medium);
    color: var(--at-ink-secondary);
}

.form-control,
.form-select {
    min-height: var(--at-control-h);
    border: 1px solid transparent;
    border-radius: var(--at-r-md);
    background-color: var(--at-surface-sunken);
    color: var(--at-ink);
    font-size: var(--at-text-md);
    transition: background-color var(--at-t-base) var(--at-ease-out),
                border-color var(--at-t-base) var(--at-ease-out),
                box-shadow var(--at-t-base) var(--at-ease-out);
}

.form-control::placeholder { color: var(--at-ink-muted); opacity: 1; }

.form-control:hover,
.form-select:hover { background-color: var(--at-surface-hover); }

.form-control:focus,
.form-select:focus {
    outline: none;
    background-color: var(--at-surface-raised);
    border-color: var(--at-accent);
    box-shadow: 0 0 0 3px var(--at-focus-ring);
    color: var(--at-ink);
}

textarea.form-control { min-height: calc(var(--at-control-h) * 2); }

.form-check-input {
    width: 1.25rem;
    height: 1.25rem;
    margin-top: 0.125rem;
    border: 1.5px solid var(--at-hairline-strong);
    background-color: var(--at-surface-raised);
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                border-color var(--at-t-fast) var(--at-ease-out);
}

.form-check-input:checked,
.form-check-input[type="radio"]:checked {
    background-color: var(--at-accent);
    border-color: var(--at-accent);
}

.form-check-input:focus {
    border-color: var(--at-accent);
    box-shadow: 0 0 0 3px var(--at-focus-ring);
}

/* --- Buttons (legacy .btn markup) ------------------------------------------ */
.btn {
    border-radius: var(--at-r-md);
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-semibold);
    letter-spacing: var(--at-tracking-body);
    transition: background-color var(--at-t-base) var(--at-ease-out),
                color var(--at-t-base) var(--at-ease-out),
                border-color var(--at-t-base) var(--at-ease-out),
                box-shadow var(--at-t-base) var(--at-ease-out),
                translate var(--at-t-instant) var(--at-ease-out);
}

.btn:hover  { background-color: transparent; }
.btn:active { translate: 0 1px; background-color: transparent; }

/* Auth CTA metrics. This re-dress outranks Bootstrap's .btn-lg, Bootstrap 5
   dropped .btn-block, and .btn-pill never existed — so the auth family's
   primary buttons rendered 37.5px tall with 13px text while every v-btn CTA
   is 48/15/pill. These map the legacy names onto the control tokens. */
.btn-lg {
    min-height: var(--at-control-h-lg);
    padding: var(--at-s2) var(--at-s5);
    font-size: var(--at-text-md);
}

.btn-pill { border-radius: var(--at-r-pill); }

.btn-block {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--at-s2);
    width: 100%;
}

.btn-primary, .btn-orange, .bg-orange, .btn-white-big {
    border-color: transparent;
    background-color: var(--at-accent);
    color: var(--at-accent-contrast);
    box-shadow: var(--at-shadow-xs);
}

.btn-primary:hover, .btn-orange:hover, .btn-white-big:hover {
    background-color: var(--at-accent-hover);
    border-color: transparent;
    color: var(--at-accent-contrast);
}

.btn-primary:focus, .btn-orange:focus {
    box-shadow: 0 0 0 3px var(--at-focus-ring);
    color: var(--at-accent-contrast);
}

.btn-secondary, .btn-neutral {
    border-color: transparent;
    background-color: var(--at-surface-raised);
    color: var(--at-ink);
    box-shadow: var(--at-edge), var(--at-shadow-xs);
}

.btn-secondary:hover, .btn-neutral:hover {
    background-color: var(--at-surface-hover);
    color: var(--at-ink);
}

.btn-danger {
    border-color: transparent;
    background-color: var(--at-danger-soft);
    color: var(--at-danger);
}

.btn-danger:hover { background-color: var(--at-danger); color: #fff; }

.btn-danger-soft {
    border-color: transparent;
    background-color: var(--at-danger-soft);
    color: var(--at-danger);
}

.btn-outline, .btn-outline-orange {
    border: 1px solid var(--at-hairline-strong);
    background: transparent;
    color: var(--at-ink);
}

.btn-outline:hover, .btn-outline-orange:hover {
    background-color: var(--at-surface-hover);
    border-color: var(--at-hairline-strong);
    color: var(--at-ink);
}

.btn-ghost { background: transparent; border-color: transparent; color: var(--at-ink-secondary); }
.btn-ghost:hover { background-color: var(--at-surface-hover); color: var(--at-ink); }

/* The auth family's identity line speaks the Inscription (auth.css is a
   route-scoped legacy sheet; the voice is applied from the system). */
.auth-title {
    font-family: var(--at-font-inscription);
    font-weight: var(--at-weight-display);
    letter-spacing: var(--at-tracking-display);
}

.styled-text { color: var(--at-accent-ink); }
.styled-text:hover { color: var(--at-accent-ink); }
.orange-link { color: var(--at-accent-ink); }
.orange-link:hover { color: var(--at-accent-hover); }

/* --- Toast (custom.js creates .template-toast) ----------------------------- */
.template-toast {
    position: fixed;
    left: 50%;
    bottom: calc(var(--at-tabbar-h) + var(--at-s4) + env(safe-area-inset-bottom, 0px));
    z-index: var(--at-z-toast);
    min-width: 13rem;
    max-width: min(26rem, calc(100vw - var(--at-s7)));
    padding: var(--at-s3) var(--at-s5);
    border-radius: var(--at-r-md);
    background-color: var(--at-ink);
    color: var(--at-ink-inverse);
    box-shadow: var(--at-shadow-lg);
    font-size: var(--at-text-sm);
    font-weight: var(--at-weight-medium);
    text-align: center;
    opacity: 0;
    transform: translateX(-50%) translateY(8px);
    pointer-events: none;
    transition: opacity var(--at-t-base) var(--at-ease-out),
                transform var(--at-t-base) var(--at-ease-out);
}

.template-toast.is-visible { opacity: 1; transform: translateX(-50%) translateY(0); }

@media (min-width: 48em) { .template-toast { bottom: var(--at-s6); } }

/* --- Bootstrap alerts ------------------------------------------------------ */
.alert {
    border: 0;
    border-radius: var(--at-r-md);
    font-size: var(--at-text-sm);
}

.alert-danger    { background: var(--at-danger-soft);   color: var(--at-danger); }
.alert-secondary { background: var(--at-surface-sunken); color: var(--at-ink-secondary); }
.alert-success   { background: var(--at-success-soft);  color: var(--at-success); }

/* =============================================================================
   11 · PASSCODE LOCK & SPLASH
   -----------------------------------------------------------------------------
   A convenience lock, not access control (see the component's doc block).
   ========================================================================== */
.lock {
    position: fixed;
    inset: 0;
    z-index: var(--at-z-lock);
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--at-scrim-strong);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    transition: opacity var(--at-t-slow) var(--at-ease-in);
}

.lock.is-out { opacity: 0; }

.lock-inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--at-s4);
    padding: var(--at-s6);
    text-align: center;
}

.lock-icon { font-size: var(--at-icon-lg); color: #fff; }

.lock-text {
    margin: 0;
    color: #fff;
    font-size: var(--at-text-md);
    font-weight: var(--at-weight-medium);
}

.lock-dots { display: flex; gap: var(--at-s3); margin: var(--at-s2) 0 var(--at-s4); }

.lock-dot {
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 50%;
    border: 1.5px solid rgba(255, 255, 255, 0.55);
    background: transparent;
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                border-color var(--at-t-fast) var(--at-ease-out);
}

.lock-dot.is-filled { background: #fff; border-color: #fff; }

.lock-dots.is-wrong { animation: v-lock-shake 0.5s var(--at-ease-inout); }
.lock-dots.is-wrong .lock-dot { border-color: var(--at-danger); background: var(--at-danger); }

@keyframes v-lock-shake {
    0%, 100% { transform: translateX(0); }
    20%, 60% { transform: translateX(-7px); }
    40%, 80% { transform: translateX(7px); }
}

.keypad {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--at-s3);
}

.key {
    width: 4.25rem;
    height: 4.25rem;
    border: 0;
    border-radius: var(--at-r-pill);
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    font-family: inherit;
    font-size: var(--at-text-xl);
    font-weight: var(--at-weight-medium);
    font-variant-numeric: tabular-nums;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                translate var(--at-t-instant) var(--at-ease-out);
}

.key:hover  { background: rgba(255, 255, 255, 0.2); }
.key:active { translate: 0 1px; background: rgba(255, 255, 255, 0.3); }

.key-empty { visibility: hidden; pointer-events: none; }
.key-back  { background: transparent; font-size: var(--at-icon-md); }

body.noscroll {
    overflow: hidden !important;
    height: 100vh;
    touch-action: none;
}

/* =============================================================================
   Splash — the first-visit launch screen (wall.php, session-gated).
   -----------------------------------------------------------------------------
   Premium, minimal, GPU-only (transform/opacity). It is shown from the very
   FIRST paint by a tiny inline <style> in header.php (so there is no white flash
   and no wait for scripts), then removed by JS the instant the window finishes
   loading. A pure-CSS fail-safe fades it away after 3s even if that JS never
   runs, so a broken script can never leave the app behind a curtain.
   Colours are theme-aware via tokens, each with a solid fallback line first so
   a browser without color-mix() still gets a correct background.
   ========================================================================== */
#splash-screen {
    position: fixed;
    inset: 0;
    z-index: var(--at-z-boot);
    display: none;                         /* the header.php boot <style> flips this to flex on first visit */
    align-items: center;
    justify-content: center;
    padding: max(var(--at-s6), env(safe-area-inset-top))
             max(var(--at-s6), env(safe-area-inset-right))
             max(var(--at-s6), env(safe-area-inset-bottom))
             max(var(--at-s6), env(safe-area-inset-left));
    overflow: hidden;
    opacity: 1;
    -webkit-user-select: none; user-select: none;
    background: var(--at-bg);
    background: radial-gradient(120% 90% at 50% 38%,
                    color-mix(in srgb, var(--at-accent, #F07629) 14%, transparent) 0%,
                    transparent 60%),
                var(--at-bg);
    transition: opacity 420ms var(--at-ease-in, ease);
    animation: splashFailSafe 0s linear 3000ms forwards;   /* JS-free safety net */
}
#splash-screen.is-hiding { opacity: 0; pointer-events: none; }

#splash-screen .splash-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--at-s4);
    text-align: center;
    transform: translateZ(0);
}

/* Logo mark with a soft, slowly breathing halo behind it. */
#splash-screen .splash-logo {
    position: relative;
    width: 88px; height: 88px;
    display: grid; place-items: center;
}
#splash-screen .splash-logo::before {
    content: ""; position: absolute; inset: -40%; z-index: -1;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(240, 118, 41, 0.42) 0%, transparent 68%);
    background: radial-gradient(circle,
                    color-mix(in srgb, var(--at-accent, #F07629) 42%, transparent) 0%,
                    transparent 68%);
    animation: splashHalo 2600ms var(--at-ease-out, ease) infinite;
}
#splash-screen .splash-logo img {
    width: 100%; height: 100%; object-fit: contain;
    pointer-events: none;
    animation: splashLogoIn 720ms var(--at-ease-out, cubic-bezier(.22,.61,.36,1)) both;
}

#splash-screen .app-name {
    margin: 0;
    font-family: var(--at-font-inscription);
    font-size: var(--at-display);
    font-weight: var(--at-weight-display);
    letter-spacing: var(--at-tracking-display);
    color: var(--at-ink);
    animation: splashRise 720ms var(--at-ease-out, ease) 90ms both;
}

/* Minimal indeterminate loader — a hairline track with a travelling highlight. */
#splash-screen .splash-loader {
    position: relative;
    width: 68px; height: 3px; margin-top: var(--at-s1);
    border-radius: 999px;
    overflow: hidden;
    background: rgba(0, 0, 0, 0.12);
    background: color-mix(in srgb, var(--at-ink, #000) 12%, transparent);
    animation: splashRise 720ms var(--at-ease-out, ease) 200ms both;
}
#splash-screen .splash-loader::after {
    content: ""; position: absolute; inset: 0;
    width: 40%; border-radius: inherit;
    background: var(--at-accent, #F07629);
    animation: splashBar 1200ms ease-in-out infinite;
}

@keyframes splashLogoIn   { from { opacity: 0; transform: scale(.82) translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes splashRise     { from { opacity: 0; transform: translateY(8px); }            to { opacity: 1; transform: none; } }
@keyframes splashHalo     { 0%, 100% { opacity: .5; transform: scale(.96); } 50% { opacity: .95; transform: scale(1.05); } }
@keyframes splashBar      { 0% { transform: translateX(-140%); } 100% { transform: translateX(320%); } }
@keyframes splashFailSafe { to { opacity: 0; visibility: hidden; pointer-events: none; } }

/* Desktop: a restrained loading state, not a stretched mobile splash — smaller
   mark, a tighter/quieter glow. */
@media (min-width: 768px) {
    #splash-screen {
        background: var(--at-bg);
        background: radial-gradient(70% 55% at 50% 42%,
                        color-mix(in srgb, var(--at-accent, #F07629) 9%, transparent) 0%,
                        transparent 60%),
                    var(--at-bg);
    }
    #splash-screen .splash-logo { width: 74px; height: 74px; }
    #splash-screen .app-name { font-size: calc(var(--at-display) * 0.9); }
}

/* Reduced motion: present the screen, skip every entrance + loop animation. The
   #splash-screen fail-safe is deliberately NOT cleared here — it is a visibility
   flip, not motion, and it must survive so a broken script can't strand anyone. */
@media (prefers-reduced-motion: reduce) {
    #splash-screen .splash-logo img,
    #splash-screen .app-name,
    #splash-screen .splash-loader { animation: none; }
    #splash-screen .splash-logo::before { animation: none; opacity: .6; }
    #splash-screen .splash-loader::after { animation: none; width: 100%; opacity: .5; }
}

body.splash-active { overflow: hidden; height: 100vh; touch-action: none; }

/* ============================================================================
   PULL-TO-REFRESH INDICATOR (assets/js/pull-refresh.js)
   ----------------------------------------------------------------------------
   A branded disc that rides down from the top edge as the page is pulled: a
   progress ring fills to the arm threshold, an arrow flips to say "release", a
   spinner turns while PJAX refetches, and a check confirms before it lifts away.
   Fixed + centred; the JS drives translateY/opacity with transform only (no
   layout). Theme-aware via tokens, so it reads on both Day and Night.
   ========================================================================== */
.v-ptr {
    position: fixed;
    top: max(var(--at-s2), env(safe-area-inset-top));
    left: 50%;
    transform: translate3d(-50%, 0, 0);
    z-index: 790;                          /* over content, under toasts (800); JS hides it when overlays open */
    opacity: 0;
    pointer-events: none;
    will-change: transform, opacity;
}
/* Finger-follow is instant (no transition); the settle/pin/hide states animate. */
.v-ptr.is-relax,
.v-ptr.is-refreshing,
.v-ptr.is-hiding {
    transition: transform 0.34s var(--at-ease-out, cubic-bezier(.22,.61,.36,1)), opacity 0.34s ease;
}

.v-ptr__disc {
    position: relative;
    width: 40px; height: 40px;
    display: grid; place-items: center;
    border-radius: 50%;
    background: var(--at-surface-raised, #fff);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16),
                0 0 0 1px color-mix(in srgb, var(--at-ink, #000) 6%, transparent);
    color: var(--at-accent, #F07629);
}

.v-ptr__ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.v-ptr__track { fill: none; stroke: color-mix(in srgb, var(--at-ink, #000) 12%, transparent); stroke-width: 3; }
.v-ptr__prog {
    fill: none;
    stroke: var(--at-accent, #F07629);
    stroke-width: 3;
    stroke-linecap: round;
    transition: stroke-dashoffset 0.06s linear;
}

.v-ptr__arrow,
.v-ptr__check {
    position: absolute;
    font-size: 15px; line-height: 1;
    display: grid; place-items: center;
    transition: opacity 0.2s ease, transform 0.2s ease;
}
.v-ptr__arrow { color: var(--at-accent, #F07629); }
.v-ptr.is-ready  .v-ptr__arrow { transform: rotate(180deg); }   /* pull → release */
.v-ptr__check { color: var(--at-accent, #F07629); opacity: 0; transform: scale(0.4); }

/* Refreshing: arrow out, ring becomes a turning spinner arc. */
.v-ptr.is-refreshing .v-ptr__arrow { opacity: 0; }
.v-ptr.is-refreshing .v-ptr__prog  { stroke-dashoffset: 66px !important; transition: none; }
.v-ptr.is-refreshing .v-ptr__ring  { animation: vptr-spin 0.8s linear infinite; }
@keyframes vptr-spin { to { transform: rotate(270deg); } }

/* Done: complete the ring, swap in the check, give the disc a soft pop. */
.v-ptr.is-done .v-ptr__arrow { opacity: 0; }
.v-ptr.is-done .v-ptr__ring  { animation: none; }
.v-ptr.is-done .v-ptr__prog  { stroke-dashoffset: 0 !important; transition: stroke-dashoffset 0.25s ease; }
.v-ptr.is-done .v-ptr__check { opacity: 1; transform: scale(1); }
.v-ptr.is-done .v-ptr__disc  { animation: vptr-pop 0.42s var(--at-ease-out, ease); }
@keyframes vptr-pop { 0% { transform: scale(1); } 42% { transform: scale(1.12); } 100% { transform: scale(1); } }

@media (prefers-reduced-motion: reduce) {
    .v-ptr.is-refreshing .v-ptr__ring { animation-duration: 1.4s; }
    .v-ptr.is-done .v-ptr__disc { animation: none; }
}

/* =============================================================================
   COMPOSE (#addpost) — the writing surface
   ========================================================================== */
.v-compose__author {
    display: flex;
    align-items: center;
    gap: var(--at-s3);
    margin-bottom: var(--at-s3);
}

/* The field carries no chrome: on a sheet you are writing on, a bordered
   box around the words is one frame too many. */
.v-compose .v-compose__text,
.v-compose .v-compose__text:hover,
.v-compose .v-compose__text:focus {
    min-height: 7rem;
    padding: 0;
    border: 0;
    background: transparent;
    box-shadow: none;
    font-size: var(--at-text-lg);
    line-height: var(--at-leading-relaxed);
    resize: none;
}

.v-compose__preview:not(:empty) { margin-top: var(--at-s4); }

/* Tools sit on a rule beneath the work, with the one act at the far end. */
.v-compose__tools {
    display: flex;
    align-items: center;
    gap: var(--at-s2);
    justify-content: flex-start;
}

.v-compose__attach { cursor: pointer; color: var(--at-accent-ink); }
.v-compose__attach:hover { background-color: var(--at-accent-soft); color: var(--at-accent-ink); }

.v-compose__filename {
    min-width: 0;
    max-width: 12rem;
    font-size: var(--at-text-xs);
    color: var(--at-ink-muted);
}

.v-compose__count { margin: 0; }

/* --- Feed composer entry --------------------------------------------------- */
.v-feedprompt {
    display: flex;
    align-items: center;
    gap: var(--at-s3);
    width: 100%;
    margin-bottom: var(--at-s4);
    padding: var(--at-s3) var(--at-s4);
    border: 0;
    border-radius: var(--at-r-lg);
    background: var(--at-surface);
    box-shadow: var(--at-edge), var(--at-shadow-sm);
    text-align: start;
    cursor: pointer;
    transition: box-shadow var(--at-t-base) var(--at-ease-out),
                translate var(--at-t-base) var(--at-ease-out);
}

.v-feedprompt:hover  { translate: 0 -1px; box-shadow: var(--at-edge), var(--at-shadow-md); }
.v-feedprompt:active { translate: 0 1px; box-shadow: var(--at-edge), var(--at-shadow-xs); }

.v-feedprompt__text {
    flex: 1 1 auto;
    font-size: var(--at-text-md);
    color: var(--at-ink-muted);
}

/* A winner's name is the only gradient in the product, and it is ink. */
.winner-text,
.is-winner {
    background: linear-gradient(45deg, var(--at-gilt), var(--at-gilt-ink));
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

/* =============================================================================
   12 · RETAINED ENGINE STYLES
   -----------------------------------------------------------------------------
   Load-bearing rules for a JS engine or for server-rendered markup. Nothing
   here is decoration; changing a number here changes behaviour.
   ========================================================================== */

/* --- Icon plumbing (UIcons glyphs) ---------------------------------------- */
.fi {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    vertical-align: -0.125em;
}

.fi::before { line-height: 1; display: block; }

.fi-size-2xs { font-size: 0.625em; }
.fi-size-xs  { font-size: 0.75em; }
.fi-size-sm  { font-size: 0.875em; }
.fi-size-lg  { font-size: 1.25em; }
.fi-size-xl  { font-size: 1.5em; }
.fi-size-2xl,
.fi-size-2x  { font-size: 2em; }
.fi-size-3x  { font-size: 3em; }
.fi-lead     { font-size: var(--at-text-sm); margin-inline-end: var(--at-s1); }

/* --- Bootstrap utility re-dress (edit.php + server HTML) ------------------- */
.text-muted     { color: var(--at-ink-muted) !important; }
.text-secondary { color: var(--at-ink-secondary) !important; }
.text-dark      { color: var(--at-ink) !important; }
.bg-white,
.bg-light       { background-color: var(--at-surface) !important; }
.border, .border-top, .border-bottom { border-color: var(--at-hairline) !important; }

/* Legacy container normalisation — edit.php only, until it migrates. */
.container[class*="col-"] {
    flex: none;
    width: 100%;
    max-width: var(--at-measure-wide);
    margin-inline: auto;
}

/* --- Profile gallery -------------------------------------------------------
   A wall of squares. The tile is the unit: everything inside it — image,
   video, veil, moderation mark — fills the same frame, so the grid stays a
   grid no matter what the posts are. */
.gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
    gap: var(--at-s2);
}

@media (max-width: 36em) {
    /* Three across on a phone: the tile stays legible and the wall still
       reads as a wall. */
    .gallery { grid-template-columns: repeat(3, 1fr); gap: var(--at-s1); }
}

.gallery-wrapper {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: var(--at-r-md);
    background: var(--at-surface-sunken);
    box-shadow: var(--at-edge);
}

.gallery-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
    cursor: pointer;
    /* The image itself moves under the frame on hover; the tile holds still,
       so the grid's rhythm never breaks. */
    transition: transform var(--at-t-slow) var(--at-ease-out),
                filter var(--at-t-base) var(--at-ease-out);
}

@media (hover: hover) {
    .gallery-wrapper:hover .gallery-img { transform: scale(1.04); }
}

/* A tile announces what it holds without a label. */
.gallery-wrapper__kind {
    position: absolute;
    top: var(--at-s2);
    inset-inline-end: var(--at-s2);
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: var(--at-r-pill);
    background: var(--at-scrim);
    color: #fff;
    font-size: 0.6875rem;
    pointer-events: none;
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
}

@media (prefers-reduced-motion: reduce) {
    .gallery-wrapper:hover .gallery-img { transform: none; }
}

.gallery-wrapper .overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: inherit;
    background: var(--at-scrim);
    color: #fff;
    pointer-events: none;
}

/* Tab panels (the profile switcher's contract). */
.content-section { display: none; }
.content-section.active { display: block; }

/* --- NFT layer stage (market + profile engines) ---------------------------- */
.image-wrapper { position: relative; overflow: hidden; }

.stars-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 17.5px;
    z-index: 2;
    pointer-events: none;
    mask-image: radial-gradient(circle, rgba(255,255,255,0.9) 30%, rgba(255,255,255,0.5) 50%, transparent 100%);
    -webkit-mask-image: radial-gradient(circle, rgba(255,255,255,0.9) 30%, rgba(255,255,255,0.5) 50%, transparent 100%);
}

.star-icon {
    display: inline-block;
    font-size: 1rem;
    opacity: 0.8;
    transition: color 0.6s var(--at-ease-out), filter 0.8s var(--at-ease-out);
}

@media (max-width: 48em) {
    .star-icon { font-size: 0.8125rem; }
    .stars-overlay { gap: 11px; }
}

.centered-square {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100px;
    height: 100px;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 3;
}

.centered-item {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}

.body-item { z-index: 1; }
.eyes-item { z-index: 2; }
.bow-item  { z-index: 3; }
.nose-item { z-index: 4; }
.hat-item  { z-index: 5; }

.centered-img     { max-width: 130%; max-height: 130%; object-fit: contain; display: none; }
.centered-img-nft { max-width: 150%; max-height: 150%; object-fit: contain; display: none; }

.lazy-load { display: none; }
.lazy-load.loaded { display: block; }

.loading-spinner {
    width: 2.5rem;
    height: 2.5rem;
    margin: auto;
    /* The large member of the .v-spinner family: same neutral hairline
       ring + accent leader, scaled up. Not a tinted donut of its own. */
    border: 3px solid var(--at-hairline-strong);
    border-top-color: var(--at-accent);
    border-radius: 50%;
    animation: v-spin 1s linear infinite;
}

/* The layer-swap flourish the engines add as .spin. */
.spin { animation: v-flipblur 0.9s var(--at-ease-inout); will-change: transform, filter; }

@keyframes v-flipblur {
    0%   { transform: scaleX(1); filter: blur(0); }
    50%  { transform: scaleX(0); filter: blur(7px); }
    100% { transform: scaleX(1); filter: blur(0); }
}

/* --- The QR share card -----------------------------------------------------
   The identity as a scannable object: who it is on top, the code as the hero,
   a quiet brand line beneath. The card is what downloadQrImage() captures, so
   it is a self-contained composition. */
.v-qr { padding: 0 var(--at-s4) var(--at-s2); }

.v-qr__card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--at-s4);
    padding: var(--at-s6);
    border-radius: var(--at-r-lg);
    background: var(--at-surface-raised);
    box-shadow: var(--at-edge), var(--at-shadow-sm);
}

.v-qr__id {
    display: flex;
    align-items: center;
    gap: var(--at-s3);
    width: 100%;
    min-width: 0;
}

.v-qr__idtext { display: flex; flex-direction: column; min-width: 0; }

.v-qr__name {
    display: flex;
    align-items: center;
    gap: var(--at-s1);
    flex-wrap: wrap;
    font-size: var(--at-text-lg);
    font-weight: var(--at-weight-semibold);
    letter-spacing: var(--at-tracking-title);
    color: var(--at-ink);
    overflow-wrap: anywhere;
}

.v-qr__handle { font-size: var(--at-text-sm); color: var(--at-ink-muted); }

/* The code is always on white so any scanner reads it, whatever the theme. */
.v-qr__code {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    max-width: 15rem;
    aspect-ratio: 1;
    padding: var(--at-s4);
    border-radius: var(--at-r-md);
    background: #fff;
    box-shadow: var(--at-edge), var(--at-shadow-xs);
}

.v-qr__code canvas,
.v-qr__code svg,
.v-qr__code img { width: 100%; height: 100%; object-fit: contain; display: block; }

.v-qr__hint {
    margin: 0;
    font-size: var(--at-text-xs);
    letter-spacing: var(--at-tracking-meta);
    color: var(--at-ink-muted);
    text-align: center;
}

.v-qr__actions { flex-direction: column; gap: var(--at-s2); }

.v-qr__scanner { width: 100%; }
.v-qr__scanner #reader { width: 100% !important; border-radius: var(--at-r-md); overflow: hidden; }

/* --- Searchable people list (followers / following) ------------------------ */
.v-listsearch {
    position: relative;
    display: flex;
    align-items: center;
    margin-bottom: var(--at-s3);
}

.v-listsearch > i {
    position: absolute;
    inset-inline-start: var(--at-s3);
    font-size: var(--at-icon-sm);
    color: var(--at-ink-muted);
    pointer-events: none;
    line-height: 1;
}

.v-listsearch__input {
    width: 100%;
    min-height: var(--at-control-h);
    padding: var(--at-s2) var(--at-s3) var(--at-s2) calc(var(--at-s5) + var(--at-s5));
    border: 0;
    border-radius: var(--at-r-pill);
    background: var(--at-surface-sunken);
    color: var(--at-ink);
    font: inherit;
    font-size: var(--at-text-md);
    transition: background-color var(--at-t-fast) var(--at-ease-out),
                box-shadow var(--at-t-fast) var(--at-ease-out);
}

.v-listsearch__input::placeholder { color: var(--at-ink-muted); }
.v-listsearch__input:focus {
    outline: none;
    background: var(--at-surface-raised);
    box-shadow: 0 0 0 3px var(--at-focus-ring);
}
.v-listsearch__input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

.v-listsearch__empty { padding: var(--at-s6) var(--at-s4); text-align: center; }

/* --- Share circles (profile share sheet) ----------------------------------- */
.share-icon-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 3.5rem;
    height: 3.5rem;
    border: 0;
    border-radius: var(--at-r-pill);
    box-shadow: var(--at-shadow-xs);
    text-decoration: none;
    cursor: pointer;
    transition: box-shadow var(--at-t-base) var(--at-ease-out),
                translate var(--at-t-base) var(--at-ease-out);
}

.share-icon-btn:hover  { translate: 0 -2px; box-shadow: var(--at-shadow-md); color: inherit; }
.share-icon-btn:active { translate: 0 1px; box-shadow: var(--at-shadow-xs); }

/* --- Like bursts (custom.js injects these) --------------------------------- */
.heart-firework {
    position: absolute;
    z-index: var(--at-z-toast);
    color: var(--at-like);
    font-size: 0.875rem;
    pointer-events: none;
    animation: v-heart-burst 0.9s var(--at-ease-out) forwards;
}

@keyframes v-heart-burst {
    0%   { opacity: 1; transform: translate(0, 0) scale(0.6); }
    100% { opacity: 0; transform: translate(var(--x), var(--y)) scale(1.25); }
}

.big-heart {
    position: absolute;
    z-index: var(--at-z-toast);
    font-size: 5rem;
    color: var(--at-like);
    pointer-events: none;
    transform: translate(-50%, -50%) scale(0);
    opacity: 0;
    animation: v-bigheart 0.7s var(--at-ease-out) forwards;
}

@keyframes v-bigheart {
    0%   { transform: translate(-50%, -50%) scale(0.2); opacity: 0; }
    25%  { transform: translate(-50%, -50%) scale(1.2); opacity: 1; }
    55%  { transform: translate(-50%, -55%) scale(1);   opacity: 1; }
    100% { transform: translate(-50%, -70%) scale(0.9); opacity: 0; }
}

/* --- Compose-preview veils (the applyBlur contract) ------------------------ */
.blurred-media {
    filter: blur(40px) brightness(0.72);
    pointer-events: none;
    transition: filter var(--at-t-slower) var(--at-ease-out);
}

.unblurred-media { filter: none; transition: filter var(--at-t-slower) var(--at-ease-out); }

.blur-overlay-icon {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 9;
    transform: translate(-50%, -50%);
    font-size: 3rem;
    color: #fff;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--at-t-slower) var(--at-ease-out);
}

.blur-overlay-icon.visible { opacity: 1; }

.custom-gif-player,
.custom-video-player {
    position: relative;
    overflow: hidden;
    border-radius: var(--at-r-md);
    background: var(--at-surface-sunken);
}

/* --- Chat composer plumbing ------------------------------------------------ */
.input-mirror-wrapper {
    position: absolute;
    visibility: hidden;
    max-width: 100%;
    padding: 10px 15px;
    font-family: inherit;
    font-size: 1rem;
    line-height: 1.5;
    white-space: pre-wrap;
    word-wrap: break-word;
    box-sizing: border-box;
}

#inputMirror { display: inline-block; }

.typing-indicator { display: inline-block; padding: var(--at-s2); }

.typing-indicator span {
    display: inline-block;
    width: 0.5rem;
    height: 0.5rem;
    margin: 0 2px;
    border-radius: 50%;
    background-color: var(--at-ink-faint);
    opacity: 0.4;
    animation: v-typing 1s var(--at-ease-inout) infinite;
}

.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }

@keyframes v-typing {
    0%, 100% { transform: translateY(0);    opacity: 0.4; }
    50%      { transform: translateY(-5px); opacity: 1; }
}

.no-select { user-select: none; -webkit-user-select: none; }


/* =============================================================================
   DOCUMENT PAGES  ·  Terms of Use, Cookies, Bherda ID, any long-form policy
   -----------------------------------------------------------------------------
   A calm reading surface that belongs to the same system as the rest of the app.
   The reading column holds a comfortable measure; a sticky "On this page" rail
   appears beside it on wide viewports. Every colour comes from a token, so light
   and dark are automatic, and the layout is built on logical properties so the
   Hebrew document mirrors with the rest of the UI.

   The shipped source documents (assets/pages/docs/*.html) are legacy markup:
   every block is an <h1> carrying a Bootstrap size class (.h2 / .h4 / .h5) and
   the blocks are wrapped in empty .d-flex rows. We do not rewrite that translated
   policy text — instead .v-prose styles those size classes as a real hierarchy
   and neutralises the flex wrappers, so the same content reads as a proper
   document.
   ========================================================================== */

/* The page sits in a .v-shell--wide (≈60rem inner) so there is room for a table
   of contents beside the reading column on wide viewports; on a phone the shell
   is full width and the document reads as a single column. */
.v-doc {
    width: 100%;
    padding: var(--at-s5) 0 var(--at-s9);
}

/* ---- header: back + title ------------------------------------------------ */
.v-doc__head {
    display: flex;
    align-items: center;
    gap: var(--at-s3);
    margin-bottom: var(--at-s6);
}

.v-doc__title {
    margin: 0;
    font-family: var(--at-font-heading);
    font-size: var(--at-text-2xl);
    font-weight: 600;
    line-height: var(--at-leading-tight);
    color: var(--at-ink);
    letter-spacing: -0.01em;
}

/* ---- layout: reading column, plus a TOC rail on wide viewports ----------- */
.v-doc__grid {
    display: block;
}

.v-doc__toc {
    margin-bottom: var(--at-s5);
}

/* The article is a plain card holding the prose; the measure is capped so lines
   never run too long to track comfortably. */
.v-doc .v-prose {
    max-width: 46rem;
}

@media (min-width: 64em) {
    .v-doc__grid {
        display: grid;
        grid-template-columns: 13rem minmax(0, 40rem);
        gap: var(--at-s7);
        align-items: start;
        justify-content: center;
    }
    .v-doc__toc {
        position: sticky;
        top: calc(var(--at-topbar-h) + var(--at-s4));
        margin-bottom: 0;
        max-height: calc(100dvh - var(--at-topbar-h) - var(--at-s7));
        overflow-y: auto;
        overscroll-behavior: contain;
        padding-inline-end: var(--at-s2);
    }
    .v-doc .v-prose {
        max-width: none;
    }
}

/* Below the Floor Plan the rail would only push the reading start down, so the
   document reads on its own and the TOC is not shown. */
@media (max-width: 63.98em) {
    .v-doc__toc { display: none !important; }
}

.v-doc__toc-title {
    margin: 0 0 var(--at-s2);
    font-size: var(--at-text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--at-ink-muted);
}

.v-doc__toc-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
    border-inline-start: 2px solid var(--at-hairline-strong);
}

.v-doc__toc-link {
    display: block;
    padding: var(--at-s2) var(--at-s3);
    margin-inline-start: -2px;
    border-inline-start: 2px solid transparent;
    font-size: var(--at-text-sm);
    line-height: var(--at-leading-snug);
    color: var(--at-ink-secondary);
    text-decoration: none;
    border-radius: 0 var(--at-r-sm) var(--at-r-sm) 0;
    transition: color var(--at-t-fast, .15s) var(--at-ease-out),
                background var(--at-t-fast, .15s) var(--at-ease-out);
}

.v-doc__toc-link:hover {
    color: var(--at-ink);
    background: var(--at-surface-hover);
}

.v-doc__toc-link.is-current {
    color: var(--at-accent-ink);
    border-inline-start-color: var(--at-accent);
    background: var(--at-accent-soft);
    font-weight: 600;
}

/* =============================================================================
   .v-prose  ·  the typography of a document
   ========================================================================== */

.v-prose {
    color: var(--at-ink-secondary);
    font-size: var(--at-text-md);
    line-height: var(--at-leading-relaxed);
    -webkit-hyphens: auto;
    hyphens: auto;
    overflow-wrap: break-word;
}

/* Legacy layout cruft: the source wraps every block in a flex row (often
   centred). Inside a document that is just a paragraph, so flatten the wrappers
   back to normal flow. Bootstrap sets these with !important, so we match it —
   scoped to .v-prose, this never touches app chrome. */
.v-prose .d-flex { display: block !important; }
.v-prose .justify-content-center,
.v-prose .justify-content-between { justify-content: flex-start !important; }
.v-prose .text-center { text-align: start !important; }

/* The document repeats its own title as an in-body .h2; the page header above
   already states it, so the duplicate is hidden. */
.v-prose > .h2,
.v-prose .h2 { display: none; }

/* Section headings — real <h2>/<h3> and the legacy .h4 size class alike. */
.v-prose h1:not([class*="h"]),
.v-prose h2,
.v-prose h3,
.v-prose .h3,
.v-prose .h4 {
    color: var(--at-ink);
    font-family: var(--at-font-heading);
    font-weight: 600;
    line-height: var(--at-leading-snug);
    letter-spacing: -0.005em;
    margin: var(--at-s7) 0 var(--at-s3);
    scroll-margin-top: calc(var(--at-topbar-h) + var(--at-s4));
}

.v-prose h2,
.v-prose .h4 { font-size: var(--at-text-xl); }
.v-prose h3,
.v-prose .h3 { font-size: var(--at-text-lg); }

/* The first section heading should not push a big gap under the page title. */
.v-prose > :first-child,
.v-prose .d-flex:first-child > :first-child { margin-top: 0; }

/* Body copy — real <p> and the legacy .h5 size class (the bulk of the text). */
.v-prose p,
.v-prose .h5,
.v-prose h1.h5 {
    font-size: var(--at-text-md);
    font-weight: 400;
    line-height: var(--at-leading-relaxed);
    color: var(--at-ink-secondary);
    margin: 0 0 var(--at-s4);
}

/* Emphasis carried by <b>/<strong> reads as a stronger ink, not just bolder. */
.v-prose b,
.v-prose strong { color: var(--at-ink); font-weight: 600; }

.v-prose em,
.v-prose i { font-style: italic; }

/* Links: prose underlines, and the legacy .orange-link joins the token accent
   instead of a hard-coded orange. */
.v-prose a,
.v-prose .orange-link {
    color: var(--at-accent-ink);
    text-decoration: underline;
    text-underline-offset: 2px;
    text-decoration-color: var(--at-hairline-strong);
    transition: text-decoration-color var(--at-t-fast, .15s) var(--at-ease-out);
}
.v-prose a:hover,
.v-prose .orange-link:hover {
    color: var(--at-accent-hover);
    text-decoration-color: currentColor;
}

/* Lists (for real <ul>/<ol>, and any future clean documents). */
.v-prose ul,
.v-prose ol {
    margin: 0 0 var(--at-s4);
    padding-inline-start: var(--at-s6);
}
.v-prose li { margin-bottom: var(--at-s2); }
.v-prose li::marker { color: var(--at-ink-muted); }

/* Quotes. */
.v-prose blockquote {
    margin: var(--at-s5) 0;
    padding: var(--at-s2) var(--at-s5);
    border-inline-start: 3px solid var(--at-accent);
    background: var(--at-surface-sunken);
    border-radius: 0 var(--at-r-sm) var(--at-r-sm) 0;
    color: var(--at-ink);
    font-style: italic;
}
.v-prose blockquote p:last-child { margin-bottom: 0; }

/* Code. */
.v-prose code {
    font-family: var(--at-font-mono);
    font-size: 0.875em;
    padding: 0.1em 0.4em;
    border-radius: var(--at-r-xs, 6px);
    background: var(--at-surface-sunken);
    color: var(--at-ink);
}
.v-prose pre {
    margin: var(--at-s5) 0;
    padding: var(--at-s4);
    border-radius: var(--at-r-md);
    background: var(--at-surface-sunken);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
.v-prose pre code { padding: 0; background: none; font-size: var(--at-text-sm); }

/* Tables — scroll inside their own box rather than widening the page. */
.v-prose table {
    display: block;
    width: 100%;
    max-width: 100%;
    overflow-x: auto;
    border-collapse: collapse;
    margin: var(--at-s5) 0;
    font-size: var(--at-text-sm);
}
.v-prose th,
.v-prose td {
    padding: var(--at-s2) var(--at-s3);
    border: 1px solid var(--at-hairline-strong);
    text-align: start;
    vertical-align: top;
}
.v-prose th {
    background: var(--at-surface-sunken);
    color: var(--at-ink);
    font-weight: 600;
}

/* Rules and media. */
.v-prose hr {
    border: 0;
    border-top: 1px solid var(--at-hairline-strong);
    margin: var(--at-s7) 0;
}
.v-prose img {
    border-radius: var(--at-r-md);
    margin: var(--at-s4) 0;
}

/* RTL (Hebrew) reading tweak: keep the document left-off nothing — logical
   properties already mirror it; hyphenation is disabled where it is unwanted. */
.v-prose[dir="rtl"] { -webkit-hyphens: none; hyphens: none; }

/* Phone comfort: a touch more breathing room is not needed (the card padding
   handles it), but pull the reading size to the responsive body scale and let
   long unbreakable strings wrap. */
@media (max-width: 47.98em) {
    .v-doc { padding: var(--at-s4) var(--at-s3) var(--at-s8); }
    .v-prose h2,
    .v-prose .h4 { font-size: var(--at-text-lg); margin-top: var(--at-s6); }
}


/* =============================================================================
   MEDIA VIEWER  ·  assets/js/video-player.js
   -----------------------------------------------------------------------------
   Inline post media carries a custom overlay (never the native UI): a duration
   pill, a loading spinner and a play glyph when paused. Videos open in the
   browser's NATIVE fullscreen; images open in the lightbox below.
   ========================================================================== */

/* Force-hide native <video> chrome on every surface... */
video[data-vplayer]::-webkit-media-controls,
video[data-vplayer]::-webkit-media-controls-enclosure,
video[data-vplayer]::-webkit-media-controls-panel { display: none !important; }
video[data-vplayer] { outline: none; }
/* ...but a video IN native fullscreen must show them again. */
video[data-vplayer]:fullscreen::-webkit-media-controls,
video[data-vplayer]:fullscreen::-webkit-media-controls-enclosure,
video[data-vplayer]:fullscreen::-webkit-media-controls-panel { display: flex !important; }
video[data-vplayer]:fullscreen { object-fit: contain; background: #000; }

/* Scroll lock while any media surface is open. */
.v-media-lock, .v-media-lock body { overflow: hidden !important; touch-action: none; }

/* =============================================================================
   UNIFIED MEDIA LOADING — skeleton shimmer + reveal  (media-loader.js)
   -----------------------------------------------------------------------------
   The skeleton (.m-skel) is an overlay inside the media's own sized wrapper, so
   there is zero layout shift and the shape/rounding are inherited. The media
   (.m-media) sits above it and fades + settles in once decoded. Theme-aware; the
   base is a theme surface token and only the sweep colour flips per mode.
   ========================================================================== */
:root { --m-skel-sheen: rgba(0, 0, 0, 0.05); }
:root[data-theme="dark"], :root[data-theme="night"] { --m-skel-sheen: rgba(255, 255, 255, 0.06); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --m-skel-sheen: rgba(255, 255, 255, 0.06); } }

.m-skel {
    position: absolute; inset: 0; z-index: 0;
    border-radius: inherit;
    background-color: var(--at-surface-sunken);
    background-image: linear-gradient(100deg, transparent 20%, var(--m-skel-sheen) 50%, transparent 80%);
    background-size: 220% 100%; background-repeat: no-repeat;
    animation: m-shimmer 1.25s var(--at-ease-inout) infinite;
    pointer-events: none;
}
.m-skel--out { opacity: 0; transition: opacity 0.3s var(--at-ease-out); }
@keyframes m-shimmer { from { background-position: 220% 0; } to { background-position: -120% 0; } }

/* The media fades + subtly scales in. Only ever applied by JS, and JS guarantees
   .m-in (synchronously for cached media, on load/decode otherwise, or via an
   8s fail-open timeout) — so content is never stranded invisible. */
.m-media {
    position: relative; z-index: 1;
    opacity: 0;
    transform: scale(1.02) translateZ(0);
    transition: opacity 0.45s var(--at-ease-out), transform 0.55s var(--at-ease-out);
    -webkit-backface-visibility: hidden; backface-visibility: hidden;
}
.m-media.m-in    { opacity: 1; transform: translateZ(0); }
.m-media.m-error { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
    .m-skel { animation: none; }
    .m-media { transform: none; transition: opacity 0.2s linear; }
}

/* =============================================================================
   MEDIA PROTECTION — media is app content, not a saveable web asset
   -----------------------------------------------------------------------------
   No iOS long-press "Save Image" callout, no drag ghost, no selection on media.
   Text fields and readable copy are untouched (the selection model owns those).
   The context menu itself is already suppressed off editable/selected text by
   lifecycle.js — this adds the iOS-specific callout + drag that CSS must carry.
   ========================================================================== */
img, video {
    -webkit-touch-callout: none;
    -webkit-user-drag: none;
    -khtml-user-drag: none;
}
img { -webkit-user-select: none; user-select: none; }

/* Kill accidental double-tap page zoom app-wide while KEEPING pinch-zoom for
   accessibility in a normal browser tab. (The installed PWA additionally locks
   the viewport in lifecycle.js; the image viewer keeps its own pinch via
   touch-action:none.) */
body { touch-action: manipulation; }

/* =============================================================================
   MEDIA PREVIEW OVERLAY BAR  (compose) — no collision with video chrome
   -----------------------------------------------------------------------------
   Top-right keeps the video duration badge; centre keeps play/spinner. All
   authoring controls (Edit + the safety/spoiler menu) live on ONE bottom bar, so
   they can never overlap the badge, the play button or the loading indicator.
   ========================================================================== */
.v-compose__mediabar {
    position: absolute; inset-inline: 0; bottom: 0; z-index: 4;
    display: flex; align-items: center; justify-content: space-between; gap: var(--at-s2);
    padding: var(--at-s2) var(--at-s3);
    background: linear-gradient(to top, rgba(0,0,0,0.55), rgba(0,0,0,0));
    pointer-events: none;
}
.v-compose__mediabar > * { pointer-events: auto; }
.v-compose__ovbtn {
    display: inline-flex; align-items: center; gap: var(--at-s2);
    height: 2.1rem; padding: 0 var(--at-s3);
    border: 0; border-radius: var(--at-r-pill);
    background: rgba(12,12,14,0.62); color: #fff;
    -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
    font-size: var(--at-text-xs); font-weight: var(--at-weight-medium); cursor: pointer;
    transition: background var(--at-t-fast) var(--at-ease-out), transform var(--at-t-fast) var(--at-ease-out);
}
.v-compose__ovbtn:hover { background: rgba(24,24,28,0.78); }
.v-compose__ovbtn:active { transform: scale(0.96); }
.v-compose__ovbtn i { font-size: 0.95rem; }
.v-compose__mediabar .dropdown { position: static; }        /* menu anchors to the chip, not the corner */

/* =============================================================================
   MEDIA STUDIO — the built-in editor (media-editor.js)
   -----------------------------------------------------------------------------
   A dark, native-feeling editor. Desktop: a workspace — big preview stage on the
   left, a tools rail on the right, the video timeline + transport under the
   stage. Phone: full-screen, preview on top, tools + timeline as a bottom deck.
   ONE shell drives images (Filters/Adjust/Crop) and videos (Trim/Filters/Adjust/
   Cover) with undo/redo/reset and a processing overlay for the video render.
   ========================================================================== */
.mstudio {
    position: fixed; inset: 0; z-index: var(--at-z-studio);
    display: flex; align-items: center; justify-content: center;
    background: rgba(2,3,5,0.82);
    -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
    opacity: 0; transition: opacity 0.2s var(--at-ease-out);
    touch-action: none; color-scheme: dark;
}
.mstudio.is-open { opacity: 1; }
.mstudio[hidden] { display: none; }

/* The editor commits to a dark chrome regardless of the app theme. */
.mstudio__dialog {
    --ed-ink: #f4f5f7; --ed-ink-2: #a9adb6; --ed-ink-3: #6d717b;
    --ed-surface: #131417; --ed-raised: #1c1e22; --ed-line: rgba(255,255,255,0.08);
    display: flex; flex-direction: column;
    width: min(1060px, 97vw); height: min(90dvh, 50rem);
    background: var(--ed-surface); color: var(--ed-ink);
    border-radius: var(--at-r-xl); overflow: hidden;
    box-shadow: 0 40px 120px -30px rgba(0,0,0,0.8), 0 0 0 1px var(--ed-line);
    transform: translateY(10px) scale(0.99); transition: transform 0.26s var(--at-ease-spring);
}
.mstudio.is-open .mstudio__dialog { transform: none; }

/* --- header: cancel · undo/redo/reset · apply --- */
.mstudio__head {
    flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: var(--at-s2);
    padding: var(--at-s2) var(--at-s3); border-bottom: 1px solid var(--ed-line);
    background: var(--ed-raised);
}
.mstudio__btn {
    border: 0; background: transparent; color: var(--ed-ink-2);
    font-size: var(--at-text-sm); font-weight: var(--at-weight-medium);
    padding: var(--at-s2) var(--at-s4); border-radius: var(--at-r-pill); cursor: pointer;
    transition: background var(--at-t-fast) var(--at-ease-out), color var(--at-t-fast) var(--at-ease-out);
}
.mstudio__btn:hover { background: rgba(255,255,255,0.06); color: var(--ed-ink); }
.mstudio__apply { color: #0a0a0a; background: var(--at-accent); font-weight: var(--at-weight-semibold); }
.mstudio__apply:hover { background: var(--at-accent-hover, var(--at-accent)); color: #0a0a0a; }
.mstudio__hist { display: flex; gap: 2px; }
.mstudio__ibtn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 2.2rem; height: 2.2rem; border: 0; border-radius: var(--at-r-md);
    background: transparent; color: var(--ed-ink-2); font-size: 1rem; cursor: pointer;
    transition: background var(--at-t-fast) var(--at-ease-out), color var(--at-t-fast) var(--at-ease-out), transform var(--at-t-fast) var(--at-ease-out);
}
.mstudio__ibtn:hover { background: rgba(255,255,255,0.07); color: var(--ed-ink); }
.mstudio__ibtn:active { transform: scale(0.92); }
.mstudio__ibtn[disabled] { opacity: 0.32; cursor: default; }

/* --- body: stage rail | tools rail --- */
.mstudio__body { flex: 1 1 auto; display: flex; min-height: 0; }
.mstudio__stagewrap { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; min-height: 0; background: #000; }
.mstudio__stage {
    flex: 1 1 auto; position: relative; min-height: 0;
    display: flex; align-items: center; justify-content: center; overflow: hidden;
    background: radial-gradient(130% 130% at 50% 0%, #17181c 0%, #0b0b0d 70%, #050506 100%);
    touch-action: none;
}
.mstudio__stageinner { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.mstudio__img, .mstudio__video {
    position: absolute; top: 50%; left: 50%;
    max-width: 100%; max-height: 100%; width: auto; height: auto;
    transform: translate(-50%, -50%);
    -webkit-user-select: none; user-select: none; -webkit-user-drag: none;
    will-change: transform, filter;
}
.mstudio__crop {
    position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
    border: 1.5px solid rgba(255,255,255,0.92); border-radius: 3px; z-index: 2;
    box-shadow: 0 0 0 100vmax rgba(0,0,0,0.5); pointer-events: none;
}
.mstudio__crop[hidden] { display: none; }

/* --- transport (video controls) --- */
.mstudio__transport {
    flex: 0 0 auto; display: flex; align-items: center; gap: var(--at-s2);
    padding: var(--at-s2) var(--at-s3); background: var(--ed-surface); border-top: 1px solid var(--ed-line);
    color: var(--ed-ink);
}
.mstudio__transport[hidden] { display: none; }
.mstudio__transport .mstudio__ibtn { color: var(--ed-ink); }
.mstudio__transport .mstudio__ibtn:hover { background: rgba(255,255,255,0.08); }
.mstudio__time { font-size: var(--at-text-xs); color: var(--ed-ink-2); font-variant-numeric: tabular-nums; }
.mstudio__spacer { flex: 1 1 auto; }
.mstudio__vol { width: 5.5rem; accent-color: var(--at-accent); }

/* --- timeline (thumbnail trim) --- */
.mstudio__timeline { flex: 0 0 auto; padding: var(--at-s2) var(--at-s3) var(--at-s3); background: var(--ed-surface); }
.mstudio__timeline[hidden] { display: none; }
.mstudio__track {
    position: relative; height: 3.2rem; border-radius: var(--at-r-md); overflow: hidden;
    background: #000; cursor: pointer; touch-action: none;
}
.mstudio__thumbs { position: absolute; inset: 0; display: flex; }
.mstudio__thumb { flex: 1 1 0; min-width: 0; background-size: cover; background-position: center; background-color: #222; border-inline-end: 1px solid rgba(0,0,0,0.25); }
.mstudio__trange {
    position: absolute; top: 0; bottom: 0; left: 0; width: 100%;
    border: 2px solid var(--at-accent); border-radius: var(--at-r-md);
    box-shadow: 0 0 0 100vmax rgba(0,0,0,0.55); pointer-events: none; z-index: 1;
}
.mstudio__handle {
    position: absolute; top: 0; bottom: 0; width: 1.15rem; transform: translateX(-50%);
    display: flex; align-items: center; justify-content: center;
    background: var(--at-accent); z-index: 3; cursor: ew-resize; touch-action: none;
}
.mstudio__handle--start { border-radius: var(--at-r-md) 0 0 var(--at-r-md); }
.mstudio__handle--end { border-radius: 0 var(--at-r-md) var(--at-r-md) 0; }
.mstudio__handle i { width: 3px; height: 40%; border-radius: 2px; background: rgba(0,0,0,0.55); }
.mstudio__playhead { position: absolute; top: -2px; bottom: -2px; left: 0; width: 2px; background: #fff; box-shadow: 0 0 6px rgba(0,0,0,0.8); z-index: 2; pointer-events: none; }
.mstudio__trimtime { text-align: center; font-size: var(--at-text-xs); color: var(--ed-ink-2); font-variant-numeric: tabular-nums; margin-top: var(--at-s2); }

/* --- tools rail --- */
.mstudio__side {
    flex: 0 0 20rem; display: flex; flex-direction: column; min-height: 0;
    background: var(--ed-raised); border-inline-start: 1px solid var(--ed-line);
}
.mstudio__tabs { flex: 0 0 auto; display: flex; gap: var(--at-s1); padding: var(--at-s3) var(--at-s3) var(--at-s2); overflow-x: auto; scrollbar-width: none; }
.mstudio__tabs::-webkit-scrollbar { display: none; }
.mstudio__tab {
    flex: 0 0 auto; display: inline-flex; flex-direction: column; align-items: center; gap: 3px;
    border: 0; background: transparent; color: var(--ed-ink-3);
    padding: var(--at-s2) var(--at-s3); border-radius: var(--at-r-md);
    font-size: var(--at-text-xs); cursor: pointer;
    transition: color var(--at-t-fast) var(--at-ease-out), background var(--at-t-fast) var(--at-ease-out);
}
.mstudio__tab i { font-size: 1.15rem; }
.mstudio__tab.is-active { color: #0a0a0a; background: var(--at-accent); }
.mstudio__panel { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: var(--at-s3); -webkit-overflow-scrolling: touch; animation: mstudio-fade 0.22s var(--at-ease-out); }
@keyframes mstudio-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.mstudio__hint { font-size: var(--at-text-xs); color: var(--ed-ink-2); line-height: var(--at-leading-relaxed); }

/* filter cards */
.mstudio__filters { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--at-s3); }
.mstudio__filter { display: flex; flex-direction: column; align-items: center; gap: var(--at-s1); border: 0; background: transparent; cursor: pointer; padding: 0; }
.mstudio__filterthumb { width: 100%; aspect-ratio: 1; border-radius: var(--at-r-md); overflow: hidden; border: 2px solid transparent; background: #222; transition: border-color var(--at-t-fast) var(--at-ease-out), transform var(--at-t-fast) var(--at-ease-out); }
.mstudio__filter:active .mstudio__filterthumb { transform: scale(0.97); }
.mstudio__filter.is-active .mstudio__filterthumb { border-color: var(--at-accent); }
.mstudio__filterthumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mstudio__filterlabel { font-size: var(--at-text-xs); color: var(--ed-ink-2); }
.mstudio__filter.is-active .mstudio__filterlabel { color: var(--ed-ink); font-weight: var(--at-weight-medium); }

/* sliders / chips */
.mstudio__adjust, .mstudio__crop-tools, .mstudio__cover { display: flex; flex-direction: column; gap: var(--at-s3); }
.mstudio__slider { display: grid; grid-template-columns: 5rem 1fr 2rem; align-items: center; gap: var(--at-s2); }
.mstudio__slabel { font-size: var(--at-text-xs); color: var(--ed-ink-2); }
.mstudio__sval { font-size: var(--at-text-xs); color: var(--ed-ink-3); text-align: end; font-variant-numeric: tabular-nums; }
.mstudio__slider input[type="range"], .mstudio__range, .mstudio__vol { width: 100%; accent-color: var(--at-accent); }
.mstudio__chiprow { display: flex; gap: var(--at-s2); flex-wrap: wrap; }
.mstudio__chip { border: 1px solid var(--ed-line); background: rgba(255,255,255,0.04); color: var(--ed-ink-2); border-radius: var(--at-r-pill); padding: var(--at-s1) var(--at-s3); font-size: var(--at-text-xs); cursor: pointer; }
.mstudio__chip.is-active { background: var(--at-accent); color: #0a0a0a; border-color: var(--at-accent); }
.mstudio__reset { align-self: flex-start; border: 1px solid var(--ed-line); background: rgba(255,255,255,0.05); color: var(--ed-ink); border-radius: var(--at-r-pill); padding: var(--at-s2) var(--at-s4); font-size: var(--at-text-xs); cursor: pointer; }

/* processing overlay */
.mstudio__processing {
    position: absolute; inset: 0; z-index: 10; display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: var(--at-s3);
    background: rgba(6,7,9,0.86); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
    color: var(--ed-ink);
}
.mstudio__processing[hidden] { display: none; }
.mstudio__spinner { width: 2.6rem; height: 2.6rem; border-radius: 999px; border: 3px solid rgba(255,255,255,0.2); border-top-color: var(--at-accent); animation: v-video-spin 0.8s linear infinite; }
.mstudio__proglabel { font-size: var(--at-text-sm); color: var(--ed-ink-2); }
.mstudio__progbar { width: 12rem; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.12); overflow: hidden; }
.mstudio__progbar > span { display: block; height: 100%; width: 0; background: var(--at-accent); transition: width 0.15s linear; }

/* --- phone: full-screen, preview on top, tools as a bottom deck --- */
@media (max-width: 47.98em) {
    .mstudio { background: #000; -webkit-backdrop-filter: none; backdrop-filter: none; }
    .mstudio__dialog { width: 100vw; height: 100dvh; border-radius: 0; }
    .mstudio__head { padding-top: calc(var(--at-s2) + env(safe-area-inset-top, 0px)); }
    .mstudio__body { flex-direction: column; }
    .mstudio__side {
        flex: 0 0 auto; max-height: 46dvh;
        border-inline-start: 0; border-top: 1px solid var(--ed-line);
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }
    .mstudio__tab { padding: var(--at-s2) var(--at-s4); }        /* larger touch targets */
    .mstudio__filters { grid-template-columns: repeat(3, 1fr); }
    .mstudio__slider { grid-template-columns: 4.5rem 1fr 2rem; }
}

@media (prefers-reduced-motion: reduce) {
    .mstudio, .mstudio__dialog, .mstudio__panel { transition: none; animation: none; }
    .mstudio__spinner { animation-duration: 1.4s; }
}

/* ---- inline overlay: badge, spinner, play glyph, hover -------------------- */
.v-video__badge {
    position: absolute; top: var(--at-s2); inset-inline-end: var(--at-s2); z-index: 4;
    padding: 0.14rem 0.5rem; border-radius: 999px;
    background: rgba(11, 11, 10, 0.55);
    -webkit-backdrop-filter: blur(10px) saturate(1.3); backdrop-filter: blur(10px) saturate(1.3);
    color: #fff; font-family: var(--at-font); font-size: var(--at-text-xs); font-weight: 600;
    font-variant-numeric: tabular-nums; letter-spacing: 0.01em; line-height: 1.35;
    pointer-events: none; opacity: 0; transform: translateY(-2px);
    transition: opacity 0.3s var(--at-ease-out), transform 0.3s var(--at-ease-out);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.28);
}
.v-video__badge.is-visible { opacity: 1; transform: none; }
.is-veiled > .v-video__badge { display: none; }

/* loading spinner — shown while .is-buffering on the media host */
.v-video__spin {
    position: absolute; top: 50%; left: 50%; z-index: 4;
    width: 2rem; height: 2rem; margin: -1rem 0 0 -1rem;
    border-radius: 999px;
    border: 2.5px solid rgba(255, 255, 255, 0.28);
    border-top-color: #fff;
    opacity: 0; pointer-events: none;
    transition: opacity 0.2s var(--at-ease-out);
    animation: v-video-spin 0.7s linear infinite;
}
.v-has-video.is-buffering > .v-video__spin { opacity: 1; }
.is-veiled > .v-video__spin { display: none; }
@keyframes v-video-spin { to { transform: rotate(360deg); } }

/* play glyph — shown when the preview is paused (and not buffering) */
.v-video__play {
    position: absolute; top: 50%; left: 50%; z-index: 4;
    width: 3.2rem; height: 3.2rem; margin: -1.6rem 0 0 -1.6rem;
    display: flex; align-items: center; justify-content: center;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.42);
    -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
    color: #fff; font-size: 1.25rem; padding-inline-start: 0.15rem;
    opacity: 0; transform: scale(0.9); pointer-events: none;
    transition: opacity 0.22s var(--at-ease-out), transform 0.22s var(--at-ease-out);
}
.v-has-video.is-paused:not(.is-buffering) > .v-video__play { opacity: 1; transform: scale(1); }
.is-veiled > .v-video__play { display: none; }

/* a soft hover lift on the media frame */
.v-vitrine.v-has-video, .gallery-wrapper.v-has-video, .post-img, .gallery-img {
    transition: transform 0.35s var(--at-ease-out), box-shadow 0.35s var(--at-ease-out);
}
@media (hover: hover) {
    .v-post .v-vitrine.v-has-video:hover { transform: translateY(-1px); }
    .post-img { cursor: pointer; }
}

/* =============================================================================
   YOUTUBE — native link posts (youtube.js + components/youtube_media.php)
   -----------------------------------------------------------------------------
   A YouTube post is a normal media attachment: it sits in the same .v-vitrine
   the feed uses, so rounding, spacing, veil and the card frame are inherited.
   What we add is a fixed aspect frame, a thumbnail-first facade with a Bherda
   play overlay, and the lazily-swapped <iframe>. No iframe exists until play.
   ========================================================================== */
.v-yt {
    position: relative;
    aspect-ratio: 16 / 9;
    cursor: pointer;
    background: var(--at-surface-sunken);
}
/* Shorts are vertical — keep a real 9:16 frame but cap it so it never dominates
   the column; letterbox on the sunken surface, centred. */
.v-yt--short {
    aspect-ratio: 9 / 16;
    max-width: min(100%, 20rem);
    margin-inline: auto;
}

/* Thumbnail fills the frame (hqdefault cover-crops its letterbox away). Overrides
   the generic .v-vitrine > img sizing. */
.v-yt .v-yt__thumb {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    max-height: none;
    object-fit: cover;
    display: block;
    z-index: 1;
}

/* Play overlay — the same blurred glass disc as the inline video player, but
   always present (this is a facade, not an autoplaying preview). */
.v-yt__play {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    background: transparent;
    cursor: pointer;
    padding: 0;
}
.v-yt__playring {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3.6rem;
    height: 3.6rem;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.46);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    color: #fff;
    font-size: 1.35rem;
    padding-inline-start: 0.16rem;   /* optical centre of the play glyph */
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.32);
    transition: transform var(--at-t-fast) var(--at-ease-out),
                background-color var(--at-t-fast) var(--at-ease-out);
}
@media (hover: hover) {
    .v-yt:hover .v-yt__playring { transform: scale(1.06); background: rgba(0, 0, 0, 0.58); }
}
.v-yt__play:active .v-yt__playring { transform: scale(0.94); }
.v-yt__play:focus-visible { outline: none; }
.v-yt__play:focus-visible .v-yt__playring {
    outline: 3px solid var(--at-accent);
    outline-offset: 3px;
}

/* Source badge — bottom-start, reads "▶ YouTube" in the accent-neutral glass. */
.v-yt__brand {
    position: absolute;
    inset-block-end: var(--at-s2);
    inset-inline-start: var(--at-s2);
    z-index: 3;
    display: inline-flex;
    align-items: center;
    gap: 0.35em;
    padding: 0.2rem 0.55rem;
    border-radius: var(--at-r-pill);
    background: rgba(11, 11, 10, 0.55);
    -webkit-backdrop-filter: blur(10px) saturate(1.3);
    backdrop-filter: blur(10px) saturate(1.3);
    color: #fff;
    font-size: var(--at-text-xs);
    font-weight: var(--at-weight-semibold);
    line-height: 1.3;
    pointer-events: none;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.28);
}
.v-yt__brand i { color: #ff2f2f; font-size: 1em; }

/* The real player — swapped in on play, fills the frame. */
.v-yt__frame {
    position: absolute;
    inset: 0;
    z-index: 4;
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
}
/* Once playing, the facade layers step aside. */
.v-yt.is-playing .v-yt__thumb,
.v-yt.is-playing .v-yt__play,
.v-yt.is-playing .v-yt__brand { display: none; }

/* Veil (spoiler / age / paywall) hides the play affordance until revealed. */
.v-yt.is-veiled .v-yt__play,
.v-yt.is-veiled .v-yt__brand { display: none; }

/* Thumbnail failed to load AND fallback failed — a Bherda skeleton-toned panel
   with a play glyph, never a blank rectangle. */
.v-yt--noimg { background:
    linear-gradient(135deg, var(--at-surface-sunken), var(--at-surface-hover)); }
.v-yt--noimg .v-yt__thumb { display: none; }

/* ---- composer preview (#addpost) --------------------------------------- */
.v-yt--preview { cursor: default; }
.v-yt__remove {
    position: absolute;
    top: var(--at-s2);
    inset-inline-end: var(--at-s2);
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border: 0;
    border-radius: 999px;
    background: rgba(11, 11, 10, 0.62);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    color: #fff;
    font-size: 1rem;
    cursor: pointer;
    transition: background-color var(--at-t-fast) var(--at-ease-out), transform var(--at-t-fast) var(--at-ease-out);
}
.v-yt__remove:hover { background: rgba(11, 11, 10, 0.8); transform: scale(1.05); }
.v-yt__remove:active { transform: scale(0.94); }

/* ---- fullscreen post modal (pv__stage) --------------------------------- */
/* The stage sizes the media; let the YouTube frame fill it rather than impose
   its own aspect box (which would fight the modal's letterbox). */
.pv__stage.v-yt { aspect-ratio: auto; }
.pv__stage.v-yt.v-yt--short { max-width: none; margin-inline: 0; }

/* ---- profile grid tile play badge -------------------------------------- */
.gallery-wrapper--yt { position: relative; }
.gallery-yt {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.6rem;
    height: 2.6rem;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.46);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    color: #fff;
    font-size: 1rem;
    padding-inline-start: 0.12rem;
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .v-yt__playring, .v-yt__remove { transition: none; }
}

/* =============================================================================
   MEDIA UPLOAD — progress bar + status chip (media-upload.js)
   -----------------------------------------------------------------------------
   A thin top progress bar reflects real upload %, and a centred glass chip shows
   "Uploading 45%" then "Processing your media…" while the server runs the shared
   pipeline. Sits above every overlay so it is visible from within the composer.
   ========================================================================== */
.media-upbar {
    position: fixed; top: 0; left: 0; z-index: var(--at-z-upload);
    height: 3px; width: 0;
    background: linear-gradient(90deg, var(--at-accent), var(--at-accent-hover, var(--at-accent)));
    box-shadow: 0 0 10px var(--at-accent-soft, rgba(0,0,0,.2));
    opacity: 0; pointer-events: none;
    transition: width 0.18s var(--at-ease-out), opacity 0.2s var(--at-ease-out);
}
.media-upbar.is-on { opacity: 1; }
.media-upbar.is-indeterminate {
    width: 100% !important;
    background: linear-gradient(90deg, transparent, var(--at-accent), transparent);
    background-size: 40% 100%; background-repeat: no-repeat;
    animation: media-upbar-slide 1.1s var(--at-ease-inout) infinite;
}
@keyframes media-upbar-slide {
    0%   { background-position: -40% 0; }
    100% { background-position: 140% 0; }
}

.media-upchip {
    position: fixed; z-index: var(--at-z-upload);
    top: calc(env(safe-area-inset-top, 0px) + var(--at-s3));
    left: 50%; transform: translateX(-50%) translateY(-8px);
    display: inline-flex; align-items: center; gap: var(--at-s2);
    max-width: 90vw;
    padding: var(--at-s2) var(--at-s4);
    border-radius: var(--at-r-pill);
    background: rgba(12, 12, 14, 0.82);
    -webkit-backdrop-filter: blur(12px) saturate(1.2); backdrop-filter: blur(12px) saturate(1.2);
    color: #fff; font-size: var(--at-text-sm); font-weight: var(--at-weight-medium);
    letter-spacing: var(--at-tracking-meta);
    box-shadow: 0 10px 30px -12px rgba(0,0,0,.6);
    opacity: 0; pointer-events: none;
    transition: opacity 0.22s var(--at-ease-out), transform 0.22s var(--at-ease-out);
}
.media-upchip.is-on { opacity: 1; transform: translateX(-50%) translateY(0); }
.media-upchip__spin {
    width: 0.95rem; height: 0.95rem; flex: 0 0 auto;
    border-radius: 999px;
    border: 2px solid rgba(255,255,255,0.28); border-top-color: #fff;
    animation: v-video-spin 0.7s linear infinite;
    opacity: 0;                                 /* only while processing */
}
.media-upchip.is-processing .media-upchip__spin { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
    .media-upbar.is-indeterminate { animation: none; }
    .media-upchip__spin { animation-duration: 1.4s; }
}

/* =============================================================================
   IMAGE LIGHTBOX
   ========================================================================== */
.v-lightbox {
    position: fixed; inset: 0; z-index: var(--at-z-viewer);
    display: flex; align-items: center; justify-content: center;
    overscroll-behavior: contain; touch-action: none;
}
.v-lightbox[hidden] { display: none; }

.v-lightbox__backdrop {
    position: absolute; inset: 0;
    background: rgba(4, 4, 6, 0.94);
    -webkit-backdrop-filter: blur(22px) saturate(1.1); backdrop-filter: blur(22px) saturate(1.1);
    opacity: 0; transition: opacity 0.3s var(--at-ease-out);
}
.v-lightbox.is-open .v-lightbox__backdrop { opacity: 1; }

.v-lightbox__stage {
    position: relative;
    max-width: 94vw; max-height: 92dvh;
    display: flex; align-items: center; justify-content: center;
    transform-origin: center center;
    transition: transform 0.34s var(--at-ease-out), opacity 0.34s var(--at-ease-out);
    will-change: transform, opacity;
}

.v-lightbox__img {
    display: block; width: auto; height: auto;
    max-width: 94vw; max-height: 92dvh;
    border-radius: var(--at-r-lg);
    background: #000;
    box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6);
    transform-origin: center center;
    /* video-player.js drives the transform (translate3d+scale) and manages the
       transition inline, so the zoom/pan hot path stays on the GPU. */
    will-change: transform;
    cursor: pointer; touch-action: none; user-select: none; -webkit-user-select: none; -webkit-user-drag: none;
}
.v-lightbox.is-zoomed  .v-lightbox__img { cursor: grab; }
.v-lightbox.is-grabbing .v-lightbox__img { cursor: grabbing; }

.v-lightbox__close {
    position: absolute;
    top: calc(env(safe-area-inset-top, 0px) + var(--at-s3));
    inset-inline-end: calc(env(safe-area-inset-right, 0px) + var(--at-s3));
    z-index: 2; width: 2.6rem; height: 2.6rem;
    display: flex; align-items: center; justify-content: center;
    border: 0; border-radius: 999px;
    background: rgba(0, 0, 0, 0.45);
    -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
    color: #fff; font-size: 1.35rem; cursor: pointer;
    opacity: 0; transition: opacity 0.25s var(--at-ease-out), background 0.2s, transform 0.2s;
}
.v-lightbox.is-open .v-lightbox__close { opacity: 1; }
.v-lightbox__close:hover { background: rgba(0, 0, 0, 0.7); transform: scale(1.05); }

/* phones: edge-to-edge black */
@media (max-width: 47.98em) {
    .v-lightbox__backdrop { background: #000; -webkit-backdrop-filter: none; backdrop-filter: none; }
    .v-lightbox__stage { max-width: 100vw; max-height: 100dvh; }
    .v-lightbox__img { max-width: 100vw; max-height: 100dvh; border-radius: 0; box-shadow: none; }
}

@media (prefers-reduced-motion: reduce) {
    .v-lightbox__backdrop, .v-lightbox__stage, .v-lightbox__img,
    .v-video__badge, .v-video__play { transition: none; }
    .v-video__spin { animation-duration: 1.4s; }
}

/* =============================================================================
   ACCOUNT CENTER — the redesigned Edit Profile (assets/pages/edit.php)
   -----------------------------------------------------------------------------
   A live-preview header, a category rail (left on desktop / a sticky scroller
   on mobile), a wide panel area, and a sticky Save bar. Everything is a draft;
   nothing commits until Save Changes. Built on the design-system field
   primitives (v-input / v-select / v-textarea / v-btn) so it stays one material.
   ========================================================================== */
.v-settings {
    max-width: 68rem;
    margin-inline: auto;
    /* Same page breathing room as every other major page: the top matches the
       Profile page's .v-main exactly (--at-page-top), so Edit Profile no longer
       sits higher/lower than the profile it edits. The generous bottom is NOT
       page spacing — it is fixed-footer clearance: the Save/Cancel bar is
       position:fixed, so the last fields must reserve room or they'd sit behind
       it (and, on mobile, behind the tab bar too — see the media queries below). */
    padding-inline: var(--at-s4);
    padding-top: var(--at-page-top);
   /* padding-bottom: calc(6rem + env(safe-area-inset-bottom, 0px)); */
}

/* ---- live preview header ---------------------------------------------------
   The preview reuses the public profile primitives verbatim — .v-pf__cover +
   .v-profile__banner-media, the seated .v-avatar, .v-profile__name/handle — so
   it is pixel-for-pixel the profile page. These rules add ONLY the edit-specific
   glue (the back chip, the media toolbar) and reuse everything else; there are
   no duplicate banner/avatar/typography styles here. */
.v-settings__preview { margin-bottom: var(--at-s5); }
.v-settings__preview .v-pf__cover { margin-bottom: 0; }
/* the avatar sits at the head of the identity, same lift as the profile rail */
.v-settings__identity { padding-bottom: var(--at-s2); }
.v-settings__back { position: absolute; top: var(--at-s3); inset-inline-start: var(--at-s3); z-index: 3; }

/* media toolbar — consistent pill buttons under the preview, grouped by target */
.v-settings__media {
    display: flex; flex-wrap: wrap; gap: var(--at-s3) var(--at-s6);
    padding: 0 var(--at-s2) var(--at-s1);
}
.v-settings__mediagroup { display: flex; align-items: center; gap: var(--at-s2); flex-wrap: wrap; }
.v-settings__medialabel {
    font-size: var(--at-text-xs); font-weight: var(--at-weight-bold);
    letter-spacing: 0.04em; text-transform: uppercase; color: var(--at-ink-muted);
    margin-inline-end: var(--at-s1);
}

/* ---- two-column body ------------------------------------------------------- */
.v-settings__body { display: grid; grid-template-columns: 15rem minmax(0, 1fr); gap: var(--at-s6); align-items: start; }

.v-settings__nav {
    position: sticky; top: calc(var(--at-topbar-h) + env(safe-area-inset-top, 0px) + var(--at-s4));
    display: flex; flex-direction: column; gap: 2px;
}
.v-settings__navitem {
    display: flex; align-items: center; gap: var(--at-s3);
    padding: var(--at-s3) var(--at-s3); min-height: var(--at-hit);
    border: 0; border-radius: var(--at-r-md); background: transparent;
    color: var(--at-ink-secondary); font-size: var(--at-text-sm); font-weight: var(--at-weight-semibold);
    text-align: start; cursor: pointer;
    transition: background-color var(--at-t-fast) var(--at-ease-out), color var(--at-t-fast) var(--at-ease-out);
}
.v-settings__navitem i { font-size: var(--at-icon-md); color: var(--at-ink-muted); }
.v-settings__navitem:hover { background: var(--at-surface-hover); color: var(--at-ink); }
.v-settings__navitem.is-active { background: var(--at-accent-soft); color: var(--at-accent-ink); }
.v-settings__navitem.is-active i { color: var(--at-accent-ink); }

.v-settings__content { min-width: 0; }
.v-settings__panel { display: none; }
.v-settings__panel.is-active { display: block; animation: v-fade-in var(--at-t-base) var(--at-ease-out); }
.v-settings__panel:focus { outline: none; }
@keyframes v-fade-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .v-settings__panel.is-active { animation: none; } }

/* ---- grouped cards + fields ------------------------------------------------ */
.v-set-group {
    padding: var(--at-s5);
    border-radius: var(--at-r-lg);
    background: var(--at-surface);
    box-shadow: var(--at-edge), var(--at-shadow-sm);
    margin-bottom: var(--at-s5);
}
.v-set-group__title {
    margin: 0 0 var(--at-s4);
    font-size: var(--at-text-xs); font-weight: var(--at-weight-bold);
    letter-spacing: 0.06em; text-transform: uppercase; color: var(--at-ink-muted);
}
.v-set-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--at-s4); }
.v-set-grid + .v-set-field, .v-set-group > .v-set-field + .v-set-field { margin-top: var(--at-s4); }

.v-set-field { display: flex; flex-direction: column; gap: var(--at-s2); min-width: 0; }
.v-set-field__label { display: flex; align-items: center; gap: var(--at-s2); font-size: var(--at-text-sm); font-weight: var(--at-weight-semibold); color: var(--at-ink); }
.v-set-field__badge {
    font-size: var(--at-text-xs); font-weight: var(--at-weight-medium);
    color: var(--at-ink-faint); text-transform: none; letter-spacing: 0;
    padding: 0 var(--at-s2); border-radius: var(--at-r-pill); background: var(--at-surface-sunken);
}
.v-set-field__hint { margin: 0; font-size: var(--at-text-xs); color: var(--at-ink-muted); }
.v-set-field__err { margin: 0; font-size: var(--at-text-xs); font-weight: var(--at-weight-medium); color: var(--at-danger); }
.v-set-field.has-error .v-input,
.v-set-field.has-error .v-select,
.v-set-field.has-error .v-textarea,
.v-set-linkrow.has-error .v-input { border-color: var(--at-danger); box-shadow: 0 0 0 1px var(--at-danger); }

/* input wrapper carrying an optional leading icon / prefix */
.v-set-input { position: relative; display: block; }
.v-set-input > .v-input, .v-set-input > .v-select { width: 100%; }
.v-set-input__icon {
    position: absolute; top: 50%; inset-inline-start: var(--at-s3); transform: translateY(-50%);
    color: var(--at-ink-muted); font-size: var(--at-icon-sm); pointer-events: none;
}
.v-set-input__prefix {
    position: absolute; top: 50%; inset-inline-start: var(--at-s3); transform: translateY(-50%);
    color: var(--at-ink-muted); font-weight: var(--at-weight-semibold); pointer-events: none;
}
.v-set-input--iconed  > .v-input, .v-set-input--iconed  > .v-select { padding-inline-start: 2.35rem; }
.v-set-input--prefixed > .v-input { padding-inline-start: 1.75rem; }

/* ---- custom links ---------------------------------------------------------- */
.v-set-links { display: flex; flex-direction: column; gap: var(--at-s3); margin-bottom: var(--at-s3); }
.v-set-linkrow { display: grid; grid-template-columns: minmax(6rem, 12rem) minmax(0, 1fr) auto; gap: var(--at-s2); align-items: center; }
.v-set-linkrow__del { flex: 0 0 auto; color: var(--at-ink-muted); }
.v-set-linkrow__del:hover { color: var(--at-danger); background: var(--at-surface-hover); }
.v-set-gemwrap { max-height: 17rem; overflow-y: auto; overscroll-behavior: contain; border-radius: var(--at-r-md); }

/* ---- save bar — slides up from the viewport bottom only when edits exist ----
   Fixed (not sticky) so showing/hiding it never reflows the page; the page
   reserves bottom room permanently, so there is no layout jump either way. */
.v-settings__savebar {
    position: fixed; inset-inline: 0; bottom: 0; z-index: 60;
    padding: var(--at-s3) var(--at-s4);
    padding-bottom: calc(var(--at-s3) + env(safe-area-inset-bottom, 0px));
    background: color-mix(in srgb, var(--at-surface) 88%, transparent);
    backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
    border-top: 1px solid var(--at-hairline);
    box-shadow: 0 -8px 24px -18px rgba(0, 0, 0, 0.5);
    transform: translateY(120%); opacity: 0; pointer-events: none;
    transition: transform var(--at-t-base) var(--at-ease-out), opacity var(--at-t-base) var(--at-ease-out);
}
#updateProfileForm.is-dirty .v-settings__savebar { transform: none; opacity: 1; pointer-events: auto; }
.v-settings__savebar-inner {
    max-width: 68rem; margin-inline: auto;
    display: flex; align-items: center; gap: var(--at-s3);
}
.v-settings__dirty { display: inline-flex; align-items: center; gap: var(--at-s2); font-size: var(--at-text-sm); font-weight: var(--at-weight-medium); color: var(--at-ink-secondary); }
.v-settings__dirty i { color: var(--at-accent); }
@media (prefers-reduced-motion: reduce) { .v-settings__savebar { transition: opacity var(--at-t-fast) linear; } }

.v-editmedia__pending--banner { inset-block-start: var(--at-s3); inset-inline-start: var(--at-s3); inset-inline-end: auto; }

/* ---- Below Floor Plan: native settings feel ------------------------------- */
@media (max-width: 63.98em) {
    /* keep a small gutter so the preview's rounded cover matches the profile,
       and match the profile page's mobile top spacing (.v-main uses
       --at-page-top-mobile) so both pages start at the same height under the bar */
    .v-settings { padding-inline: var(--at-s3); padding-top: var(--at-page-top-mobile); }
    .v-settings__body { grid-template-columns: 1fr; gap: 0; }
    /* the rail becomes a sticky, edge-to-edge horizontal segmented scroller */
    .v-settings__nav {
        position: sticky; top: calc(var(--at-topbar-h) + env(safe-area-inset-top, 0px));
        z-index: 15; flex-direction: row; gap: var(--at-s1);
        margin-inline: calc(var(--at-s3) * -1); padding: var(--at-s2) var(--at-s3);
        overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
        background: color-mix(in srgb, var(--at-bg) 92%, transparent);
        backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
        border-bottom: 1px solid var(--at-hairline);
    }
    .v-settings__nav::-webkit-scrollbar { display: none; }
    .v-settings__navitem { flex: 0 0 auto; white-space: nowrap; min-height: var(--at-hit); padding: var(--at-s2) var(--at-s3); border-radius: var(--at-r-pill); }
    .v-settings__navitem span { font-size: var(--at-text-sm); }
    .v-settings__content { padding: var(--at-s4) 0 0; }
    .v-set-group { padding: var(--at-s4); border-radius: var(--at-r-lg); }
    .v-set-grid { grid-template-columns: 1fr; }
    /* media toolbar: each target group stacks, label centred, two large
       tappable buttons side by side — no tiny controls, no image overlap */
    .v-settings__media { flex-direction: column; align-items: stretch; gap: var(--at-s3); padding-inline: 0; }
    .v-settings__mediagroup { flex-wrap: wrap; justify-content: center; }
    .v-settings__medialabel { flex: 1 0 100%; text-align: center; margin-inline-end: 0; }
    .v-settings__media .v-btn { flex: 1 1 0; min-height: var(--at-hit); }
    /* big touch targets on the full-width bottom bar */
    .v-settings__savebar .v-btn { min-height: var(--at-hit); }
    .v-settings__savebar-inner .v-btn--primary { flex: 1 1 auto; }
    .v-settings__dirty { display: none; }
    .v-set-linkrow { grid-template-columns: 1fr auto; }
    .v-set-linkrow__label { grid-column: 1 / -1; }
}

/* ---- Phone (<48em): the app's bottom tab bar (.v-tabbar, fixed, z-nav) is
   present. The save bar must sit ABOVE it, never behind it, and the page must
   reserve room for BOTH so neither ever covers a field. Above 48em the tab bar
   is gone, so the base bottom:0 save bar is correct. --------------------------- */
@media (max-width: 47.98em) {
    .v-settings__savebar {
        bottom: calc(var(--at-tabbar-h) + env(safe-area-inset-bottom, 0px));
        padding-bottom: var(--at-s3);   /* the tab bar below already owns the safe area */
    }
    .v-settings {
        /* tab bar + save bar + a comfortable gap, so content clears both */
       /* padding-bottom: calc(var(--at-tabbar-h) + 5.5rem + env(safe-area-inset-bottom, 0px)); */
    }
}

/* ---- accessibility: focus + high contrast --------------------------------- */
.v-settings__navitem:focus-visible,
.v-set-linkrow__del:focus-visible,
.v-settings__back:focus-visible { outline: 2px solid var(--at-accent); outline-offset: 2px; }
@media (prefers-contrast: more) {
    .v-set-group, .v-settings__preview { box-shadow: 0 0 0 1px var(--at-ink); }
    .v-settings__navitem.is-active { outline: 1px solid var(--at-accent-ink); }
}

/* =============================================================================
   OVERLAY / LIST PERFORMANCE — open instantly, render progressively
   -----------------------------------------------------------------------------
   Large modal lists (followers / following) and post grids pre-render every row
   server-side. content-visibility:auto lets the browser SKIP layout + paint for
   rows/tiles that are off-screen in their scroll container, so the modal or tab
   opens in constant time no matter how many entries there are; contain-intrinsic-
   size reserves each item's box so the scrollbar and positions stay stable (no
   jump). Search still sees every row's text — content-visibility hides painting,
   not the DOM. Scoped so only the heavy surfaces are affected.
   ========================================================================== */
[data-list-scope] .v-person {
    content-visibility: auto;
    contain-intrinsic-size: auto 4rem;
}
.gallery-wrapper {
    content-visibility: auto;
    contain-intrinsic-size: auto 10rem;
}

/* =============================================================================
   FEED INFINITE SCROLL — skeleton cards + sentinel (feed.js)
   -----------------------------------------------------------------------------
   Skeletons match the post card's shape (avatar + name, media block, two text
   lines) so the incoming batch never leaves a blank gap or shifts layout; they
   reuse the media-loader shimmer (--m-skel-sheen + m-shimmer) so the whole app
   loads with one motion. Removed the instant the real cards arrive.
   ========================================================================== */
.v-post--skel { pointer-events: none; overflow: hidden; }
.v-post--skel .v-post__head { display: flex; align-items: center; gap: var(--at-s3); padding: var(--at-s4); }
.v-post--skel .v-post__body { padding: var(--at-s4); }
.v-skelblock {
    display: block;
    background-color: var(--at-surface-sunken);
    background-image: linear-gradient(100deg, transparent 20%, var(--m-skel-sheen) 50%, transparent 80%);
    background-size: 220% 100%;
    background-repeat: no-repeat;
    animation: m-shimmer 1.25s var(--at-ease-inout) infinite;
    border-radius: var(--at-r-sm);
}
.v-skelblock--avatar { flex: 0 0 auto; width: 2.75rem; height: 2.75rem; border-radius: var(--at-r-pill); }
.v-skelblock--name   { height: 0.85rem; width: 9rem; }
.v-skelblock--media  { height: clamp(14rem, 42vw, 22rem); width: 100%; border-radius: 0; }
.v-skelblock--line   { height: 0.7rem; width: 100%; margin-top: var(--at-s2); }
.v-skelblock--short  { width: 55%; }
.v-feed-sentinel { display: block; width: 100%; height: 1px; }
@media (prefers-reduced-motion: reduce) { .v-skelblock { animation: none; } }

.v-skel-tile { pointer-events: none; }

/* ============================================================================
   USER TEXT (assets/js/rich-text.js) — one renderer for posts / comments /
   bios / messages / descriptions. No nl2br anywhere: intentional line breaks
   render via white-space:pre-wrap (nl2br + pre-wrap doubled every break), and
   the parser turns @mentions / #hashtags / URLs into these safe anchors.
   ========================================================================== */

.pv__caption,
.v-comment__text,
.v-profile__biotext { white-space: pre-wrap; overflow-wrap: anywhere; }

.v-rt-mention,
.v-rt-tag {
    color: var(--at-accent, #F07629);
    font-weight: 600;
    text-decoration: none;
}
.v-rt-mention:hover,
.v-rt-tag:hover { text-decoration: underline; }

.v-rt-link {
    color: var(--at-accent, #F07629);
    text-decoration: underline;
    text-underline-offset: 2px;
    overflow-wrap: anywhere;
}

/* Language onboarding modal (footer.php) — native-name option rows. */
.v-lang-list { display: flex; flex-direction: column; gap: var(--at-s2, 0.5rem); }
.v-lang-opt {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; padding: var(--at-s3, 0.75rem) var(--at-s4, 1rem);
    border: 1px solid var(--at-line, rgba(0,0,0,.12));
    border-radius: var(--at-r-md, 12px);
    background: var(--at-surface-raised, #fff);
    color: var(--at-ink, #14110C);
    font-size: var(--at-text-md, 1rem); font-weight: 600;
    cursor: pointer; text-align: start;
    transition: border-color .15s ease, background-color .15s ease, transform .06s ease;
}
.v-lang-opt:hover { border-color: var(--at-accent, #F07629); background: color-mix(in srgb, var(--at-accent, #F07629) 7%, var(--at-surface-raised, #fff)); }
.v-lang-opt:active { transform: scale(.99); }
.v-lang-opt.is-loading { opacity: .6; pointer-events: none; }
.v-lang-opt__go { color: var(--at-ink-muted, #9a938a); font-size: var(--at-icon-sm, 0.9rem); flex: none; }
.v-lang-opt:hover .v-lang-opt__go { color: var(--at-accent, #F07629); }

/* ============================================================================
   REPOSTS — profile Reposts tab feed + "↻ X reposted" attribution header.
   A repost references the original post; the header sits above the unchanged
   post_card, so the original author's identity stays intact.
   ========================================================================== */
.v-repost-feed { display: flex; flex-direction: column; gap: var(--at-s4, 1rem); }
.v-repost__tag {
    display: flex; align-items: center; gap: var(--at-s2, 0.5rem);
    padding: 0 var(--at-s2, 0.5rem) var(--at-s1, 0.25rem);
    font-size: var(--at-text-sm, 0.9rem); color: var(--at-ink-muted, #6b6156);
}
.v-repost__icon { color: var(--at-accent, #F07629); }
.v-repost__who { color: var(--at-ink, #14110C); font-weight: 600; text-decoration: none; }
.v-repost__who:hover { text-decoration: underline; }
.v-repost__dot { width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: .5; flex: none; }
/* The "reposted" (on) state carries the accent, matching liked/saved. */
.unrepost_btn.is-on { color: var(--at-accent, #F07629); }

/* Repost banner inside the fullscreen post viewer (post_viewer.php) — a subtle
   "↻ X reposted · time" line above the original author, so the reposter is never
   mistaken for the creator. Reuses the viewer's metadata rhythm. */
.pv__repost {
    display: flex; align-items: center; gap: var(--at-s2, 0.5rem);
    padding: var(--at-s3, 0.75rem) var(--at-s4, 1rem) 0;
    font-size: var(--at-text-sm, 0.9rem); color: var(--at-ink-muted, #6b6156);
}
.pv__repost-icon { color: var(--at-accent, #F07629); }
.pv__repost-who { color: var(--at-ink, #14110C); font-weight: 600; text-decoration: none; }
.pv__repost-who:hover { text-decoration: underline; }
.pv__repost-dot { width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: .5; flex: none; }
/* Reposted (on) state carries the accent, matching the like/save on-states. */
.pv__actbtn.unrepost_btn.is-on { color: var(--at-accent, #F07629); }

/* =============================================================================
   PUSH NOTIFICATIONS — built ON the settings-family components already used by
   the ?settings page: .v-stack-6 > .v-group > .v-rowitem, plus .v-switch, .v-btn
   and .telegram-modal. The ONLY additions are (a) a description line for a
   .v-rowitem (a reusable enhancement to the shared row) and (b) the enable
   modal's hero. Every value is a real token (no invented radius/surface/type).
   ========================================================================== */

/* A two-line settings row: a muted description under the row label. Reusable by
   any .v-rowitem that needs a subtitle. */
.v-rowitem__label .v-rowitem__sub {
    display: block;
    margin-top: 1px;
    font-size: var(--at-text-xs);
    font-weight: var(--at-weight-regular);
    letter-spacing: var(--at-tracking-meta);
    color: var(--at-ink-muted);
}
/* A non-interactive .v-rowitem (e.g. the status row): no pointer, no hover well. */
.v-rowitem--static { cursor: default; }
.v-rowitem--static:hover { background-color: transparent; color: var(--at-ink); }

/* The shared .v-switch, paused while its change saves to the server. */
.v-switch.is-saving { opacity: .5; pointer-events: none; }

/* Enable modal — content INSIDE the shared .telegram-modal (centred desktop /
   bottom sheet mobile; backdrop, radius, padding, close, z-index all inherited). */
.v-pushmodal .modal-header { justify-content: flex-end; border-bottom: 0; padding-bottom: 0; }
.v-pushmodal .modal-body { text-align: center; }
.v-pushmodal__hero { position: relative; display: grid; place-items: center; margin-bottom: var(--at-s4); }
.v-pushmodal__hero::before {
    content: ""; position: absolute; inline-size: 6.5rem; block-size: 6.5rem;
    border-radius: var(--at-r-pill); background: var(--at-accent-soft);
}
.v-pushmodal__icon {
    position: relative; display: grid; place-items: center;
    inline-size: 4rem; block-size: 4rem; border-radius: var(--at-r-pill);
    background: var(--at-accent); color: var(--at-accent-contrast);
    font-size: var(--at-icon-lg); box-shadow: var(--at-shadow-md);
}
.v-pushmodal__title {
    font-size: var(--at-text-xl); font-weight: var(--at-weight-semibold);
    letter-spacing: var(--at-tracking-title); color: var(--at-ink); margin-bottom: var(--at-s2);
}
.v-pushmodal__sub { font-size: var(--at-text-sm); color: var(--at-ink-muted); margin: 0 auto var(--at-s5); max-width: 24rem; }
.v-pushmodal__steps { list-style: none; margin: 0 0 var(--at-s5); padding: 0; display: grid; gap: var(--at-s1); text-align: start; }
.v-pushmodal__step { display: flex; align-items: center; gap: var(--at-s3); min-height: var(--at-hit); padding: var(--at-s2); }
.v-pushmodal__step > span:last-child { font-size: var(--at-text-sm); color: var(--at-ink-secondary); }
.v-pushmodal__num {
    flex: 0 0 auto; display: grid; place-items: center; inline-size: 1.75rem; block-size: 1.75rem;
    border-radius: var(--at-r-pill); background: var(--at-surface-sunken); color: var(--at-ink-secondary);
    font-size: var(--at-text-sm); font-weight: var(--at-weight-semibold);
}
.v-pushmodal__note { display: flex; align-items: center; justify-content: center; gap: var(--at-s2);
    font-size: var(--at-text-sm); color: var(--at-ink-muted); margin-bottom: var(--at-s5); }
.v-pushmodal__note.is-ok   { color: var(--at-success); }
.v-pushmodal__note.is-warn { color: var(--at-danger); }
.v-pushmodal__cta.is-loading { pointer-events: none; opacity: .8; }

@media (prefers-reduced-motion: no-preference) {
    .telegram-modal.show .v-pushmodal__icon { animation: vPushPop var(--at-t-slower) var(--at-ease-spring) both; }
}
@keyframes vPushPop { from { transform: scale(.6); opacity: 0; } to { transform: scale(1); opacity: 1; } }
