/* ---- App bar logo ---- */
.appbar-logo {
    max-height: 44px;
    width: auto;
    height: auto;
    display: block;
}

/* ---- Gallery grid tiles ---- */
.gallery-tile {
    display: flex;
    flex-direction: column;
    transition: transform .15s ease, box-shadow .15s ease;
}

.gallery-tile:hover {
    transform: translateY(-2px);
}

.gallery-tile-image {
    aspect-ratio: 1 / 1;
    overflow: hidden;
    background: rgba(0, 0, 0, .15);
}

.gallery-tile-img,
.gallery-tile .mud-image {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.gallery-tile-placeholder {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: .5;
}

/* ---- Series page thumbnails ---- */
.gallery-card-thumb {
    position: relative;
    aspect-ratio: 3 / 4;
    overflow: hidden;
    border-radius: 6px;
    cursor: pointer;
    background: rgba(0, 0, 0, .15);
}

.gallery-card-thumb-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform .2s ease;
}

.gallery-card-thumb:hover .gallery-card-thumb-img {
    transform: scale(1.05);
}

.gallery-card-thumb-overlay {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 6px 8px;
    color: #fff;
    background: linear-gradient(to top, rgba(0, 0, 0, .75), rgba(0, 0, 0, 0));
    opacity: 0;
    transition: opacity .2s ease;
}

.gallery-card-thumb:hover .gallery-card-thumb-overlay {
    opacity: 1;
}

/* Small transparent badge marking cards that have variant images */
.variant-badge {
    position: absolute;
    top: 4px;
    right: 4px;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    color: #fff;
    background: rgba(0, 0, 0, .45);
    backdrop-filter: blur(2px);
    pointer-events: none;
    transition: background-color .2s ease;
}

.variant-badge .mud-icon-root {
    font-size: 1rem;
}

.gallery-card-thumb:hover .variant-badge {
    background: rgba(126, 111, 255, .85);
}

/* ---- Locked NSFW placeholder tile (restricted-state guests) ---- */
.nsfw-locked {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 3 / 4;
    border-radius: 6px;
    cursor: pointer;
    color: #b2b0bf;
    background:
        repeating-linear-gradient(45deg, rgba(126, 111, 255, .08) 0 12px, rgba(126, 111, 255, .03) 12px 24px),
        rgba(0, 0, 0, .25);
    border: 1px dashed rgba(126, 111, 255, .4);
    transition: color .2s ease, border-color .2s ease;
}

.nsfw-locked:hover {
    color: #fff;
    border-color: rgba(126, 111, 255, .8);
}

.nsfw-locked-body {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: 8px;
}

/* ---- Lightbox ---- */
.cardlightbox-overlay {
    position: fixed;
    inset: 0;
    z-index: 2000;
    background: rgba(0, 0, 0, .92);
    display: flex;
    flex-direction: column;
    outline: none;
}

.cardlightbox-topbar {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 8px 12px;
    color: #fff;
}

/* On phones the topbar can hold counter + Material + Finish selects + Add-to-cart +
   Info + Close — too wide for one row. Let it wrap and keep the Close button last but
   always reachable, and cap the selects so they don't shove the controls off-screen. */
@media (max-width: 600px) {
    .cardlightbox-topbar {
        flex-wrap: wrap;
        row-gap: 8px;
    }
    /* Material/Finish selects share the row below the counter/close on narrow screens. */
    .cardlightbox-opt {
        flex: 1 1 40%;
        min-width: 120px;
    }
    /* Add-to-cart takes the full width of its wrapped row so the price label always fits. */
    .cardlightbox-addcart {
        flex: 1 1 100%;
        order: 5;
    }
}

.cardlightbox-counter {
    color: #fff;
    opacity: .85;
}

/* Keep the "Add to cart" label on one line next to the icon buttons. */
.cardlightbox-addcart {
    white-space: nowrap;
}

/* The lightbox overlay sits at z-index 2000; MudBlazor's snackbar containers
   default to 1500, so "Added to cart" toasts render behind the open viewer.
   Lift every snackbar position container above the overlay. */
div[class*="mud-snackbar-location"] {
    z-index: 2100 !important;
}

/* Same problem for dialogs: MudBlazor's dialog container defaults to ~1400, so
   the report dialog opened from the lightbox renders *behind* the overlay. Lift
   the whole dialog container above the lightbox. Only the container is raised —
   NOT the inner .mud-overlay-dialog (the scrim's wrapper): raising the container
   carries the scrim AND the dialog above the lightbox together while preserving
   their relative order, so the scrim stays behind the dialog content. (Raising
   .mud-overlay-dialog to match the container would float the scrim on top of the
   dialog and swallow every click inside it.) */
.mud-dialog-container {
    z-index: 2100 !important;
}

.cardlightbox-body {
    position: relative;
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
}

.cardlightbox-stage {
    flex: 1 1 auto;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 8px;
    touch-action: pan-y;
}

.cardlightbox-image {
    max-width: 100%;
    max-height: calc(100vh - 120px);
    object-fit: contain;
    user-select: none;
}

.cardlightbox-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    color: #fff !important;
    background: rgba(0, 0, 0, .35) !important;
    z-index: 1;
}

.cardlightbox-nav-left {
    left: 8px;
}

.cardlightbox-nav-right {
    right: 8px;
}

.cardlightbox-details {
    background: rgba(20, 20, 27, .95) !important;
    color: #fff;
    padding: 12px 16px;
    max-height: 35vh;
    overflow-y: auto;
}

