/*  Self-hosted Fonts
----------------------------------------------------- */
/* Variable Karla covers all weights (200-800) in a single file.
   Lobster Two ships only as fixed weights — we use Bold as the display face. */
@font-face {
    font-family: "Karla";
    src: url("/fonts/Karla.woff2") format("woff2");
    font-weight: 200 800;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Karla";
    src: url("/fonts/Karla-Italic.woff2") format("woff2");
    font-weight: 200 800;
    font-style: italic;
    font-display: swap;
}

@font-face {
    font-family: "Lobster Two";
    src: url("/fonts/LobsterTwo-Bold.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/*  Accessibility
----------------------------------------------------- */

/* Visually hidden but accessible to screen readers */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Skip navigation link — visible on focus for keyboard users */
.skip-link {
    position: absolute;
    top: -100%;
    left: 50%;
    transform: translateX(-50%);
    z-index: 100;
    padding: 12px 24px;
    background: #000;
    color: #fff;
    text-decoration: underline;
    border-radius: 0 0 4px 4px;
    font-size: 16px;
}

.skip-link:focus {
    top: 0;
}

/* Focus styles for keyboard navigation */
a:focus-visible,
input:focus-visible,
button:focus-visible {
    outline: 2px solid var(--text-tertiary, rgba(255, 255, 255, 0.7));
    outline-offset: 2px;
    border-radius: 2px;
}

/* Remove default outline for mouse clicks but keep for keyboard */
a:focus:not(:focus-visible),
input:focus:not(:focus-visible),
button:focus:not(:focus-visible) {
    outline: none;
}

/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/*  Base Components
----------------------------------------------------- */
* {
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
}

html {
    width: 100%;
    height: 100%;
    background: inherit;
}

body {
    background: #000;
    font-family: "Karla", "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 18px;
    line-height: 30px;
    color: #fff;
    text-align: center;
    width: 100%;
    min-height: 100%;
    font-weight: 400;
    display: flex;
    flex-direction: column;
}

h1 {
    font-family: "Lobster Two", "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-weight: 700;
    font-size: 28px;
    line-height: 36px;
    color: var(--text-secondary, rgba(255, 255, 255, 0.8));
    transition: color 0.2s ease-out;
}

h1:hover {
    color: #fff;
}

h2 {
    margin-top: 10px;
    font-size: 16px;
    line-height: 24px;
    font-weight: 400;
    color: var(--text-tertiary, rgba(255, 255, 255, 0.7));
}

@media screen and (max-width: 480px) {
    /* Compact header on mobile to de-emphasize and let content shine */

    h1 {
        font-size: 24px;
        line-height: 30px;
    }

    h2 {
        margin-top: 5px;
        font-size: 14px;
        line-height: 20px;
    }
}

a,
a:hover {
    color: #fff;
    text-decoration: underline;
}

h1 a,
h1 a:hover {
    text-decoration: none;
    color: inherit;
}

h2 a {
    font-weight: 700;
}

p {
    color: var(--text-primary, rgba(255, 255, 255, 0.9));
    font-size: 16px;
    line-height: 25px;
    font-weight: 400;
    text-align: center;
}

hr {
    background: #fff;
    background: var(--border-subtle, rgba(255, 255, 255, 0.25));
    height: 1px;
    border: 0;
    margin: 20px 0;
}

@media screen and (max-width: 480px) {
    hr {
        margin: 12px 0;
    }

    /* Hide the hr between header and username section on mobile */
    header + hr {
        display: none;
    }
}

/*  Wrapper - background color is set dynamically via JS
----------------------------------------------------- */
#wrap {
    padding: 0 30px;
    text-align: center;
    background: #000;
    min-height: 100vh;
    flex: 1;
    display: flex;
    flex-direction: column;
}

/*  Header
----------------------------------------------------- */
header {
    margin: 35px 0 20px;
}

@media screen and (max-width: 480px) {
    header {
        margin: 30px 0 20px;
    }
}

/*  Music data content
----------------------------------------------------- */
.content {
    text-align: center;
    margin-top: 0px;
    margin-bottom: 50px;
    /* Limit to 4 tiles per row: 4 × (250px tile + 30px margin) = 1120px */
    max-width: 1120px;
    margin-left: auto;
    margin-right: auto;
}

/*  Music Personality Headline - Hero element, larger than site title
----------------------------------------------------- */
.music-personality {
    font-family: "Lobster Two", "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-weight: 700;
    font-size: 50px;
    line-height: 1.2;
    text-align: center;
    margin: 50px auto 30px;
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.6s ease-out;
}

.music-personality.loading,
.music-personality.visible {
    visibility: visible;
    opacity: 1;
}

.music-personality .personality-label {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    font-family: "Karla", "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 15px;
    line-height: 20px;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    font-weight: 400;
    white-space: nowrap;
    color: var(--text-secondary, rgba(255, 255, 255, 0.8));
    margin-bottom: 5px;
}

/* Decorative lines on either side of the label */
.music-personality .personality-label::before,
.music-personality .personality-label::after {
    content: "";
    flex-shrink: 1;
    width: 40px;
    height: 1px;
    background: var(--text-muted, rgba(255, 255, 255, 0.4));
}

/* Fixed-height container for rolling/text to prevent layout shift */
.music-personality .personality-content {
    display: block;
    min-height: 65px;
    padding: 0 10px;
    font-style: italic;
}

/* Rolling text loading animation */
.music-personality .personality-rolling {
    display: inline-block;
    color: var(--text-muted, rgba(255, 255, 255, 0.6));
    font-style: italic;
    font-size: 40px;
    transition: opacity 0.3s ease-out;
}

.music-personality .personality-rolling.fading-out {
    opacity: 0;
}

/* Actual headline text - starts hidden */
.music-personality .personality-text {
    display: none;
    opacity: 0;
}

/* When visible, hide rolling and show text with fade-in + glow */
.music-personality.visible .personality-rolling {
    display: none;
}

.music-personality.visible .personality-text {
    display: inline;
    color: #fff;
    animation: personalityReveal 2s ease-out forwards;
    /* Glow uses CSS variable set by JS, falls back to white */
    text-shadow:
        0 0 10px var(--personality-glow, rgba(255, 255, 255, 0.4)),
        0 0 30px var(--personality-glow, rgba(255, 255, 255, 0.4));
}

@keyframes personalityReveal {
    0% {
        opacity: 0;
        transform: translateY(10px);
    }
    100% {
        opacity: 1;
        transform: translateY(0);
    }
}

@media screen and (max-width: 480px) {
    .music-personality {
        margin: 30px auto;
    }
    .music-personality .personality-label {
        font-size: 13px;
        gap: 8px;
        letter-spacing: 1px;
    }
    .music-personality .personality-label::before,
    .music-personality .personality-label::after {
        width: 20px;
        min-width: 0;
    }
    .music-personality .personality-content {
        min-height: auto;
    }
}

/*  Artist tiles
----------------------------------------------------- */

/* Anchor wrapper for artist tiles - needs block display for proper focus outline */
.content a:has(.artist) {
    display: inline-block;
    border-radius: 8px;
    margin: 15px;
    text-decoration: none;
}

/* Override default focus styles for artist tile links */
.content a:has(.artist):focus-visible {
    outline: 2px solid var(--text-tertiary, rgba(255, 255, 255, 0.7));
    outline-offset: 3px;
}

.artist {
    position: relative;
    display: block;
    width: 250px;
    height: 250px;
    background-color: rgba(0, 0, 0, 0.5);
    background-position: center center;
    background-size: cover;
    -webkit-box-shadow: 2px 2px 4px 0 rgba(0, 0, 0, 0.25);
    -moz-box-shadow: 2px 2px 4px 0 rgba(0, 0, 0, 0.25);
    box-shadow: 2px 2px 4px 0 rgba(0, 0, 0, 0.25);
    /* Prevent any layout shift during crossfade */
    overflow: hidden;
    border-radius: 8px;
    transition: box-shadow 0.2s ease-out;
}

/* Inset specular overlay — subtle light edge + soft inner glow (Layer 4)
   mix-blend-mode: screen lets the underlying image colors tint the glow,
   so bright/saturated image edges produce a colored specular highlight
   while dark edges keep the glow subtle and neutral. */
.artist::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border-radius: 8px;
    box-shadow:
        inset 0 0 0 1px rgba(255, 255, 255, 0.15),
        inset 0 0 8px 2px rgba(255, 255, 255, 0.07),
        inset 0 1px 3px 0 rgba(255, 255, 255, 0.12);
    mix-blend-mode: screen;
    z-index: 3;
    pointer-events: none;
}

