/* ============================================
   CITIES - All city-related UI styles

   Sections:
     1. CITIES LIST   - .city_cards grid + skeletons (staggered brick layout)
     2. CITY CARD     - .city_card item rendered inside the list
     3. CITY PANEL    - .city-page right-hand full-city view
   ============================================ */


/* ============================================
   1. CITIES LIST
   ============================================ */

/* Staggered brick layout - columns set via --cols CSS variable */
.city_cards {
    display: grid;
    grid-template-columns: repeat(var(--cols, 1), 1fr);
    gap: 20px;
    padding: 16px var(--pad-x-with-scrollbar) 16px var(--pad-x);
    justify-items: center;
    /* overflow: hidden; */
}

/* Skeleton placeholders */
.skeleton {
    width: 100%;
    max-width: 350px;
    aspect-ratio: 1;
    padding-bottom: 43px;
    border-radius: 10px;
    background-color: #f8f9fa;
}

/* Stagger pattern: columns offset vertically for a brick effect */
/* Pattern: col1 = -100%, col2 = -50%, col3 = -75%, repeating */

.city_cards.cols-2 > :nth-child(2n+1) { transform: translateY(-100%); }
.city_cards.cols-2 > :nth-child(2n+2) { transform: translateY(-50%); }

.city_cards.cols-3 > :nth-child(3n+1) { transform: translateY(-100%); }
.city_cards.cols-3 > :nth-child(3n+2) { transform: translateY(-50%); }
.city_cards.cols-3 > :nth-child(3n+3) { transform: translateY(-75%); }

.city_cards.cols-4 > :nth-child(4n+1) { transform: translateY(-100%); }
.city_cards.cols-4 > :nth-child(4n+2) { transform: translateY(-50%); }
.city_cards.cols-4 > :nth-child(4n+3) { transform: translateY(-75%); }
.city_cards.cols-4 > :nth-child(4n+4) { transform: translateY(-103%); }

.city_cards.cols-5 > :nth-child(5n+1) { transform: translateY(-100%); }
.city_cards.cols-5 > :nth-child(5n+2) { transform: translateY(-50%); }
.city_cards.cols-5 > :nth-child(5n+3) { transform: translateY(-75%); }
.city_cards.cols-5 > :nth-child(5n+4) { transform: translateY(-103%); }
.city_cards.cols-5 > :nth-child(5n+5) { transform: translateY(-35%); }

/* Keeping the brick transforms on mobile by themselves would let the
   rising first row paint over the sets widget / filter title, because on
   mobile .content has overflow: visible (page-scroll). The desktop layout
   doesn't have this problem - .content has overflow: hidden and clips the
   rise at its top edge. Replicate the same one-sided clipping with
   clip-path: the inset clips at the top/sides, the large negative bottom
   value lets cards keep flowing into the page below. */


/* ============================================
   2. CITY CARD
   ============================================ */