@media (min-width: 960px) {
    .cardlightbox-details {
        position: absolute;
        top: 56px;
        right: 12px;
        width: 320px;
        max-height: calc(100vh - 88px);
        border-radius: 8px;
    }
}

/* ---- Generic full-screen image viewer (Components/Shared/ImageLightbox.razor) ----
   A slimmed-down sibling of the card lightbox above: no material/finish/rework chrome, just a
   full-bleed image with prev/next, a fullscreen toggle, and a close button. Shared by the studio
   result panel, the studio history, and the collection. */
.imagelightbox-overlay {
    position: fixed;
    inset: 0;
    z-index: 2000;
    background: rgba(0, 0, 0, .94);
    display: flex;
    flex-direction: column;
    outline: none;
}

.imagelightbox-topbar {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 8px 12px;
    color: #fff;
}

.imagelightbox-counter {
    color: #fff;
    opacity: .85;
}

.imagelightbox-body {
    position: relative;
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
}

.imagelightbox-stage {
    position: relative;
    flex: 1 1 auto;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 8px;
    touch-action: pan-y;
}

.imagelightbox-image {
    max-width: 100%;
    max-height: calc(100vh - 96px);
    object-fit: contain;
    user-select: none;
}

/* When the overlay itself is the browser's fullscreen element, the topbar is gone-ish visually but
   the stage should use the full height. The prompt caption is also hidden so it can't cover the
   image — the studio's history lightbox feeds each generation's Prompt in as the caption, and a long
   prompt can eat much of a fullscreened image. */
.imagelightbox-overlay:fullscreen .imagelightbox-image {
    max-height: calc(100vh - 64px);
}

.imagelightbox-overlay:fullscreen .imagelightbox-caption {
    display: none;
}

.imagelightbox-caption {
    position: absolute;
    left: 50%;
    bottom: 12px;
    transform: translateX(-50%);
    max-width: 80%;
    padding: 4px 12px;
    border-radius: 999px;
    background: rgba(0, 0, 0, .55);
    color: #fff;
    font-size: .85rem;
    text-align: center;
    pointer-events: none;
}

.imagelightbox-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    color: #fff !important;
    background: rgba(0, 0, 0, .35) !important;
    z-index: 1;
}

.imagelightbox-nav-left {
    left: 8px;
}

.imagelightbox-nav-right {
    right: 8px;
}

/* ---- Before/after compare wipe inside the full-screen viewer ----
   The container is sized to the result image; the base sits underneath and the result is clipped on top
   by affinityCompare (JS sets clip-path/left). Mirrors the studio's inline compare, fitted to the stage. */
.imagelightbox-compare {
    position: relative;
    max-width: 100%;
    max-height: calc(100vh - 96px);
    overflow: hidden;
    user-select: none;
    touch-action: none;
    cursor: ew-resize;
}

.imagelightbox-overlay:fullscreen .imagelightbox-compare {
    max-height: calc(100vh - 64px);
}

.imagelightbox-compare-base {
    display: block;
    max-width: 100%;
    max-height: calc(100vh - 96px);
    object-fit: contain;
    pointer-events: none;
}

.imagelightbox-overlay:fullscreen .imagelightbox-compare-base {
    max-height: calc(100vh - 64px);
}

.imagelightbox-compare-top {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
}

.imagelightbox-compare-handle {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 2px;
    margin-left: -1px;
    background: #fff;
    box-shadow: 0 0 4px rgba(0, 0, 0, .6);
    pointer-events: none;
}

.imagelightbox-compare-knob {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 0 4px rgba(0, 0, 0, .5);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #333;
    font-size: 14px;
}

/* ---- REWORK overlay (blur + banner) ---- */
.rework .gallery-card-thumb-img,
.rework.cardlightbox-image,
img.rework {
    filter: blur(14px);
}

/* Wrapper that positions the REWORK banner over an image */
.rework-wrap {
    position: relative;
    overflow: hidden;
}

.rework-banner {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(-20deg);
    background: rgba(200, 30, 45, .92);
    color: #fff;
    font-weight: 800;
    letter-spacing: .15em;
    padding: 4px 40px;
    font-size: 1rem;
    text-transform: uppercase;
    white-space: nowrap;
    pointer-events: none;
    z-index: 2;
    box-shadow: 0 2px 8px rgba(0, 0, 0, .5);
}

.rework-banner-lg {
    font-size: 2rem;
    padding: 8px 80px;
    letter-spacing: .25em;
}

/* Set Manager shows reworked cards UNBLURRED (the owner needs to see what a reviewer flagged) — a
   small corner chip marks them instead of the full blur + banner used everywhere buyers can see. */
.rework-chip {
    position: absolute;
    top: 6px;
    right: 6px; /* top-left is taken by the select checkbox in the set manager */
    z-index: 3;
    background: rgba(200, 30, 45, .92);
    color: #fff;
    font-weight: 700;
    font-size: .62rem;
    letter-spacing: .12em;
    padding: 2px 8px;
    border-radius: 4px;
    text-transform: uppercase;
    pointer-events: none;
}

/* Comment count over a set-manager grid tile — bottom-right, clear of the checkbox and rework chip. */
.card-comment-badge {
    position: absolute;
    bottom: 8px;
    right: 12px;
    z-index: 3;
    color: #fff;
    pointer-events: none;
}

/* ---- Notifications (bell dropdown + list page) ---- */
.notif-menu {
    min-width: 320px;
    max-width: 360px;
}