.artist.loading-image::before,
.artist.loading-active::before {
    box-shadow:
        inset 0 0 0 1px var(--border-specular, rgba(255, 255, 255, 0.12)),
        inset 0 0 6px 1px rgba(255, 255, 255, 0.04),
        inset 0 1px 2px 0 rgba(255, 255, 255, 0.08);
    mix-blend-mode: normal;
}

/* Source layer divs for smooth crossfade transitions between image sources (Layers 6-8) */
.source-layer {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-position: center center;
    background-size: cover;
    opacity: 0;
    transition: opacity 0.8s ease-in-out;
    z-index: 1;
    pointer-events: none;
}

/* Active source layer is visible */
.source-layer.active {
    opacity: 1;
}

/* Star placeholder base styles (Layer 5) */
.artist.no-image::after,
.artist.loading-image::after,
.artist.loading-active::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 150px;
    height: 150px;
    /* Standard 5-pointed star polygon, solid white */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpolygon fill='white' points='50,5 61,40 98,40 68,62 79,97 50,75 21,97 32,62 2,40 39,40'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 150px 150px;
    z-index: 2;
}

/* Waiting in queue - static, very transparent star */
.artist.loading-image::after {
    opacity: 0.15;
}

/* Currently loading - pulsing star animation */
.artist.loading-active::after {
    animation: pulse 1.5s ease-in-out infinite;
}

@keyframes pulse {
    0%,
    100% {
        opacity: 0.3;
    }
    50% {
        opacity: 0.7;
    }
}

/* No image found - star uses CSS variable for background color (set by JS) */
.artist.no-image::after {
    opacity: 1;
    /* Star color comes from CSS variable --bg-color set by JS, fallback to dark */
    background-image: var(
        --star-bg,
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpolygon fill='%23333' points='50,5 61,40 98,40 68,62 79,97 50,75 21,97 32,62 2,40 39,40'/%3E%3C/svg%3E")
    );
}

/*  Hover effect on artist tiles (Layer 3)
----------------------------------------------------- */
.artist .dark {
    opacity: 0;
    position: absolute;
    bottom: 0;
    right: 0;
    top: 0;
    left: 0;
    z-index: 4;
    border-radius: 8px;
    background: #000;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
}

.artist:hover .dark {
    opacity: 0.35;
    box-shadow:
        inset 0 0 0 1px rgba(255, 255, 255, 0.08),
        inset 0 0 8px 2px rgba(0, 0, 0, 0.15);
}