.city_card{
    position: relative;
    border-radius: 10px;
    overflow: hidden;
    isolation: isolate;
    font-size: 0;
    transition: .3s;
    line-height: 1;
    width: 100%;
    max-width: 350px;
    cursor: pointer;
    aspect-ratio: 1;
    padding-bottom: 38px;

    &.selected{
        box-shadow: 0 0 12px 6px #906ec3;
        filter: saturate(1);
        opacity: 1;

        .footer{
            .title{
                opacity: 1;
            }
        }
    }

    /* &:hover {
        filter: saturate(1);
        opacity: 1;
    } */

    .image{
        width: 100.1%;
        height: 100%;
        object-fit: cover;
        display: block;
        box-shadow: none;
    }

    .flipped_image{
        width: 100%;
        display: block;
        transform: scaleY(-1);
    }

    .footer{
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        height: 38px;
        overflow: hidden;
        background-color: #393939; /* safety net: blends any sub-pixel gap around the flipped image */
    }

    /* <picture> defaults to display:inline; in Safari the inline baseline can
       leave a sub-pixel gap above the block <img> inside, leaking the page
       background through the translucent .info backdrop-filter. */
    .footer picture{
        display: block;
    }

    .info{
        backdrop-filter: blur(12px);
        background-color: #0000003d;

        position: absolute;
        bottom:-1px;
        left: 0;
        right: 0;
        height: 39px;

        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        align-items: center;
        padding: 0 10px 1px;
    }

    .title{
        font-size: 1rem;
        font-weight: bold;
        color: #ffffff;
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
        line-height: 38px;
        padding-left: 10px;
        opacity: 0.5;
        min-width: 0;
    }

    .country{
        width: 22px;
        height: 22px;
        border-radius: 5px;
        object-fit: cover;
        flex-shrink: 0;
    }

    .country.wand{
        color: white;
        opacity: 0.5;
    }

    /* Tag - text variant. All tags are hollow; the optional color modifier
       picks the border + text color. Opacity is fixed so the tag doesn't
       shift on card hover. */
    .tag{
        margin-left: 10px;
        font-size: 0.6875rem;
        font-weight: 600;
        border-radius: 7px;
        white-space: nowrap;
        line-height: 1.4;

        &.hollow{
            background: transparent;
            color: #ffffff94;
            border: 1px solid currentColor;
            padding: 1px 7px 2px;

            text-shadow: 0 0 2px #00000069;
            box-shadow: 0 0 6px #00000029;
            background: #0000001f;

            &.blue{
                color: #81b7e1;
                
            }

            &.light{
                color: #a0a0a0;
            }
        }
    }

    /* Icon row - replaces the text tag for set_track / features behaviors.
       All icons render at one fixed size; level is conveyed in the panel
       click-through. */
    .tag-icons{
        margin-left: 10px;
        display: flex;
        align-items: center;
        gap: 4px;

        .feature-icon{
            flex-shrink: 0;
            object-fit: contain;
            width: 22px;
            height: 22px;
        }
    }

    /* Info-panel tint variants. Purple = "ours and noteworthy" (in a set,
       or just owned, depending on behavior). */
    &.tint-purple .info{
        background-color: #72257f80;
    }

    /* Washed-out treatment - used in set_track for non-yours cities and in
       browsing for non-ERC721 cities. Filter is scoped to the artwork only
       so the info-panel text, the tag, and the card's box-shadow stay crisp.
       Hover restores full colour. */
    &.washed-out{
        .image{
            filter: grayscale(0.75) brightness(1.05) opacity(0.75);
        }

        /* &:hover .image, &:hover .flipped_image{
            filter: none;
        } */
    }

    .badge{
        position: absolute;
        bottom: 10px;
        right: 9px;
        padding: 3px 5px 5px;
        border-radius: 6px;
        font-size: 0.75rem;
        font-weight: bold;
        transition: .3s;
        color: #fff;
        border: 2px solid transparent;
    }
}


/* ============================================
   3. CITY PANEL - full-screen city view
   ============================================ */

/* Single-column centered layout with scrollable content */
.city-page {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 100vh;
    background: #f9f9f9;
    overflow: hidden;
    container: city-page / size;
}

/* On mobile the dashboard collapses to a single panel, so the grey panel
   background reads as a stray band against the white page above and below.
   Match the page colour instead. */
@media (max-width: 900px) {
    .city-page {
        background: #fff;
    }
}

/* Hide body scrollbar when city page is active */
body:has(.city-page) {
    overflow: hidden;
}

/* Fixed header at top */
.city-page .top-bar {
    flex-shrink: 0;
    padding: 10px var(--pad-x);
}

/* Scrollable content area. Scrollbar styling comes from .thin-scroll
   (applied in HTML). Right padding subtracts scrollbar width so content
   aligns with the top-bar header above. */
.city-page .content {
    flex: 1;
    overflow-y: scroll;
    overflow-x: hidden;
    flex-direction: column;
    align-items: center;
    padding: 0 var(--pad-x-with-scrollbar) 0 var(--pad-x);
}