.notif-item {
    display: flex;
    flex-direction: column;
    gap: 2px;
    white-space: normal;
    padding: 2px 0;
}

.notif-msg {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Unread rows get a faint accent bar so they stand out in the bell and on the list page. */
.notif-unread {
    border-left: 3px solid var(--mud-palette-error);
    background: rgba(255, 255, 255, .04);
}

/* ---- Review Activity page ---- */
.review-activity-thumb {
    width: 64px;
    height: 64px;
    object-fit: cover;
    border-radius: 6px;
    cursor: pointer;
    flex-shrink: 0;
}

.review-activity-preview {
    display: flex;
    justify-content: center;
    max-height: 70vh;
}

.review-activity-preview img {
    max-width: 100%;
    max-height: 70vh;
    object-fit: contain;
    border-radius: 6px;
}

/* ---- @mention picker in the review-note editor ---- */
/* Rendered in normal flow directly under the note field (no caret-pixel positioning), so it shows
   reliably regardless of the textarea's computed line-height. */
.mention-list {
    margin-top: 2px;
    max-height: 200px;
    overflow-y: auto;
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 6px;
    padding: 4px 0;
}

.mention-option {
    display: flex;
    align-items: center;
    padding: 6px 12px;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.mention-option:hover {
    background: var(--mud-palette-action-default-hover, rgba(255, 255, 255, .08));
}

/* ---- Character Finder ---- */
.finder-card {
    position: relative;
    aspect-ratio: 3 / 4;
    overflow: hidden;
    border-radius: 8px;
    cursor: pointer;
    background: rgba(0, 0, 0, .15);
}

.finder-card-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform .2s ease;
}

.finder-card:hover .finder-card-img {
    transform: scale(1.04);
}

.finder-card-caption {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 6px 8px;
    color: #fff;
    background: linear-gradient(to top, rgba(0, 0, 0, .82), rgba(0, 0, 0, 0));
    z-index: 3;
}

.finder-badge {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 3;
}

/* ---- Lightbox variant strip ---- */
.cardlightbox-strip {
    display: flex;
    gap: 8px;
    padding: 10px 12px;
    overflow-x: auto;
    justify-content: center;
    background: rgba(0, 0, 0, .35);
}

.cardlightbox-strip-thumb {
    flex: 0 0 auto;
    width: 64px;
    height: 64px;
    padding: 0;
    border: 2px solid transparent;
    border-radius: 6px;
    overflow: hidden;
    background: rgba(255, 255, 255, .08);
    cursor: pointer;
    transition: border-color .15s ease, transform .15s ease;
}

.cardlightbox-strip-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.cardlightbox-strip-thumb:hover {
    transform: translateY(-2px);
}

.cardlightbox-strip-thumb.active {
    border-color: var(--mud-palette-primary, #7e6fff);
}

.cardlightbox-variant-label {
    position: absolute;
    bottom: 12px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(0, 0, 0, .7);
    color: #fff;
    padding: 4px 14px;
    border-radius: 16px;
    font-size: .85rem;
    pointer-events: none;
}

/* ---- Admin card selection (bulk editor) ---- */
.card-select-check {
    position: absolute;
    top: 4px;
    left: 4px;
    z-index: 3;
    background: rgba(0, 0, 0, .45);
    border-radius: 4px;
    padding: 0 2px;
}

.card-selected {
    outline: 2px solid var(--mud-palette-primary, #7e6fff);
    outline-offset: 1px;
}

/* Guard against horizontal overflow shoving centered content (e.g. the register button) off the
   right edge on narrow screens. The app is single-column; nothing should scroll sideways. */
html, body {
    overflow-x: hidden;
    max-width: 100%;
}

/* ---- Nav section wayfinding accents ----
   Each top-level nav section carries a thin coloured left stripe and a matching icon tint so the long
   nav reads as distinct zones (Gallery / Roleplay / Blog / Account / Seller / Admin). Labels keep their normal
   colour for readability; only the stripe + section icon are tinted. Hues are CSS custom properties so
   they're easy to retune, and they reference MudBlazor palette tokens so day/night both work. The stripe
   sits on the link/group via border-left (no new stacking context — safe for overlays). */
.nav-sec {
    border-left: 3px solid var(--nav-accent, transparent);
}
/* Tint the section's own icon (the group header icon, or the standalone link's icon). Scoped to the
   direct icon so child nav-link icons inside a group stay neutral. */
.nav-sec > .mud-nav-link .mud-nav-link-icon,
.nav-sec > .mud-nav-group > .mud-nav-link .mud-nav-link-icon {
    color: var(--nav-accent, inherit);
}
.nav-sec-gallery  { --nav-accent: var(--mud-palette-info); }
.nav-sec-roleplay { --nav-accent: var(--mud-palette-secondary); }
.nav-sec-blog     { --nav-accent: var(--mud-palette-tertiary); }
.nav-sec-account  { --nav-accent: var(--mud-palette-warning); }
.nav-sec-seller   { --nav-accent: var(--mud-palette-success); }
.nav-sec-admin    { --nav-accent: var(--mud-palette-error); }

/* ---- Store/community links row at the bottom of the nav drawer ---- */
.nav-brand-row {
    display: flex;
    justify-content: center;
    gap: 8px;
    padding: 16px 12px;
    margin-top: auto;
    border-top: 1px solid var(--mud-palette-divider, #292838);
}

/* ---- App-bar brand link icons (favicons) ---- */
.brand-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    transition: background-color .15s ease;
}

.brand-link:hover {
    background-color: rgba(255, 255, 255, .12);
}

.brand-icon {
    width: 22px;
    height: 22px;
    object-fit: contain;
    display: block;
    border-radius: 4px;
}

/* ---- Announcements carousel ---- */
/* Single source of truth for the banner aspect ratio. The carousel box is locked to this ratio (not a
   fixed height), so it scales proportionally at every window width and always shows the same crop of the
   image. Slides are authored at this exact ratio by the uploader's cropper, so object-fit:cover never
   has anything to trim. Keep this in sync with AnnouncementCropper's AspectW/AspectH. */
.announcement-carousel {
    --announcement-ratio: 3 / 1;
    aspect-ratio: var(--announcement-ratio);
    width: 100%;
    border-radius: 12px;
    overflow: hidden;
}

.announcement-slide {
    position: relative;
    display: block;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, .15);
}

.announcement-clickable {
    cursor: pointer;
}

.announcement-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* Uploader cropper: a frame locked to the carousel ratio. The canvas itself draws the live preview; the
   empty-state overlay sits on top until an image is loaded. */
.announcement-cropper {
    position: relative;
    width: 100%;
    border-radius: 8px;
    overflow: hidden;
    background: rgba(0, 0, 0, .15);
}

.announcement-cropper-canvas {
    display: block;
    width: 100%;
}

.announcement-cropper-empty {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    text-align: center;
    padding: 16px;
    color: rgba(255, 255, 255, .75);
    pointer-events: none;
}

.announcement-caption {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 16px 20px;
    color: #fff;
    font-size: 1.1rem;
    font-weight: 600;
    background: linear-gradient(to top, rgba(0, 0, 0, .75), rgba(0, 0, 0, 0));
}

/* ---- Site footer (legal links) — unobtrusive, below the page body ---- */
.site-footer {
    margin-top: 48px;
    padding: 16px 8px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 8px;
    opacity: .6;
    border-top: 1px solid var(--mud-palette-divider, #292838);
}

.site-footer .site-footer-sep {
    opacity: .5;
}

/* ---- Site announcement / vacation strips (SiteBanner.razor) ----
   A slim, full-width strip at the top of the page body. Colour is driven by the announcement style
   (or the vacation variant); all pull from the active MudBlazor palette so day/night both look right.
   Kept calm and legible per the shopping-surface rules — no motion, no decoration. */
.site-banner {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 16px;
    margin: 0 -16px 16px;                  /* bleed to the left/right edges of MudMainContent's pa-4 padding */
    font-size: .9rem;
    line-height: 1.4;
    border-bottom: 1px solid var(--mud-palette-divider, #292838);
    /* --banner-accent is set per-variant below; text stays on the normal palette for legibility. */
    background: color-mix(in srgb, var(--banner-accent, var(--mud-palette-primary)) 14%, var(--mud-palette-surface));
    color: var(--mud-palette-text-primary);
    border-left: 4px solid var(--banner-accent, var(--mud-palette-primary));
}

.site-banner .site-banner-icon {
    display: inline-flex;
    align-items: center;
    color: var(--banner-accent, var(--mud-palette-primary));
    flex: 0 0 auto;
}

.site-banner .site-banner-text {
    flex: 1 1 auto;
}

.site-banner .site-banner-link {
    margin-left: 8px;
    font-weight: 600;
    color: var(--banner-accent, var(--mud-palette-primary));
    text-decoration: underline;
    white-space: nowrap;
}

.site-banner .site-banner-close {
    flex: 0 0 auto;
    background: none;
    border: none;
    cursor: pointer;
    font-size: 1rem;
    line-height: 1;
    padding: 4px 6px;
    border-radius: 4px;
    color: var(--mud-palette-text-secondary);
}

.site-banner .site-banner-close:hover {
    color: var(--mud-palette-text-primary);
    background: var(--mud-palette-action-default-hover, rgba(255, 255, 255, .08));
}

.site-banner-promo    { --banner-accent: var(--mud-palette-tertiary); }
.site-banner-info     { --banner-accent: var(--mud-palette-info); }
.site-banner-success  { --banner-accent: var(--mud-palette-success); }
.site-banner-warning  { --banner-accent: var(--mud-palette-warning); }
.site-banner-vacation { --banner-accent: var(--mud-palette-warning); }

/* ---- Legal pages (Terms, Privacy) ---- */
.legal-list {
    margin: 0 0 8px 0;
    padding-left: 22px;
    line-height: 1.6;
}

.legal-list li {
    margin-bottom: 4px;
}

/* ---- Scroll-to-top button (gallery pages) — unobtrusive, bottom-right ----
   MudScrollToTop owns the fixed positioning, z-index and show/hide on its
   wrapper (.scroll-to-top). Do NOT position the inner FAB: pulling it out of
   the wrapper flow collapses the wrapper (the actual click/scroll target) to
   zero size, so the button renders but clicking it does nothing. Style the
   FAB's appearance only. */
.scroll-to-top-fab {
    opacity: .55;
    transition: opacity .15s ease;
}

.scroll-to-top-fab:hover {
    opacity: 1;
}

/* ---- Cart line thumbnail (cart page) ---- */
.cart-line-thumb {
    width: 64px;
    height: 64px;
    object-fit: cover;
    border-radius: 6px;
    flex: 0 0 auto;
    display: block;
}

.cart-line-thumb-link {
    flex: 0 0 auto;
    line-height: 0;
    border-radius: 6px;
    transition: opacity .15s ease;
}

.cart-line-thumb-link:hover {
    opacity: .85;
}

/* ---- Admin inventory table row thumbnail ---- */
.admin-card-row-thumb {
    width: 44px;
    height: 44px;
    object-fit: cover;
    border-radius: 4px;
    cursor: pointer;
    display: block;
    transition: opacity .15s ease;
}

.admin-card-row-thumb:hover {
    opacity: .8;
}

/* ---- Auth forms (Login / Register / Forgot / Reset) ----
   These pages render statically (no Blazor circuit), so they use plain semantic <input>s instead of
   the MudStatic* components, whose companion JS doesn't run here. Styled with MudBlazor palette vars so
   they track the active theme (dark/light). */
.auth-form {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.auth-field {
    display: flex;
    flex-direction: column;
    margin-bottom: 16px;
}

.auth-field label {
    font-size: .8rem;
    margin-bottom: 6px;
    color: var(--mud-palette-text-secondary, #92929f);
}

.auth-field input {
    width: 100%;
    box-sizing: border-box;
    padding: 12px 14px;
    font: inherit;
    color: var(--mud-palette-text-primary, #b2b0bf);
    background: var(--mud-palette-background-grey, rgba(255, 255, 255, .04));
    border: 1px solid var(--mud-palette-lines-inputs, #44424f);
    border-radius: 8px;
    transition: border-color .15s ease, box-shadow .15s ease;
}

.auth-field input:focus {
    outline: none;
    border-color: var(--mud-palette-primary, #7e6fff);
    box-shadow: 0 0 0 2px rgba(126, 111, 255, .25);
}

.auth-field input::placeholder {
    color: var(--mud-palette-text-disabled, #ffffff55);
}

/* Validation messages from <ValidationMessage>. */
.auth-field .validation-message {
    margin-top: 6px;
    font-size: .78rem;
    color: var(--mud-palette-error, #ff3f5f);
}

/* Up-front field guidance (e.g. the password policy), shown before submit. */
.auth-field .auth-hint {
    margin-top: 6px;
    font-size: .78rem;
    color: var(--mud-palette-text-secondary, #92929f);
}

.auth-checkbox {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 4px 0 8px;
    cursor: pointer;
    color: var(--mud-palette-text-primary, #b2b0bf);
}

.auth-checkbox input {
    width: 18px;
    height: 18px;
    accent-color: var(--mud-palette-primary, #7e6fff);
    cursor: pointer;
}

.auth-submit {
    width: 100%;
    margin-top: 8px;
    padding: 12px 20px;
    font: inherit;
    font-weight: 600;
    color: #fff;
    background: var(--mud-palette-primary, #7e6fff);
    border: none;
    border-radius: 8px;
    cursor: pointer;
    transition: filter .15s ease;
}

.auth-submit:hover {
    filter: brightness(1.1);
}

/* Discord's brand colour, for the "Continue with Discord" button on Login/Register. */
.auth-submit.auth-discord {
    background: #5865f2;
}

/* "or" separator between the password form and the Discord button. */
.auth-divider {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 16px 0 4px;
    color: var(--mud-palette-text-secondary, #92929f);
    font-size: .8rem;
}

.auth-divider::before,
.auth-divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--mud-palette-lines-default, #ffffff22);
}

/* Help/support pages: readable indented lists. */
.help-list { margin: 0 0 1rem 0; padding-left: 1.4rem; line-height: 1.6; }
.help-list li { margin-bottom: .35rem; }

/* ---- Roleplay chat transcript ---- */
/* The scroll container; the MudPaper around it is the sized flex box. */
.rp-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    scroll-behavior: smooth;
}

/* One message row. User messages align right, characters left — the familiar chat layout. */
.rp-msg {
    display: flex;
    flex-direction: column;
    margin-bottom: 14px;
    max-width: 82%;
}
.rp-msg-user { margin-left: auto; align-items: flex-end; }
.rp-msg-char { margin-right: auto; align-items: flex-start; }

.rp-speaker { margin-bottom: 2px; font-weight: 600; opacity: .8; }
.rp-msg-head { width: 100%; }

/* The bubble. Distinct fills for the two sides; rounded with one squared corner toward the speaker. */
.rp-bubble {
    padding: 8px 12px;
    border-radius: 14px;
    line-height: 1.5;
    word-break: break-word;
    box-shadow: 0 1px 2px rgba(0, 0, 0, .12);
}
/* Roleplay rich text: narration (*asterisks*) renders as italic on its own line, dialogue stays upright,
   so a reply reads as alternating action/speech instead of a flat block. The narration is slightly muted
   to set it apart from spoken lines. */
.rp-rich em {
    font-style: italic;
    opacity: .82;
}
.rp-msg-user .rp-bubble .rp-rich em { opacity: .9; } /* keep contrast on the dark user bubble */
.rp-msg-char .rp-bubble {
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-bottom-left-radius: 4px;
}
/* User bubble: a deeper magenta→violet fill (still on-brand vaporwave) with solid white text — the
   light primary pink left white text hard to read. The gradient + faint text shadow lift contrast well
   past WCAG AA for body text in both palettes. */
.rp-msg-user .rp-bubble {
    background: linear-gradient(135deg, #c2247a 0%, #8b2fd6 100%);
    color: #ffffff;
    text-shadow: 0 1px 1px rgba(0, 0, 0, .35);
    border-bottom-right-radius: 4px;
}
.rp-msg-user .rp-bubble a { color: #ffe3f3; }

/* The streaming cursor fades in/out while the character writes (smooth, not a hard on/off flicker). */
.rp-caret { animation: rp-blink 1.1s ease-in-out infinite; opacity: .7; }
@keyframes rp-blink { 0%, 100% { opacity: .7; } 50% { opacity: 0; } }

/* "Typing" indicator shown before the first token arrives — three dots bouncing in sequence. Sized to
   sit on the text baseline like a short word, so it occupies about the same space the caret did. */
.rp-typing {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 1.5em; /* match the bubble's line-height so the row doesn't jump when text replaces it */
    vertical-align: text-bottom;
}
.rp-typing .rp-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
    opacity: .45;
    animation: rp-typing-bounce 1.2s ease-in-out infinite;
}
.rp-typing .rp-dot:nth-child(2) { animation-delay: .15s; }
.rp-typing .rp-dot:nth-child(3) { animation-delay: .3s; }
@keyframes rp-typing-bounce {
    0%, 80%, 100% { transform: translateY(0); opacity: .45; }
    40% { transform: translateY(-3px); opacity: .9; }
}

/* "Thinking" ticker: while the model reasons (thinking on) before the reply starts, the latest complete
   reasoning sentence shows here in place of the dots. A single clipped line — when a new sentence completes,
   the old one is replaced and the new line rises in from the bottom, so it reads as scrolling upward. Dimmed
   and italic to signal "this is the model's scratch thinking, not the reply." Ephemeral; never persisted. */
.rp-thinking {
    display: block;
    overflow: hidden;       /* clip the rising line so it appears to scroll out of its row */
    height: 1.5em;          /* one line, matches the bubble line-height (no row jump when the reply replaces it) */
    line-height: 1.5em;
    max-width: 100%;
    opacity: .6;
    font-style: italic;
    font-size: .92em;
}
.rp-thinking-line {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    /* Each new sentence (fresh element via @key) rises up from the bottom edge and fades in. GPU-cheap. */
    animation: rp-thinking-rise .32s ease-out;
}
@keyframes rp-thinking-rise {
    from { transform: translateY(1.5em); opacity: 0; }
    to   { transform: translateY(0);     opacity: 1; }
}

/* The per-message action icons (edit/delete/regenerate/swipe) sit quiet until you hover the row. */
.rp-msg .rp-msg-head .mud-icon-button { opacity: .35; transition: opacity .15s ease; }
.rp-msg:hover .rp-msg-head .mud-icon-button { opacity: 1; }

/* Recall indicator (issue #246): a small, quiet "🧠 N" chip showing how many long-term memories the reply
   drew on; clicking it opens a popover. Kept compact and unobtrusive so the transcript stays readable. */
.rp-recall-chip { height: 20px; min-width: 0; font-size: .72rem; opacity: .55; transition: opacity .15s ease; }
.rp-msg:hover .rp-recall-chip { opacity: 1; }
.rp-recall-panel { max-width: 340px; min-width: 240px; }
/* Each recalled item is a collapsible row: a left-aligned title button over the revealed text. */
.rp-recall-item { border-top: 1px solid var(--mud-palette-divider); }
.rp-recall-item:first-of-type { border-top: none; }
.rp-recall-item-head { justify-content: flex-start; text-transform: none; text-align: left; }
.rp-recall-item-title { overflow-wrap: anywhere; }

/* Cost indicator (issue #267): a small, quiet "🪙 N" chip showing the credits a reply cost; clicking it opens
   a popover with the token breakdown and charge-state. Same quiet treatment as the recall chip so the two sit
   together without cluttering the transcript. */
.rp-cost-chip { height: 20px; min-width: 0; font-size: .72rem; opacity: .55; transition: opacity .15s ease; }
.rp-msg:hover .rp-cost-chip { opacity: 1; }
.rp-cost-panel { max-width: 320px; min-width: 240px; }
.rp-cost-credits { line-height: 1; }
.rp-cost-row { padding: 2px 0; }

@media (prefers-reduced-motion: reduce) {
    .rp-scroll { scroll-behavior: auto; }
    .rp-caret { animation: none; }
    /* No bounce; keep the dots visible (static) so the "typing" state is still conveyed. */
    .rp-typing .rp-dot { animation: none; opacity: .55; }
    /* No rise; the sentence still swaps in place (text changes), just without motion. */
    .rp-thinking-line { animation: none; }
}

/* Chat header title: truncate a long character name to one line so it never wraps and pushes the header
   down onto the session tabs below. min-width:0 lets a flex child actually shrink to allow the ellipsis. */
.rp-chat-title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Guest advertisement hero on /roleplay — a subtle primary wash, kept calm and legible. */
.rp-guest-hero {
    background:
        linear-gradient(135deg,
            rgba(var(--mud-palette-primary-rgb), .06),
            rgba(var(--mud-palette-secondary-rgb), .04));
}

/* Locked redemption-character teaser cards on /roleplay — greyed and slightly muted so they read as
   "not yours yet" without being invisible. The placeholder tile fills the avatar slot. */
.rp-locked-card {
    opacity: .72;
    filter: grayscale(35%);
}
.rp-locked-card:hover {
    opacity: .9;
}

/* ---- Roleplay guided tour (issue #274) ---- */
/* The dialog's staged mini-chat scrolls within this box; it's also the positioning context the JS scrolls
   a focused target into view inside. position:relative so the absolutely-positioned ring/callout below
   are placed against the dialog content. The tour dialog is full-screen (issue #288), so this fills the
   content area and scrolls internally rather than being capped to a fraction of the viewport. */
.rp-tour-scroll {
    position: relative;
    height: 100%;
    overflow-y: auto;
}
/* In the full-screen tour dialog, let the content area grow so .rp-tour-scroll has a height to fill. */
.mud-dialog-fullscreen .rp-tour-scroll {
    min-height: 100%;
}
/* On a wide window the staged chat reads as a centred column (like a real chat) rather than stretching
   edge-to-edge. The spotlight ring measures each target's live rect, so centring doesn't break placement. */
.rp-tour-content {
    max-width: 900px;
    margin-inline: auto;
}

/* The spotlight ring: a thin highlight border around the focused element whose enormous, low-alpha
   box-shadow dims everything else in the dialog (the "spotlight" cut-out). Pointer-events:none so it never
   intercepts clicks; affinity-tour.js sets top/left/width/height each step. Fixed-positioned to track the
   element against the viewport (the JS measures with getBoundingClientRect). */
.rp-tour-ring {
    position: fixed;
    pointer-events: none;
    border: 2px solid var(--mud-palette-primary);
    border-radius: 8px;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, .55);
    z-index: 1402; /* above MudDialog content, below the callout */
    transition: top .25s ease, left .25s ease, width .25s ease, height .25s ease;
}

/* The callout bubble beside the focused element: the step's title/body and Next/Previous/Skip controls.
   Clamped to a width that reflows on a phone; the JS positions it (below the target, flipping above when
   there's no room) and clamps it horizontally so it never spills off a narrow viewport. */
.rp-tour-callout {
    position: fixed;
    z-index: 1403; /* above the ring so its buttons are clickable */
    width: min(320px, calc(100vw - 24px));
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 12px;
    box-shadow: 0 8px 28px rgba(0, 0, 0, .35);
    padding: 16px;
}

@media (prefers-reduced-motion: reduce) {
    .rp-tour-ring { transition: none; }
}

/* ---- Roleplay session tabs ---- */
/* The whole strip: a "Sessions" label, the horizontally-scrolling chip track, and a pinned + button.
   A top border sets it off from the header controls above so the chips don't read as squeezed in. */
.rp-tabs-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    padding-top: 8px;
    border-top: 1px solid var(--mud-palette-lines-default);
}
.rp-tabs-heading {
    flex: 0 0 auto;
    text-transform: uppercase;
    letter-spacing: .04em;
    font-size: .68rem;
}
.rp-tabs {
    display: flex;
    align-items: center;
    gap: 6px;
    overflow-x: auto;
    flex-wrap: nowrap;
    flex: 1 1 auto;
    min-width: 0;          /* let the track shrink so the pinned + stays visible */
    padding-bottom: 2px;
}
/* Pinned "new session" button: never scrolls away with the chips. */
.rp-tab-new { flex: 0 0 auto; }
.rp-tab {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 2px 4px 2px 10px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 14px;
    white-space: nowrap;
    cursor: pointer;
    font-size: .82rem;
    background: var(--mud-palette-surface);
    transition: background .15s ease, border-color .15s ease;
}
.rp-tab:hover { border-color: var(--mud-palette-primary); }
.rp-tab-active {
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    border-color: var(--mud-palette-primary);
}
.rp-tab-label { padding: 2px 0; }
.rp-tab-close { opacity: .5; }
.rp-tab:hover .rp-tab-close { opacity: 1; }

/* ---- Blog ---------------------------------------------------------------
   Typographic styles for rendered Markdown post bodies (.blog-content) and a
   monospace source area for the editor. Functional only; the decorative layer
   stays in vaporwave.css. */
.blog-content {
    line-height: 1.7;
    font-size: 1.02rem;
    overflow-wrap: anywhere;
}
.blog-content > :first-child { margin-top: 0; }
.blog-content h1, .blog-content h2, .blog-content h3,
.blog-content h4, .blog-content h5, .blog-content h6 {
    margin: 1.6em 0 .5em;
    line-height: 1.25;
    font-weight: 600;
}
.blog-content h1 { font-size: 1.8rem; }
.blog-content h2 { font-size: 1.5rem; }
.blog-content h3 { font-size: 1.25rem; }
.blog-content p { margin: 0 0 1em; }
.blog-content ul, .blog-content ol { margin: 0 0 1em; padding-left: 1.6em; }
.blog-content li { margin: .25em 0; }
.blog-content a { color: var(--mud-palette-primary); text-decoration: underline; }
.blog-content img { max-width: 100%; height: auto; border-radius: 6px; }
.blog-content blockquote {
    margin: 0 0 1em;
    padding: .3em 1em;
    border-left: 3px solid var(--mud-palette-primary);
    color: var(--mud-palette-text-secondary);
}
.blog-content code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: .9em;
    background: var(--mud-palette-surface);
    padding: .1em .35em;
    border-radius: 4px;
}
.blog-content pre {
    background: var(--mud-palette-surface);
    padding: 1em;
    margin: 0;
    border-radius: 0;
    /* Wrap long lines instead of a side-scroll: a code block reads as a block, not a one-line ticker. */
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.blog-content pre code {
    background: none;
    padding: 0;
    white-space: inherit;
}

/* Enhanced code block (built by blog-code.js): a delineated card with a header bar + Copy button.
   The script wraps each <pre> in <div class="blog-code"> with a <div class="blog-code-header">. */
.blog-content .blog-code {
    margin: 0 0 1em;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 6px;
    overflow: hidden;
}
.blog-content .blog-code-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: .3em .6em .3em .8em;
    background: var(--mud-palette-background-grey);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}
.blog-content .blog-code-lang {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: .75em;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
}
.blog-content .blog-code-copy {
    font: inherit;
    font-size: .8em;
    cursor: pointer;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 4px;
    padding: .15em .6em;
    background: var(--mud-palette-surface);
    color: var(--mud-palette-text-primary);
    transition: background-color .15s ease, color .15s ease;
}
.blog-content .blog-code-copy:hover { background: var(--mud-palette-action-default-hover); }
.blog-content .blog-code-copy.is-copied {
    background: var(--mud-palette-success);
    color: var(--mud-palette-success-text);
    border-color: var(--mud-palette-success);
}
/* The wrapped <pre> sits flush inside the card (the card owns the rounding/border now). */
.blog-content .blog-code pre { border-radius: 0; }
.blog-content table { border-collapse: collapse; margin: 0 0 1em; width: 100%; }
.blog-content th, .blog-content td {
    border: 1px solid var(--mud-palette-lines-default);
    padding: .4em .6em;
    text-align: left;
}
.blog-md-source textarea {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: .92rem;
    line-height: 1.5;
}

/* ---- Checkout reward progress (functional) ---- */
.reward-progress { width: 100%; }
.reward-progress__earned { display: flex; flex-wrap: wrap; }
.reward-progress__bar { height: 8px; }
.reward-progress__caption { display: flex; align-items: center; }

/* ---- Home SEO intro (functional) ----
   A keyword-rich <h1> + lede on the single-seller home page. Kept visually restrained (not a giant
   hero) so the shopping surface stays calm and legible — the value is semantic, for crawlers. */
.home-seo-intro-h1 {
    font-size: 1.75rem;
    line-height: 1.2;
    font-weight: 600;
    margin: 0;
}
@media (min-width: 960px) {
    .home-seo-intro-h1 { font-size: 2.125rem; }
}

/* ---- SEO landing pages + hub (functional) ----
   Calm, legible surfaces (shopping/content rule): plain grid + list, no decoration. The card grid uses
   real <a> anchors; keep it responsive and cheap. */
.landing-h1 { font-size: 2rem; line-height: 1.2; font-weight: 600; margin: 0; }
.landing-h2 { font-size: 1.375rem; font-weight: 600; margin-bottom: .5rem; }
@media (min-width: 960px) { .landing-h1 { font-size: 2.5rem; } }

.landing-breadcrumb { font-size: .875rem; color: var(--mud-palette-text-secondary); }
.landing-breadcrumb a { color: inherit; text-decoration: none; }
.landing-breadcrumb a:hover { text-decoration: underline; }

.landing-card-grid {
    list-style: none; margin: 0; padding: 0;
    display: grid; gap: 12px;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.landing-card { margin: 0; }
.landing-card-link { display: block; text-decoration: none; color: inherit; }
.landing-card-thumb {
    display: block; aspect-ratio: 5 / 7; overflow: hidden; border-radius: 8px;
    background: var(--mud-palette-background-grey);
}
.landing-card-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.landing-card-caption { display: block; font-size: .8125rem; margin-top: 6px; line-height: 1.3; }
.landing-empty { color: var(--mud-palette-text-secondary); }

.landing-hub-list {
    list-style: none; margin: 0 0 .5rem; padding: 0;
    display: grid; gap: 4px 16px;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.landing-hub-list li { margin: 0; }
.landing-hub-count { color: var(--mud-palette-text-secondary); font-size: .8125rem; margin-left: 4px; }

/* ImageGen Studio: let the prompt textarea be dragged taller (vertical only) for long prompts.
   The wrapping MudTextField uses AutoGrow elsewhere; here we opt out of it (see Studio.razor) so the
   manual drag height sticks. Horizontal resize is disabled to keep the two-column layout intact.
   MudBlazor's own `.mud-input>textarea.mud-input-root` forces `resize:none` at specificity (0,2,1),
   so this override must out-specify it — matching that selector under our marker class does (0,4,1).
   A plain `.imagegen-prompt-resizable textarea` (0,1,1) loses and the drag handle never appears. */
.imagegen-prompt-resizable .mud-input > textarea.mud-input-root {
    resize: vertical;
    min-height: 6.5em;
}

/* ImageGen Studio: the inline prompt-library popover (account-synced nested folder tree of snippets). */
.imagegen-prompt-library { min-width: 280px; max-width: 360px; padding-bottom: 4px; }
.imagegen-prompt-tree { max-height: 320px; overflow-y: auto; }
.imagegen-prompt-row { padding-right: 4px; }
.imagegen-prompt-snip { padding-right: 4px; }
.imagegen-prompt-label.mud-button-root { text-transform: none; min-width: 0; padding-top: 2px; padding-bottom: 2px; }
.imagegen-prompt-banner { border-radius: 4px; padding: 4px 8px; margin-bottom: 4px; background: var(--mud-palette-action-default-hover); font-size: 0.8125rem; }
.imagegen-prompt-target { outline: 1px dashed var(--mud-palette-primary); outline-offset: -1px; border-radius: 4px; }
.imagegen-prompt-moving { background: var(--mud-palette-action-default-hover); border-radius: 4px; }