/*  Boxed title styling (Layers 1-2: title text + backdrop blur)
----------------------------------------------------- */
.title {
    z-index: 5;
    position: absolute;
    bottom: 0;
    right: 0;
    left: auto;
    opacity: 0;
    background: none;
    -webkit-backdrop-filter: blur(8px) saturate(200%);
    backdrop-filter: blur(8px) saturate(200%);
    margin: 0 10px 10px;
    padding: 5px 10px;
    text-decoration: none;
    text-align: right;
    font-weight: 400;
    font-size: 20px;
    line-height: 25px;
    color: #fff;
    border-radius: 4px;
    overflow: hidden;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

.title::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    /* Intentionally use white for blur behavior */
    background: rgba(255, 255, 255, 0.15);
    border-radius: 4px;
    border: 1px solid rgba(255, 255, 255, 0.3);
    z-index: -1;
    /* Ease backdrop color/border with the same 0.8s ease-in-out window as
       .source-layer's opacity crossfade above, so the title theme transition
       feels cohesive with the underlying image change rather than snapping. */
    transition:
        background-color 0.8s ease-in-out,
        border-color 0.8s ease-in-out;
}

.title:hover {
    opacity: 0.8;
    text-decoration: none;
}

.title span {
    display: block;
    margin-top: 4px;
    font-size: 14px;
    line-height: 18px;
    font-style: italic;
    opacity: 0.8;
    margin-bottom: 4px;
}

.artist:hover .title,
.content a:has(.artist):focus-visible .title {
    opacity: 1;
}

@media screen and (max-width: 480px) {
    /* On mobile, dark overlay only shows on hover (via :hover rule above) */
    .title {
        opacity: 1;
    }
}

.artist.light-image .title {
    /* text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); */
}

.artist.light-image .title::before {
    background: rgba(0, 0, 0, 0.15);
    border: 1px solid rgba(255, 255, 255, 0.5);
}

.artist.light-image .title span {
    /* text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); */
}

/*  Username Section (sticky on desktop only)
----------------------------------------------------- */
.username-section {
    padding: 15px 0 20px;
    margin: -15px 0 0;
    border-bottom: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.25));
}

.username-section p {
    margin: 0;
    line-height: 40px;
}

.username-section a {
    font-weight: 700;
}

/* Sticky only on larger viewports */
@media screen and (min-width: 481px) {
    .username-section {
        position: sticky;
        top: 0;
        z-index: 20;
        background: inherit;
    }
}

/* Compact username section on mobile to de-emphasize */
@media screen and (max-width: 480px) {
    .username-section {
        padding: 10px 0 20px;
    }

    .username-section p {
        font-size: 13px;
        line-height: 28px;
        color: var(--text-muted, rgba(255, 255, 255, 0.5));
    }
}

/*  Form fields
----------------------------------------------------- */
input[type="text"] {
    border: 1px solid var(--input-border, rgba(255, 255, 255, 0.6));
    background: var(--input-bg, rgba(255, 255, 255, 0.15));
    font-family: "Karla", "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 15px;
    line-height: 15px;
    padding: 10px 12px;
    min-height: 44px;
    margin: 0 5px;
    min-width: 150px;
    color: #fff;
    text-align: left;
    font-weight: 400;
    border-radius: 5px;
    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}

input[type="text"]:focus {
    border: 1px solid var(--input-border-focus, rgba(255, 255, 255, 0.8));
    background: var(--input-bg-focus, rgba(255, 255, 255, 0.3));
    box-shadow:
        inset 0 1px 3px rgba(0, 0, 0, 0.2),
        0 0 12px var(--personality-glow, rgba(255, 255, 255, 0.2));
}

input[type="text"]::placeholder {
    color: var(--text-tertiary, rgba(255, 255, 255, 0.7));
}

@media screen and (max-width: 480px) {
    input[type="text"] {
        display: block;
        margin: 0 auto;
        width: 65%;
        /* Prevent Safari iOS auto-zoom on focus - requires 16px minimum */
        font-size: 16px;
    }
}

/*  Transitions
----------------------------------------------------- */
a,
.title,
.dark,
input {
    -webkit-transition: all 0.2s ease-out;
    -moz-transition: all 0.2s ease-out;
    -ms-transition: all 0.2s ease-out;
    -o-transition: all 0.2s ease-out;
    transition: all 0.2s ease-out;
}

/*  Error States
----------------------------------------------------- */
.error-state {
    text-align: center;
    padding: 150px 30px;
    max-width: 700px;
    margin: 0 auto;
}

.error-state p {
    margin-bottom: 10px;
    font-size: 18px;
    line-height: 30px;
    color: var(--text-tertiary, rgba(255, 255, 255, 0.7));
}

/*  Loading State for Artist Images - plain background only
----------------------------------------------------- */
/* No shimmer or special effects during loading */