.city-page-header {
    display: flex;
    align-items: baseline;
    gap: 16px;

    .city-page-name {
        font-size: 2.7em;
        font-weight: 700;
        color: #000;
        margin: 0;
        padding: 0;
        flex: 0 1 auto;
        min-width: 0;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        line-height: 1.4em;
    }

    /* overflow: hidden makes the chip's baseline fall at its bottom margin edge,
       so align-items: baseline on the header puts the blockie's bottom on the
       title's text baseline. Internally the chip still centres its content. */
    .city-page-owner {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        font-size: 0.875rem;
        color: #555;
        cursor: pointer;
        flex-shrink: 0;
        overflow: hidden;

        .blockie {
            width: 22px;
            height: 22px;
            border-radius: 5px;
            overflow: hidden;
            flex-shrink: 0;
            filter: grayscale(0.8);
        }

        &:hover {
            color: #111;
        }
    }

    .city-page-action {
        margin-left: auto;
        align-self: center;
    }
}

@container city-page (max-aspect-ratio: 8/10) {
    /* Narrow / mobile - give the title, action button, and image more
       breathing room. The header wraps to two rows; row-gap puts space
       between title row and owner+action row. */
    .city-page .top-bar {
        padding-top: 18px;
        padding-bottom: 12px;
    }

    .city-page-header {
        flex-wrap: wrap;
        align-items: end;
        row-gap: 10px;

        /* flex: 0 0 100% forces the title to a full-width row and shuts down
           shrink, so the owner can't squeeze onto the title's line even at
           short names. flex-basis: 100% alone wasn't enough because the
           desktop rule's `flex: 0 1 auto` leaves flex-shrink at 1. */
        .city-page-name {
            flex: 0 0 100%;
            font-size: 2em;
            line-height: 1.4;
        }

        .city-page-action {
            align-self: end;
            padding: 7px 14px;
            font-size: 0.8125rem;
            margin-top: 0;

            svg {
                width: 14px;
                height: 14px;
            }
        }
        .city-page-owner{
            margin-bottom: 5px;
        }
    }

    .city-page .city-page-image {
        margin-bottom: 18px;
    }

    .city-page .city-page-tags {
        margin-bottom: 24px;
        gap: 10px;
    }
}

.city-page-image {
    position: relative;
    width: 100%;
    max-width: 2048px;
    aspect-ratio: 1;
    margin-bottom: 10px;
    border-radius: 12px;
    overflow: hidden;

    > img,
    > picture > img {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        object-fit: cover;
        object-position: center;
    }

    > img:first-of-type,
    > picture:first-of-type > img {
        z-index: 1;
    }

    > img:last-of-type,
    > picture:last-of-type > img {
        z-index: 2;
    }
}

/* ─── World-map overlay ──────────────────────────────────────
   On hover (or click-lock) of the coordinates tag, a blurred
   backdrop fills the entire city image, with a Mercator world
   map centred inside it. The map's bottom is cropped so
   Antarctica's distorted strip doesn't show.

   Layout:
     .city-map-overlay  - fills the city image; fade target
       .world-map       - full-size dark blurred backdrop
         .world-map-frame   - the visible window onto the map;
                              centred vertically, has the cropped
                              aspect ratio, overflow: hidden
           .world-map-canvas - full natural aspect of the SVG;
                               taller than the frame, so its bottom
                               (Antarctica) is clipped. Owns the
                               image and the dot - dot %s are
                               relative to the full SVG, not the
                               cropped frame. */

.city-page-image .city-map-overlay {
    position: absolute;
    inset: 0;
    z-index: 3;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
}

.city-page.map-hover .city-map-overlay,
.city-page.map-locked .city-map-overlay {
    opacity: 1;
    visibility: visible;
}

.world-map {
    position: absolute;
    width: 35%;
    height: 25%;
    background: rgb(0 0 0 / 10%);
    backdrop-filter: blur(30px);
    -webkit-backdrop-filter: blur(30px);
    border-radius: 10px;
    overflow: hidden;
    border: 1px solid #fff;
    right: 2%;
    bottom: 2%;
}

/* Cropped window - keeps the top 88% of the SVG (Antarctica gone).
   Fades in slightly behind the blur so the backdrop-filter has time
   to render before the map content lands on top of it. On hide, the
   map fades out first (no delay) so it doesn't linger. */
.world-map-frame {
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    aspect-ratio: 1 / 1;
    overflow: hidden;
    opacity: 0;

}

.city-page.map-hover .world-map-frame,
.city-page.map-locked .world-map-frame {
    opacity: 1;
    /* transition: opacity 0.18s ease 0.12s; */
}

/* Holds the SVG at its natural aspect ratio so dot %s line up. */
.world-map-canvas {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    aspect-ratio: 1 / 1;
}

.city-page-image .world-map .world-map-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    z-index: auto;
    opacity: 0.65;
    pointer-events: none;
}

/* The scale(1/--zoom) counter-scales the marker so it stays the
   same visual size when the canvas is zoomed in. */
.world-map-dot {
    position: absolute;
    width: 14px;
    height: 14px;
    background: #b35eec;
    border: 2px solid #fff;
    border-radius: 50%;
    box-sizing: border-box;
    transform: translate(-50%, -50%) scale(calc(1 / var(--zoom, 1)));
    pointer-events: none;
    z-index: 2;
}

/* The locked-state coords tag picks up the same blue tint as the rarity tag so the
   two "sticky" toggles read as a consistent pair. */
.city-page.map-locked .city-page-tag.coords::after {
    background: rgba(33, 150, 243, 0.85);
}

.city-page.map-locked .city-page-tag.coords:hover::after {
    background: rgba(33, 150, 243, 0.95);
}

/* ─── Stats-mode tags ─────────────────────────────────────────────────────────────────────────────────
   Two views:
     - name   - feature title + level ("Religion 5") / country title / "City Size"
     - stats  - count + bar fill ("14 cities" + blue bar)

   Persistent mode is .stats-mode (toggled by clicking the rarity tag). Hovering a single tag
   (.tag-preview) previews its alternate. The rarity tag itself does NOT preview on hover - it's a hard
   toggle on click. Truth table is plain XOR:

     stats  preview  →  view
       F      F      →  name
       F      T      →  stats
       T      F      →  stats
       T      T      →  name
*/

/* bar visible when stats XOR preview is true */
.city-page.stats-mode .city-page-tag:not(.tag-preview) .bar,
.city-page:not(.stats-mode) .city-page-tag.tag-preview .bar {
    width: var(--bar-pct, 0%);
    opacity: 1;
}

/* text-normal hidden when stats XOR preview is true */
.city-page.stats-mode .city-page-tag:not(.tag-preview) .text-normal,
.city-page:not(.stats-mode) .city-page-tag.tag-preview .text-normal {
    visibility: hidden;
}

/* text-stats visible when stats XOR preview is true */
.city-page.stats-mode .city-page-tag:not(.tag-preview) .text-stats,
.city-page:not(.stats-mode) .city-page-tag.tag-preview .text-stats {
    visibility: visible;
}

/* In stats mode, lock stat-bearing tags to a fixed width so the bar fills are directly comparable in
   absolute pixels rather than as a fraction of varying tag widths. Doesn't apply outside stats mode -
   default name view sizes naturally to the title. */
.city-page.stats-mode .city-page-tag.feature-tag,
.city-page.stats-mode .city-page-tag.size-tag,
.city-page.stats-mode .city-page-tag.country-tag {
    width: 90px;
}

/* Outside stats mode, size and country tags wear the lighter ::after tint (same as the coords pill)
   so they read as "trait info" alongside coords rather than competing with feature tags. In stats
   mode they revert to the default dark tint so the blue rarity bar reads cleanly. */
.city-page:not(.stats-mode) .city-page-tag.size-tag::after,
.city-page:not(.stats-mode) .city-page-tag.country-tag::after {
    background: rgba(255, 255, 255, 0.3);
}