/* Fade-in animation for images */
@keyframes fadeIn {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

.artist.image-loaded {
    animation: fadeIn 0.5s ease-in-out;
}

/*  Footer
----------------------------------------------------- */
footer {
    padding: 20px 30px;
    margin: 0 -30px;
    background: linear-gradient(to top, transparent 0%, rgba(0, 0, 0, 0.35) 60%, rgba(0, 0, 0, 0.5) 100%);
    text-align: center;
}

footer hr {
    margin: 15px 0;
}

footer p {
    font-size: 14px;
    line-height: 22px;
    margin: 0;
}

.footer-content {
    text-align: center;
}

.footer-copyright {
    margin-bottom: 5px;
}

/* Small viewports: stack credit line below copyright */
@media screen and (max-width: 480px) {
    .footer-copyright .credit-sep {
        display: none;
    }
}

.footer-data {
    color: var(--text-secondary, rgba(255, 255, 255, 0.8));
}

/* Large viewports: inline with middot separators */
@media screen and (min-width: 769px) {
    .footer-data .data-group {
        white-space: nowrap;
    }

    .footer-data .data-sep {
        color: var(--text-muted, rgba(255, 255, 255, 0.5));
    }
}

/* Small/Medium viewports: wrap naturally, hide main separator */
@media screen and (max-width: 768px) {
    .footer-data {
        line-height: 1.8;
    }

    .footer-data .data-sep {
        display: none;
    }

    .footer-data .data-group {
        display: inline;
    }

    /* Stack artist-data on its own line */
    .footer-data .data-group.artist-data {
        display: block;
        margin-top: 3px;
    }

    /* Break the artist-data links onto their own line after "via" */
    .footer-data .data-group.artist-data .data-links {
        display: block;
    }
}

/*  Image Sources Config
----------------------------------------------------- */
.image-sources-config {
    border: none;
    margin: 0;
    padding: 10px 0;
    font-size: 14px;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 15px;
}

.image-sources-config .config-label {
    font-weight: normal;
    padding: 0;
    margin: 0;
    color: var(--text-tertiary, rgba(255, 255, 255, 0.7));
}

.image-sources-config label {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
    color: var(--text-tertiary, rgba(255, 255, 255, 0.7));
    padding: 10px 14px;
    min-height: 44px;
    border-radius: 4px;
    border: 1px solid transparent;
    transition:
        color 0.2s ease-out,
        background-color 0.2s ease-out,
        border-color 0.2s ease-out;
}

.image-sources-config label:hover {
    color: var(--text-secondary, rgba(255, 255, 255, 0.85));
    background-color: var(--bg-subtle, rgba(255, 255, 255, 0.08));
}

.image-sources-config label:has(input:checked) {
    color: #fff;
    background-color: var(--bg-medium, rgba(255, 255, 255, 0.12));
    border-color: var(--border-subtle, rgba(255, 255, 255, 0.25));
}

/* Show focus outline on label when radio is keyboard-focused */
.image-sources-config label:has(input:focus-visible) {
    outline: 2px solid var(--text-tertiary, rgba(255, 255, 255, 0.7));
    outline-offset: 2px;
    border-radius: 2px;
}

/* Visually hide radio buttons but keep them accessible */
.image-sources-config input[type="radio"] {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Mobile: horizontal row with tighter spacing */
@media screen and (max-width: 480px) {
    .image-sources-config {
        gap: 8px;
        padding: 8px 0;
    }

    .image-sources-config .config-label {
        width: 100%;
        text-align: center;
        margin-bottom: 4px;
    }

    .image-sources-config label {
        padding: 6px 10px;
        font-size: 13px;
    }
}

/*  Artist Dialog
----------------------------------------------------- */
.artist-dialog {
    /* Per-artist accent (JS sets --artist-accent / --artist-accent-soft).
       Fall back to text/border tokens so the dialog still reads coherent
       before JS has run. */
    --artist-accent: var(--text-primary, #fff);
    --artist-accent-soft: var(--bg-medium, rgba(255, 255, 255, 0.15));
    /* Dialog surface: deeper step of the body background. JS overrides via
       --dialog-surface; the fallback is a neutral darken so the dialog never
       looks broken if JS misses. */
    --dialog-surface: rgba(0, 0, 0, 0.85);
    --dialog-radius: 16px;

    color: var(--text-primary, #fff);
    background: var(--dialog-surface);
    border: 1px solid var(--border-medium, rgba(255, 255, 255, 0.45));
    border-radius: var(--dialog-radius);
    padding: 0;
    max-width: 720px;
    width: calc(100% - 32px);
    max-height: calc(100vh - 64px);
    overflow: hidden;
    box-shadow:
        0 24px 60px rgba(0, 0, 0, 0.5),
        inset 0 1px 0 var(--border-specular, rgba(255, 255, 255, 0.1));
}

/* Native <dialog> defaults to display:block once opened; lock to centered. */
.artist-dialog[open] {
    display: flex;
    flex-direction: column;
    margin: auto;
}

/* The dialog itself takes initial focus (tabindex=-1) so :focus-visible
   doesn't paint a ring on the close button when the modal opens. Hide
   any focus outline on the dialog container — focusable interactive
   elements inside still keep their own focus styles. */
.artist-dialog:focus,
.artist-dialog:focus-visible {
    outline: none;
}

/* Lock background scroll while the dialog is open so wheel/trackpad doesn't
   propagate to the page behind. Native <dialog>.showModal() already traps
   focus and pointer to the dialog; this just covers macOS rubber-band that
   can still scroll body on some browsers. */
body.dialog-open {
    overflow: hidden;
}

/* Dim the artist tiles behind the dialog by darkening THEM in place —
   not via an overlay (which would carry its own bg color and break
   Safari's chrome-blends-with-body-bg trick). Scoped to .content only:
   header/footer/username/personality stay at their natural brightness so
   the page chrome around the dialog still reads cleanly. Same 0.7s
   ease-out as animateBackgroundColor so the tile dim syncs with the
   page-bg tween. */
body.dialog-open .content {
    filter: brightness(0.45) saturate(0.85);
}

.content {
    transition: filter 0.7s ease-out;
}

/* Switching artists in an open dialog (clicking an in-top similar item):
   fade content out, swap, fade in. The JS toggles the class for ~180ms,
   matching the fade-out half here. */
.artist-dialog.artist-dialog--swapping .dialog-body,
.artist-dialog.artist-dialog--swapping .dialog-hero {
    opacity: 0;
    transition: opacity 180ms ease-out;
}

.artist-dialog .dialog-body,
.artist-dialog .dialog-hero {
    transition: opacity 220ms ease-in;
}

/* Backdrop is just a blur — the page itself is animated to a deeper artist-
   tinted color while the dialog is open, which gives the dim effect (and
   feels intentional, not slapped on). The transparent fill lets the page
   color show through the blur. */
.artist-dialog::backdrop {
    background: transparent;
    -webkit-backdrop-filter: blur(14px) saturate(120%);
    backdrop-filter: blur(14px) saturate(120%);
    animation: dialog-backdrop-in 0.3s ease-out;
}

.artist-dialog[open] {
    animation: dialog-in 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}

@keyframes dialog-in {
    from {
        opacity: 0;
        transform: translateY(16px) scale(0.98);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

@keyframes dialog-backdrop-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

.dialog-surface {
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overflow-x: hidden;
    max-height: calc(100vh - 64px);
    position: relative;
    /* Contain rubber-band scroll so the dialog's edge bg color doesn't peek
       past the hero on macOS. */
    overscroll-behavior: contain;
    background-color: var(--dialog-surface);
}

/* Close button — mirrors .title's two-layer treatment: backdrop-filter
   (blur + saturate) on the button itself with no background, then a
   ::before pseudo holds a 15% black fill and white-rgba border. White
   X sits on top. The .light-image variant — toggled by syncHeroDots
   when the active hero slide reads as light — deepens the pseudo's
   black so the X stays readable against bright imagery, in the same
   spirit as .artist.light-image .title::before. */
.dialog-close {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 20;
    width: 38px;
    height: 38px;
    box-sizing: border-box;
    border: 0;
    background: none;

    -webkit-backdrop-filter: blur(10px) saturate(180%);
    backdrop-filter: blur(10px) saturate(180%);
    color: #fff;
    border-radius: 50%;
    cursor: pointer;
    padding: 0;
    overflow: hidden;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 0;
}

.dialog-close::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.2);
    border: 1px solid rgba(255, 255, 255, 0.6);
    border-radius: 50%;
    z-index: -1;
    transition:
        background-color 0.2s ease-out,
        border-color 0.2s ease-out;
}

.dialog-close.light-image::before {
    background: rgba(0, 0, 0, 0.4);
    border: 1px solid rgba(255, 255, 255, 0.6);
}

.dialog-close svg {
    display: block;
    width: 16px;
    height: 16px;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    fill: none;
    /* viewBox = render size (16) so the strokes pixel-align — no subpixel
       blur. shape-rendering: geometricPrecision keeps the diagonals
       smooth even on non-retina screens. */
    shape-rendering: geometricPrecision;
}

.dialog-close:hover::before {
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 255, 255, 0.7);
}

.dialog-close.light-image:hover::before {
    background: rgba(0, 0, 0, 0.5);
    border-color: rgba(255, 255, 255, 0.7);
}

/* Override the global button:focus-visible rule that flattens border-radius. */
.dialog-close:focus-visible {
    border-radius: 50%;
}

/* Hero — horizontal-scroll carousel adapted from payamyousefi.com's
   Carousel component pattern. One slide per available image source
   (iTunes / Discogs / AudioDB thumb / AudioDB fanart). Scroll-snap
   means swipe and dot-clicks both settle on a slide.
----------------------------------------------------- */
.dialog-hero {
    position: relative;
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    background: linear-gradient(135deg, var(--artist-accent-soft), rgba(0, 0, 0, 0.2));
    /* Pull-down framed-card reveal (mobile Safari overscroll). --hero-pull is
       0 at rest and ramps toward 1 as the user pulls the dialog past its top
       (set by the touchmove handler in initDialog). As it grows we scale the
       hero in a touch, push it down for a top margin, and round its corners so
       the image nests cleanly inside the dialog's radius — the revealed
       background frames it as a card instead of showing a flat edge.
       transform-origin top center keeps the top edge anchored while the
       translateY adds the top inset; the scale supplies the left/right (and
       bottom) margins. overflow:hidden clips the slides to the rounded box. */
    box-sizing: border-box;
    overflow: hidden;
    transform-origin: center top;
    transform:
        translateY(calc(10px * var(--hero-pull, 0)))
        scale(calc(1 - 0.05 * var(--hero-pull, 0)));
    /* Corners round and a hairline border fade in ONLY as the dialog is pulled
       down. At rest — and during a horizontal swipe, where the axis lock keeps
       --hero-pull at 0 — the hero is square and borderless (rounded only by the
       dialog itself). No bottom border so it still blends into the body. */
    /* Top corners snap to the dialog's own radius the moment a downward pull is
       detected (--hero-round flips to 1), so the rounding starts matched to the
       dialog instead of growing up from square with pull distance. A horizontal
       swipe keeps --hero-round at 0 → square corners. Bottom stays square so it
       blends into the body. */
    border-radius:
        calc(var(--dialog-radius, 16px) * var(--hero-round, 0))
        calc(var(--dialog-radius, 16px) * var(--hero-round, 0))
        0 0;
    transition:
        transform 0.12s ease-out,
        border-radius 0.12s ease-out;
}

.dialog-hero-viewport {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    /* Cap hero height by viewport so the rest of the dialog gets to fit
       without scrolling on most desktop sizes. min() picks the smaller of
       a viewport-proportional cap and an absolute cap — keeps the image
       sensible on ultra-tall windows. The aspect-ratio still applies when
       the dialog is narrow enough that 16:9 yields a shorter value. */
    max-height: min(38vh, 340px);
    display: flex;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    /* scroll-behavior: auto (not smooth) — when we reset scrollLeft = 0 on
       dialog open, a smooth scroll-behavior turns the reset into an
       animation that competes with append-during-paint (fanart slides) and
       gets visually "stuck between images". goToHeroSlide() opts into
       smooth scrolling explicitly via scrollTo({behavior: 'smooth'}) so
       dot clicks still glide nicely. */
    scroll-behavior: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    outline: none;
}

.dialog-hero-viewport::-webkit-scrollbar {
    display: none;
}

.dialog-hero-slide {
    flex: 0 0 100%;
    min-width: 0;
    height: 100%;
    scroll-snap-align: start;
    background-size: cover;
    /* Anchor to the top so portraits keep faces in frame and album covers
       keep their title/artist text visible. Center-center cropped out the
       most-important top region on tall images (especially album covers
       displayed in a 16:9 viewport, where the bottom half of the square
       cover gets shown). */
    background-position: top center;
    background-color: transparent;
    opacity: 0;
    transition: opacity 0.4s ease-out;
}

.dialog-hero-slide.loaded {
    opacity: 1;
}

/* Bottom gradient fade into the dialog surface. */
.dialog-hero::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to bottom, transparent 30%, var(--dialog-surface) 100%);
    pointer-events: none;
}

/* Dots row — lives inside the dialog body, between the header (artist
   name + meta) and the stats. Pulled up tight against the header so it
   visually belongs to the image/title block, not the stats below.
   Hidden when there's only one slide. */
.dialog-hero-dots {
    display: flex;
    justify-content: center;
    gap: 0;
    /* Negative top margin counters the dialog-body's `gap: 20px` so the dots
       sit close to the header. Bottom margin trims the gap before stats. */
    margin: -22px 0 0;
}

.dialog-hero-dots:empty,
.dialog-hero-dots:has(.dialog-hero-dot:only-child) {
    display: none;
}

.dialog-hero-dot {
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    outline: none;
}

.dialog-hero-dot::before {
    content: "";
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--text-tertiary, rgba(255, 255, 255, 0.7));
    opacity: 0.4;
    transition:
        opacity 0.2s ease,
        transform 0.2s ease,
        background-color 0.2s ease;
}