/* When previewing a feature tag inside stats mode (the only case where ellipsis kicks in), tighten the
   gap between the name and the level so the level reads as part of the same token (e.g. "Empl.. 4"
   rather than "Empl..   4"). */
.city-page.stats-mode .city-page-tag.tag-preview.feature-tag .text-normal {
    gap: 2px;
}

/* Coords aren't part of the score - fade when other tags are showing stats. */
.city-page.stats-mode .city-page-tag.muted {
    opacity: 0.35;
}

/* The blue rarity tag is the trigger itself - leave it untouched (no bar, no fade, no text swap). */
.city-page-tag.blue .bar { display: none; }

/* "(?)" help dot sitting at the end of the city-panel tag row. Aligns
   vertically with the 22px-tall tags and points at the meta info page so
   readers can dig into what every tag actually represents. */
.city-page-help {
    align-self: center;
    margin-left: 4px;
}

/* ID/share tag - clicking copies the canonical /city/{id} URL to clipboard;
   the label inside the tag flips from the city id to "Copied" briefly, then
   back. Both labels share one grid cell so the tag width is locked to the
   wider of the two and never resizes during the swap. */
.city-page-tag.id-tag .text-swap {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: center;
}
.city-page-tag.id-tag .text-swap > span {
    grid-area: 1 / 1;
    transition: opacity 0.18s ease;
}
.city-page-tag.id-tag .label-copied {
    opacity: 0;
    color: #8be9a4;
    font-size: 0.625rem;
    margin-right: -3px;
    margin-left: -2px;
}
.city-page-tag.id-tag.copied .label-id {
    opacity: 0;
}
.city-page-tag.id-tag.copied .label-copied {
    opacity: 1;
}

/* "Rarity can change · More info" link - sits next to the tags but only in stats mode. Same visual weight
   as the tag labels, blue + underlined to read as a link. */
.rarity-info-link {
    display: none;
    align-self: center;
    margin-left: 15px;
    font-size: 0.75rem;
    font-weight: 600;
    color: #c2c2c2;
    text-decoration: underline;
    cursor: pointer;
    white-space: nowrap;
}
.city-page.stats-mode .rarity-info-link {
    display: inline-block;
}
.rarity-info-link:hover {
    color: #96a0be;
}