.dialog-hero-dot:hover::before {
    opacity: 0.7;
}

.dialog-hero-dot:focus-visible::before {
    box-shadow:
        0 0 0 2px var(--dialog-surface),
        0 0 0 4px var(--artist-accent);
}

.dialog-hero-dot[aria-current="true"]::before {
    opacity: 1;
    transform: scale(1.4);
    background: var(--text-primary, #fff);
}

/* Body
----------------------------------------------------- */
.dialog-body {
    padding: 0 28px 28px;
    text-align: left;
    display: flex;
    flex-direction: column;
    gap: 20px;
}

/* Pull the header up over the gradient fade. */
.dialog-header {
    margin-top: -48px;
    margin-bottom: 10px;
    position: relative;
    z-index: 1;
}

.dialog-name {
    font-family: "Lobster Two", "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 36px;
    line-height: 1.1;
    font-weight: 700;
    margin: 0;
    color: var(--text-primary, #fff);
    text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
    text-align: left;
}

/* No hover treatment — name is large and clickable; underline animation
   added visual noise without adding signal. Keyboard focus still shows
   via the global :focus-visible rule. */
.dialog-name-link {
    color: inherit;
    text-decoration: none;
}

.dialog-name-link:hover,
.dialog-name-link:focus {
    text-decoration: none;
}

.dialog-meta {
    margin: 6px 0 0;
    font-size: 14px;
    line-height: 20px;
    color: var(--text-secondary, rgba(255, 255, 255, 0.85));
    text-align: left;
}

.dialog-meta:empty {
    display: none;
}

.dialog-meta .meta-sep {
    margin: 0 8px;
    opacity: 0.5;
}

.dialog-personality-tie {
    margin: 0;
    font-size: 14px;
    line-height: 20px;
    font-style: italic;
    color: var(--text-tertiary, rgba(255, 255, 255, 0.7));
    padding: 10px 14px;
    border-left: 3px solid var(--artist-accent);
    background: var(--bg-subtle, rgba(255, 255, 255, 0.08));
    border-radius: 0 6px 6px 0;
    text-align: left;
}

.dialog-personality-tie:empty {
    display: none;
}

/* Stats
----------------------------------------------------- */
.dialog-stats {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 10px;
}

.dialog-stat {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: 12px 14px;
    background: var(--bg-subtle, rgba(255, 255, 255, 0.08));
    border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.25));
    border-radius: 8px;
    gap: 4px;
}