.city-page-tags {
    width: 100%;
    max-width: 1024px;
    margin-bottom: 20px;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;

    .city-page-tag {
        position: relative;
        display: inline-flex;
        align-items: center;
        gap: 8px;
        height: 22px;
        padding: 4px 12px 4px 4px;
        border-radius: 999px;
        color: #fff;
        font-size: 0.8125rem;
        font-weight: 600;
        line-height: 1.3em;
        white-space: nowrap;
        cursor: pointer;
        overflow: hidden;
        /* iOS Safari fails to clip the blurred ::before to the rounded corners with overflow:hidden alone
           (filtered children get their own compositing layer that ignores the parent's border-radius),
           leaving a rectangular silhouette around each pill on iPhone. clip-path forces the rounding. */
        clip-path: inset(0 round 999px);
        isolation: isolate;
        transition: opacity 0.25s ease;

        /* City image rendered at natural size, offset per-tag,
           then blurred to pick up the ambient color. background-color is a
           neutral slate fallback for the rare case where the AVIF preview is
           missing - the tag still reads as a solid pill rather than ghosting. */
        &::before {
            content: '';
            position: absolute;
            inset: -20px;
            background-color: #3a3f4d;
            background-image: var(--tag-bg);
            background-repeat: no-repeat;
            background-size: auto;
            background-position: calc((var(--tag-idx, 0) + 1) * 9%) bottom;
            filter: blur(30px);
            z-index: -3;
        }

        /* Dark tint so the white text stays legible. */
        &::after {
            content: '';
            position: absolute;
            inset: 0;
            background: rgba(0, 0, 0, 0.15);
            transition: background 0.2s ease;
            z-index: -2;
        }

        /* Rarity progress bar - sits between the dark tint and the text content. Width is set per-tag from the
           city model (--bar-pct = bits / FeatureStat.MaxBits * 100). Hidden until the page enters stats mode. */
        .bar {
            position: absolute;
            inset: 0;
            width: 0%;
            background: rgba(33, 150, 243, 0.85);
            opacity: 0;
            transition: width 0.45s cubic-bezier(0.22, 0.61, 0.36, 1), opacity 0.25s ease;
            pointer-events: none;
            z-index: -1;
        }

        /* Both labels stack in the same grid cell so the tag's width is sized to the wider of the two - the
           tag never shrinks/grows when toggling modes. The inactive label is hidden via `visibility` (not
           `display: none`), which keeps it contributing to the cell's intrinsic size.

           grid-template-columns: minmax(0, 1fr) lets the cell shrink below content size when the tag is
           width-locked in stats mode, so the .name span inside .text-normal can ellipsise instead of
           overflowing. flex:1 + min-width:0 makes the text-stack claim only the remaining tag width
           (after the icon) rather than its content width. */
        .text-stack {
            display: grid;
            grid-template-columns: minmax(0, 1fr);
            align-items: center;
            flex: 1;
            min-width: 0;
        }
        .text-stack > .text-normal,
        .text-stack > .text-stats {
            grid-area: 1 / 1;
            display: inline-flex;
            align-items: center;
            gap: 5px;
            max-width: 100%;
            min-width: 0;
        }

        /* The leading .name span shrinks first and ellipsises; the trailing .size span (feature level
           number) stays visible. So a width-locked feature tag clipping "Employment 4" reads "Empl.. 4"
           instead of "Employmen". The string form ('..') is only supported in newer browsers (Chrome 121+,
           recent Firefox); older ones fall back to the standard "…" via the prior declaration. */
        .text-normal > .name,
        .text-stats > .name {
            min-width: 0;
            overflow: hidden;
            text-overflow: ellipsis;
            text-overflow: '..';
            white-space: nowrap;
        }
        .text-normal > .size {
            flex-shrink: 0;
        }

        /* Default: name visible, stats hidden. The XOR rules below flip this when needed. */
        .text-stats { visibility: hidden; }

        &:hover::after {
            background: rgba(0, 0, 0, 0.6);
        }

        &.blue {
            /* Same blue (and same opacity) as the stats-mode progress bar - keeps the rarity tag visually
               consistent with the bars it triggers. */
            &::after {
                background: rgba(33, 150, 243, 0.85);
            }

            &:hover::after {
                background: rgba(33, 150, 243, 0.95);
            }
        }

        &.muted::after {
            background: rgba(255, 255, 255, 0.3);
        }

        &.muted:hover::after {
            background: rgba(0, 0, 0, 0.4);
        }

        .icon {
            width: 22px;
            height: 22px;
            border-radius: 50%;
            object-fit: cover;
            flex-shrink: 0;
        }

        /* Inline SVG icon shown inside a translucent dark disc - used by the
           rarity, coords, and id-tag pills. Slightly darker than the tag's
           ambient tint so the white SVG glyph reads cleanly against it. */
        .icon-circle {
            background: rgba(0, 0, 0, 0.15);
            color: #fff;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            overflow: hidden;
        }
        .icon-circle > svg {
            width: 65%;
            height: 65%;
            display: block;
        }

        /* Size tag's "icon" is the size number in a purple round badge - visually mirrors the feature
           thumbnails / country flags so the size tag sits naturally alongside them. */
        .size-icon {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            background: rgba(155, 89, 182, 0.85);
            color: #fff;
            font-size: 0.6875rem;
            font-weight: 700;
            line-height: 1;
            font-variant-numeric: tabular-nums;
        }

        .size {
            /* opacity: 0.75; */
        }
    }
}

/* ─── Action button (inline in title row) ─────────────────── */
/* Pill button on the same line as the title. Auto-width; title takes
   the rest and ellipsises when long. Exactly one action shown at a time. */