.dialog-stat-value {
    font-size: 22px;
    line-height: 26px;
    font-weight: 700;
    color: var(--text-primary, #fff);
    font-variant-numeric: tabular-nums;
}

.dialog-stat-label {
    font-size: 12px;
    line-height: 16px;
    color: var(--text-tertiary, rgba(255, 255, 255, 0.7));
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.dialog-stat-skeleton {
    background: var(--bg-medium, rgba(255, 255, 255, 0.15));
    border-radius: 4px;
    height: 22px;
    width: 60%;
    animation: pulse 1.5s ease-in-out infinite;
}

/* Chips (mood / style / genre) and similar list
----------------------------------------------------- */
.dialog-chips,
.dialog-similar-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.dialog-chip {
    display: inline-flex;
    align-items: center;
    padding: 6px 12px;
    min-height: 32px;
    font-size: 13px;
    line-height: 16px;
    font-weight: 700;
    color: var(--text-primary, #fff);
    background: var(--bg-medium, rgba(255, 255, 255, 0.15));
    border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.25));
    border-radius: 999px;
    white-space: nowrap;
}

.dialog-chip-mood {
    /* JS sets --chip-tint based on which mood it is, falls back to accent. */
    background: var(--chip-tint, var(--artist-accent-soft));
    border-color: var(--chip-tint-border, var(--border-medium, rgba(255, 255, 255, 0.45)));
}

.dialog-chip-style,
.dialog-chip-genre {
    background: var(--bg-subtle, rgba(255, 255, 255, 0.08));
}

/* Sections
----------------------------------------------------- */
.dialog-section {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* Author display:flex above wins over the UA stylesheet's [hidden]{display:none};
   make hidden explicit so the section truly disappears when empty. */
.dialog-section[hidden] {
    display: none;
}

.dialog-section h3 {
    font-family: "Karla", "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 12px;
    line-height: 16px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-tertiary, rgba(255, 255, 255, 0.7));
    margin: 0;
}

.dialog-section[aria-busy="true"] .dialog-section-skeleton {
    height: 14px;
    background: var(--bg-medium, rgba(255, 255, 255, 0.15));
    border-radius: 4px;
    animation: pulse 1.5s ease-in-out infinite;
}

.dialog-section[aria-busy="true"] .dialog-section-skeleton + .dialog-section-skeleton {
    margin-top: 8px;
}

.dialog-section[aria-busy="true"] .dialog-section-skeleton:nth-child(2) {
    width: 90%;
}

.dialog-section[aria-busy="true"] .dialog-section-skeleton:nth-child(3) {
    width: 60%;
}

/* Reveal sections as data arrives. Plain opacity fade — no transform — so
   late-arriving sections settle in without their own distinct motion,
   keeping the dialog's open animation the only thing that moves.
   The similar section additionally uses the grid-template-rows trick so its
   HEIGHT animates from 0 to content-height — eliminates the modal-height
   pop the user noticed. */
.dialog-section.revealed {
    animation: dialog-section-in 0.3s ease-out both;
}

@keyframes dialog-section-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

/* Similar-artists section uses grid-template-rows: 0fr → 1fr to animate
   its REAL height (not just opacity), so the modal grows smoothly rather
   than snapping. The inner wrap is the grid item being collapsed/expanded;
   overflow:hidden + min-height:0 lets it actually go to zero height. */
.dialog-similar {
    /* No [hidden] toggle on this section — the reveal-wrap handles show/hide. */
    gap: 0;
}

.dialog-similar .dialog-reveal-wrap {
    display: grid;
    grid-template-rows: 0fr;
    transition:
        grid-template-rows 0.45s ease-out,
        opacity 0.3s ease-out;
    /* `clip` + 1px ink-overflow margin so the chip border-radius's
       antialiased bottom curve isn't cropped at the wrap boundary.
       The wrap's grid-template-rows: 1fr resolves to a sub-pixel
       value driven by font line-heights, which puppeteer's screenshot
       rasterizer rounds harder than the live compositor — visible as
       a half-pill cut along the bottom of every similar-artist chip
       in desktop screenshots. */
    overflow: clip;
    overflow-clip-margin: 1px;
    opacity: 0;
}

/* Instant mode — used when re-opening an artist whose similar data is
   already cached. Suppresses the section's grid-row tween and fade-in
   so cached opens feel like the dialog is remembering its state rather
   than rebuilding it. The JS keeps this class on long enough that none
   of the suppressed animations re-fire when it's eventually removed. */
/* The !important here is defensive — the section animation rule and the
   wrap transition rule have the same specificity as their --instant
   overrides, so source order would normally suffice. But cached re-opens
   were occasionally showing one paint of animation before the override
   stuck, which !important eliminates. */
.dialog-similar--instant .dialog-reveal-wrap {
    transition: none !important;
}

.dialog-similar--instant.revealed {
    animation: none !important;
}

.dialog-similar.revealed .dialog-reveal-wrap {
    grid-template-rows: 1fr;
    opacity: 1;
}

.dialog-similar .dialog-reveal-inner {
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* No per-chip stagger — the wrap's grid-row + opacity transition carries the
   whole list in as a single group, matching the bio's plain fade so the
   dialog reads as one settled unit instead of pieces cascading in. */

/* Bio
----------------------------------------------------- */
.dialog-bio-content {
    font-size: 15px;
    line-height: 24px;
    color: var(--text-secondary, rgba(255, 255, 255, 0.85));
}

.dialog-bio-content p {
    margin: 0 0 12px;
    text-align: left;
    /* The global `p { font-size: 16px; line-height: 25px; color: var(--text-
       primary) }` rule has higher specificity than properties inherited from
       .dialog-bio-content, so a plain <p> here would silently pick up the
       wrong size, line-height, and color. Inherit from the container so
       bio rendering stays under .dialog-bio-content's control. */
    font-size: inherit;
    line-height: inherit;
    color: inherit;
}

.dialog-bio-content p:last-child {
    margin-bottom: 0;
}

/* Hard-truncate the bio to a few lines via line-clamp — no fade mask, no
   inline expand. The "Read more on Last.fm" link below it sends the
   reader to the canonical source for the rest. */
.dialog-bio-content.collapsed {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.dialog-bio-content.collapsed p:not(:first-child) {
    display: none;
}

.dialog-bio-toggle {
    align-self: flex-end;
    background: none;
    border: none;
    padding: 4px 0;
    margin: 0;
    color: var(--text-secondary, rgba(255, 255, 255, 0.85));
    font-weight: 700;
    font-size: 13px;
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 3px;
    min-height: 32px;
    transition: color 0.2s ease-out;
}

.dialog-bio-toggle:hover {
    color: var(--text-primary, #fff);
}

/* Similar artists — uniform pill height whether link or in-top button.
----------------------------------------------------- */
.dialog-similar-item,
.dialog-similar-item:hover,
.dialog-similar-item:focus,
.dialog-similar-item:focus-visible {
    text-decoration: none;
}

.dialog-similar-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    box-sizing: border-box;
    height: 32px;
    padding: 0 12px;
    color: var(--text-primary, #fff);
    background: var(--bg-subtle, rgba(255, 255, 255, 0.08));
    border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.25));
    border-radius: 999px;
    font:
        700 13px/16px "Karla",
        "Helvetica Neue",
        Helvetica,
        Arial,
        sans-serif;
    cursor: pointer;
    transition:
        border-color 0.2s ease-out,
        background-color 0.2s ease-out;
}

/* Button reset for in-top items (rendered as <button>). */
button.dialog-similar-item {
    appearance: none;
    -webkit-appearance: none;
}

.dialog-similar-item:hover {
    border-color: var(--border-strong, rgba(255, 255, 255, 0.65));
    background: var(--bg-medium, rgba(255, 255, 255, 0.15));
}

.dialog-similar-item.in-top {
    border-color: var(--artist-accent);
    background: var(--artist-accent-soft);
}

.dialog-similar-item.in-top:hover {
    background: var(--artist-accent);
    color: hsl(0, 0%, 5%);
}

.dialog-similar-name {
    font-weight: 700;
}

.dialog-similar-badge {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--text-tertiary, rgba(255, 255, 255, 0.7));
    margin-left: 2px;
}

.dialog-similar-item.in-top:hover .dialog-similar-badge {
    color: hsl(0, 0%, 5%);
    opacity: 0.7;
}

/* Mobile: keep the designed dialog frame visible (border, radius — no shadow)
   with a small gutter on all sides so the backdrop shows around it. This matters
   on Safari iOS in particular: the browser chrome (status bar, home indicator)
   tints itself from whatever is rendered at the viewport edges, and a full-bleed
   dialog would erase the framing — then rubber-band scroll reveals the modal
   surface as a hard line against the chrome. A single opaque backdrop (tinted by
   JS to the dark page-bg variant) covers the whole page behind the card, so the
   gutter and the chrome read as one clean, uniform color with no contrasting
   line. A 12px gutter reads as a card without giving up usable area.
----------------------------------------------------- */
@media (max-width: 480px) {
    /* Mobile: the dialog floats as a card with a small gutter on all sides
       (like desktop, just tighter) instead of a full-bleed sheet. The opaque
       tinted backdrop (below) fills the gutter and covers the page; rounded
       corners keep the hero image from butting a hard line against the status
       bar. */
    .artist-dialog,
    .artist-dialog[open] {
        inset: 0;
        margin: auto;
        max-width: none;
        width: calc(100% - 24px); /* 12px gutter each side */
        height: auto;
        /* Size to svh (small viewport height = toolbar SHOWING), not dvh/lvh.
           iOS Safari opens the dialog when you've scrolled the page and the
           bottom toolbar is hidden, so dvh resolves to the LARGE viewport;
           scrolling inside the dialog then brings the toolbar back, shrinks the
           viewport, and the card's bottom ends up behind it. svh caps the card
           at the smallest (toolbar-visible) height so it always fits regardless
           of toolbar state. The bottom gutter also clears the home indicator
           via safe-area-inset. vh is the fallback for browsers without svh. */
        max-height: calc(100vh - 24px);
        max-height: calc(100svh - 24px - env(safe-area-inset-bottom, 0px));
        border: 1px solid var(--border-medium, rgba(255, 255, 255, 0.16));
        border-radius: var(--dialog-radius);
        box-shadow: none;
    }

    .dialog-surface {
        max-height: calc(100vh - 24px);
        max-height: calc(100svh - 24px - env(safe-area-inset-bottom, 0px));
    }

    /* One opaque backdrop covers the whole page behind the card — uniformly,
       no per-element hiding/dimming (which left header/footer visible while
       #wrap disappeared). JS tints it to the dark page-bg variant so the gutter
       around the card and the browser chrome (which samples this at the viewport
       edges) read as one cohesive color. No blur/saturate: a blurred page reads
       nice but leaves a contrasting line at the chrome, so keep it clean. The
       cover also makes the page behind invisible, so the iOS body-scroll "double
       scroll" can't be seen — no scroll-pinning needed. */
    .artist-dialog::backdrop {
        background: var(--dialog-backdrop, #000);
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
    }

    /* Drop the desktop hero cap on mobile — at this width the aspect-ratio
       already gives a sensible height, and the cap was tuned for desktop. */
    .dialog-hero-viewport {
        max-height: none;
    }

    /* Title kept at the same 36px as desktop so its proportion to the
       body text below (stats 20px, meta 14px, body 15px) matches what
       desktop established. A smaller mobile title broke the hierarchy —
       script display faces (Lobster Two) need this absolute size to read
       as the page's primary heading rather than secondary text. */
    .dialog-name {
        font-size: 36px;
        line-height: 1.05;
    }

    .dialog-meta {
        font-size: 14px;
        line-height: 20px;
    }

    .dialog-body {
        padding: 0 18px 18px;
        gap: 16px;
    }

    .dialog-header {
        margin-top: -44px;
    }

    /* Dots had `margin: -22px 0 0` for desktop — on mobile that pulled
       them right under the meta with no breathing room. Reset to natural
       flow so the dialog-body's gap (16px) gives them air on both sides. */
    .dialog-hero-dots {
        margin: -14px 0 0;
    }

    /* Stat cards on mobile: scale the value DOWN (22px felt heavy in a
       2-col grid), trim padding, and slightly reduce the value→label gap
       so the cards feel compact and balanced rather than bulky. */
    .dialog-stats {
        gap: 8px;
    }

    .dialog-stat {
        padding: 10px 12px;
        gap: 2px;
    }

    .dialog-stat-value {
        font-size: 20px;
        line-height: 24px;
    }

    .dialog-stat-label {
        font-size: 11px;
        letter-spacing: 0.03em;
    }

    /* Section headings (ABOUT / SIMILAR TO) were 12px which made them
       almost-invisible against the body. 13px gives them a hint more
       presence as section labels without becoming shouty. */
    .dialog-section h3 {
        font-size: 13px;
    }
}

/* Reduced motion: kill the slide/scale/crossfade entrance and section reveals.
   The global rule already neuters transitions; explicitly drop animations too. */
@media (prefers-reduced-motion: reduce) {
    .artist-dialog[open],
    .artist-dialog::backdrop,
    .dialog-section.revealed,
    .dialog-hero-slide {
        animation: none !important;
        transition: none !important;
    }

    .dialog-hero-slide {
        opacity: 1;
    }

    .dialog-hero-viewport {
        scroll-behavior: auto;
    }
}