.city-page-action {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 10px 18px;
    border-radius: 999px;
    border: 1px solid rgba(0, 0, 0, 0.12);
    background: #fff;
    cursor: pointer;

    color: #111;
    font-size: 0.875rem;
    font-weight: 600;
    white-space: nowrap;
    margin-right: 10px;
    margin-top: 8px;

    transition: background 0.2s ease, border-color 0.2s ease, transform 0.15s ease;

    &:hover {
        background: #f5f5f5;
        border-color: rgba(0, 0, 0, 0.2);
    }

    &:active {
        transform: scale(0.98);
    }

    svg {
        width: 16px;
        height: 16px;
        flex-shrink: 0;
        opacity: 0.7;
    }
}

/* Disabled state - historic (pre-NFT) tokens that can't be bought yet. */
.city-page-action.is-disabled {
    color: #888;
    background: #f2f2f2;
    border-color: rgba(0, 0, 0, 0.08);
    cursor: not-allowed;

    svg {
        opacity: 0.5;
    }

    &:hover {
        background: #f2f2f2;
        border-color: rgba(0, 0, 0, 0.08);
    }

    &:active {
        transform: none;
    }
}

/* Promoted state - upgrade, add-to-set, buy. Pulsing blue halo to draw the eye.
   Remove-from-set stays plain so it doesn't compete for attention. */
.city-page-action.is-promoted {
    border-color: rgba(0, 123, 255, 0.45);
    color: #0b5fbf;
    animation: city-page-action-pulse 2s ease-out infinite;

    svg {
        opacity: 0.9;
        color: #007bff;
    }
}

@keyframes city-page-action-pulse {
    0%   { box-shadow: 0 0 0 0   rgba(0, 123, 255, 0.55); }
    70%  { box-shadow: 0 0 0 12px rgba(0, 123, 255, 0); }
    100% { box-shadow: 0 0 0 0   rgba(0, 123, 255, 0); }
}


/* ─── Mobile: collapse internal scroll into the page ───────────
   The city panel switches from a fixed-height flex column with an
   internally-scrolling content area to a plain block that flows in
   the page. container-type: normal is critical - size containment
   makes the element ignore its children's heights, so without this
   override .city-page would collapse to 0 even with height: auto.

   The @container city-page (max-aspect-ratio: 8/10) rules above
   would normally do double duty for narrow desktop panels AND
   mobile, but dropping size containment kills the container query.
   The mobile-only versions of those rules are re-stated inside this
   media block so the narrow-layout tweaks still apply on phones.

   This block lives at the END of the file on purpose: it has to
   override the desktop .city-page-header nested rules earlier in
   the file, and at equal specificity source order is what wins. */
@media (max-width: 900px) {
    body:has(.city-page) {
        overflow: visible;
    }

    .city-page {
        display: block;
        height: auto;
        overflow: visible;
        container-type: normal;
    }

    .city-page .content {
        display: block;
        overflow: visible;
        padding: 0 var(--pad-x);
    }

    .city-page .top-bar {
        padding-top: 18px;
        padding-bottom: 12px;
    }

    .city-page-header {
        flex-wrap: wrap;
        align-items: end;
        row-gap: 10px;

        /* Title gets a forced full-width row even for short names. flex: 0 0 100%
           shuts down both grow and shrink so the owner badge can't squeeze onto
           the title's line. With flex-wrap: wrap on the header that pushes the
           owner + action button onto row 2 every time. */
        .city-page-name {
            flex: 0 0 100%;
            font-size: 2em;
            line-height: 1.4;
        }

        .city-page-action {
            align-self: end;
            padding: 7px 14px;
            font-size: 0.8125rem;
            margin-top: 0;

            svg {
                width: 14px;
                height: 14px;
            }
        }

        .city-page-owner {
            margin-bottom: 5px;
        }
    }

    .city-page .city-page-image {
        margin-bottom: 18px;
    }

    .city-page .city-page-tags {
        margin-bottom: 24px;
        gap: 10px;
    }
}
