:root {
    --accent: #6c63ff;
    --accent-dark: #5548e0;
    --accent-glow: rgba(108, 99, 255, 0.35);
    /* The primary button's gradient starts a shade deeper than --accent.
       White on #6c63ff is 4.32:1 — just under the 4.5 floor — and every
       filled button in the app carries white text. --accent itself is left
       alone because borders and focus rings are graphics, not text, and
       darkening it would shift those too. */
    --btn-from: #5f56f5;
    --bg: #0e0f14;
    --card: #181a23;
    --card-alt: #1e2030;
    --well: #0f1117;
    --border: #262838;
    --premium: #ff9d2e;
    --text: #eee;
    --text-strong: #fff;
    --muted: #9a9ba8;
    --overlay-rgb: 255, 255, 255;
    --radius: 12px;
    --radius-sm: 8px;
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
    --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.45);
    --danger-text: #ff8a8a;
    --danger-border: #5a2323;
    /* Mirrors the --success-bg pair, which was already tokenised while the
       error box next to it kept a hardcoded maroon and stayed dark on a
       light page. These dark values are exactly what it used, so nothing
       changes here. */
    --danger-bg: #4a1f24;
    --danger-bg-text: #ffb4bb;
    --success-text: #7fd98a;
    --success-bg: #1f4a2f;
    --success-bg-text: #b8f0c8;
    --coin-bg: #3a2e12;
    --coin-text: #ffc452;
    --coin-border: #5c4a1f;
    /* --accent is tuned to carry white text on top of it (buttons, gradients),
       which makes it too dark to BE text on a card: it lands at 4.0:1, under
       the 4.5:1 AA floor. Anything that paints the accent as text or an icon
       on a surface uses this lightened tone instead. */
    --accent-readable: #a39bff;
    --country-chip-text: var(--accent-readable);
    --country-chip-bg: rgba(108, 99, 255, 0.16);
    /* Same idea for the premium gold: --premium is a fill colour, and as text
       on a white card it only reaches 2.98:1. This is the tone to use when
       the gold IS the text. */
    --premium-text: #ffd166;

    /* ---- Brand ----
       One gradient for the whole app: the same purple-to-pink that's on the
       launcher icon, the store graphic and the login mark. It used to
       compete with a second pink-to-orange gradient on the primary buttons
       and the like FAB — which was Tinder's palette, so the app read as a
       clone of it. Everything brand-coloured now goes through these tokens
       so the two can't drift apart again. */
    --brand-from: #6c63ff;
    --brand-to: #fd267a;
    --brand-gradient: linear-gradient(135deg, var(--brand-from), var(--brand-to));
    /* Same purple-to-pink ramp, deepened for surfaces that carry white TEXT
       rather than just an icon. White on the bright end of --brand-gradient
       is 3.66:1 — fine for a 3:1 icon, a fail for the body text in a chat
       bubble. These two ends hold 5.9:1 and 5.9:1. */
    --brand-gradient-deep: linear-gradient(135deg, #5548e0, #c2185b);
    --brand-glow: rgba(108, 99, 255, 0.38);
    --brand-glow-soft: rgba(108, 99, 255, 0.22);
    /* Pink is the gradient's far end, so it works as a standalone accent —
       lightened here because the raw value only hits 4.7:1 on --card. */
    --brand-pink-text: #ff7aa8;

    /* ---- Scale ----
       The app had grown 21 different font sizes (including one-offs like
       10.5 and 19px), 8 different corner radii, and no spacing scale at all,
       so margins were picked per-component and never lined up. These are the
       allowed steps; new rules should use them rather than raw pixels. */
    --text-xs: 11px;
    --text-sm: 12px;
    --text-base: 13px;
    --text-md: 14px;
    --text-lg: 16px;
    --text-xl: 20px;
    --text-2xl: 24px;

    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 24px;
    --space-6: 32px;

    --radius-xs: 6px;
    --radius-pill: 999px;
    --radius-lg: 20px;   /* big surfaces: auth card, upload field */
}

/* Explicit opt-in light theme (toggled in profile settings, applied via a
   blocking inline script in nav.php to avoid a flash of the dark theme on
   load). Everything else in this file reads colors through the tokens
   above, so redefining them here is the entire light theme. */
[data-theme="light"] {
    --accent: #6c63ff;
    --accent-dark: #5548e0;
    --accent-glow: rgba(108, 99, 255, 0.25);
    --bg: #f7f3f2;
    --card: #ffffff;
    --card-alt: #efe9e9;
    --well: #f2ecec;
    --border: #e3dbdc;
    --premium: #d9800a;
    --text: #211c22;
    --text-strong: #0d0a0d;
    --muted: #6e6b78;
    --overlay-rgb: 0, 0, 0;
    --shadow: 0 2px 8px rgba(90, 60, 60, 0.12);
    --shadow-lg: 0 12px 32px rgba(90, 60, 60, 0.18);
    --danger-text: #c23b3b;
    --danger-border: #e3b3b3;
    --danger-bg: #fbe6e6;
    --danger-bg-text: #a02020;
    --success-text: #1f7a3d;
    --success-bg: #e3f5e8;
    --success-bg-text: #1f6b3a;
    --coin-bg: #fdf1d9;
    --coin-text: #8a5a00;
    --coin-border: #f0d9a0;
    /* Darkened instead of lightened here, since it sits on white. */
    --accent-readable: #4a41c9;
    /* Deep amber rather than the fill orange: 5.4:1 on a white card. */
    --premium-text: #9a5b00;
    --country-chip-text: var(--accent-readable);
    --country-chip-bg: rgba(108, 99, 255, 0.12);

    /* Same brand gradient; only the glows and the standalone pink need to
       be re-tuned for a light ground. */
    --brand-glow: rgba(108, 99, 255, 0.28);
    --brand-glow-soft: rgba(108, 99, 255, 0.16);
    --brand-pink-text: #c2185b;
}

/* ---------- Typography ----------
   Nunito, self-hosted (SIL Open Font License) rather than pulled from a font
   CDN: no third-party request from a page that's otherwise entirely
   first-party, and nothing extra to declare in the privacy policy.
   Split by unicode-range so a Lithuanian reader never downloads Cyrillic and
   vice versa — the browser fetches only the subset the page actually needs.
   Variable weight 400–900, so one file covers every heading weight.
   font-display: swap keeps text readable while it loads. */
@font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 400 900;
    font-display: swap;
    src: url('/fonts/nunito-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                   U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                   U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 400 900;
    font-display: swap;
    src: url('/fonts/nunito-latin-ext.woff2') format('woff2');
    /* Lithuanian diacritics (ą č ę ė į š ų ū ž) live in this range. */
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                   U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                   U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 400 900;
    font-display: swap;
    src: url('/fonts/nunito-cyrillic.woff2') format('woff2');
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

* {
    box-sizing: border-box;
}

/* Headings and other "voice" elements get Nunito; running text and form
   controls stay on the system stack, which stays the most legible at small
   sizes on a phone and never shifts as a webfont loads. */
h1, h2, h3,
.brand,
.release-version,
.swipe-heading,
.tinder-card h1,
.stat-value,
.audit-stat-total,
.version-pill,
.likes-you-count {
    font-family: 'Nunito', system-ui, -apple-system, "Segoe UI", sans-serif;
    letter-spacing: -0.01em;
}

[hidden] {
    display: none !important;
}

html {
    scrollbar-color: var(--border) var(--bg);
}

::-webkit-scrollbar {
    width: 10px;
    height: 8px;
}

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

::-webkit-scrollbar-thumb {
    background: var(--border);
    border-radius: var(--radius-pill);
}

::-webkit-scrollbar-thumb:hover {
    background: var(--border);
}

/* ---------- Pull-to-refresh (native app shell only — see nav.php) ---------- */

.pull-refresh-indicator {
    position: fixed;
    top: 0;
    left: 50%;
    width: 36px;
    height: 36px;
    margin-left: -18px;
    margin-top: -40px;
    border-radius: 50%;
    background: var(--card);
    border: 1px solid var(--border);
    box-shadow: var(--shadow);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-lg);
    color: var(--muted);
    opacity: 0;
    z-index: 100;
    pointer-events: none;
    transition: transform 0.2s ease, opacity 0.2s ease;
}

.pull-refresh-indicator.dragging {
    transition: none;
}

.pull-refresh-indicator.ready {
    color: var(--accent);
    border-color: var(--accent);
}

.pull-refresh-indicator.loading {
    opacity: 1;
    animation: pull-refresh-spin 0.7s linear infinite;
}

@keyframes pull-refresh-spin {
    from { transform: translateY(50px) rotate(0deg); }
    to { transform: translateY(50px) rotate(360deg); }
}

html {
    /* Mobile browsers flash a translucent blue-grey box over whatever you tap.
       It's drawn on the element's own rectangle, ignoring border-radius, so a
       round button flashed as a square and a list row lit up edge to edge. The
       app gives its own press feedback (:active below, plus hover states), so
       the system one is only noise. */
    -webkit-tap-highlight-color: transparent;
}

body {
    margin: 0;
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    background: var(--bg);
    color: var(--text);
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
    letter-spacing: -0.01em;
}

/* Without a colour of its own, a link falls back to the browser's #0000EE
   blue (and purple once visited) — which showed up on every link the
   stylesheet didn't happen to name. This is the app's default; anything that
   sets its own colour or inherits still wins. */
a {
    color: var(--accent-readable);
    transition: color 0.15s ease, opacity 0.15s ease;
}

/* Replaces the tap highlight: a quick dim on press, on the element's real
   shape, so a tap still feels acknowledged. */
a:active,
button:active,
.person-row:active {
    opacity: 0.72;
}

/* Selected text used the browser's blue as well. */
::selection {
    background: var(--brand-glow);
    color: var(--text-strong);
}

/* Keyboard focus keeps a visible ring — :focus-visible only matches keyboard
   navigation, so a tap or click never leaves one behind. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--radius-xs);
}

.app {
    max-width: 960px;
    margin: 0 auto;
    padding: 16px;
}

header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
}

header h1 {
    font-size: var(--text-xl);
    margin: 0;
}

.status {
    font-size: var(--text-base);
    padding: 4px 10px;
    border-radius: var(--radius-pill);
    background: var(--border);
    color: var(--muted);
}

.status.matched { background: #1f6f43; color: #fff; }
.status.waiting { background: #7a5c12; color: #fff; }

.videos {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

.video-box {
    position: relative;
    background: #000;
    border-radius: var(--radius-sm);
    overflow: hidden;
    aspect-ratio: 4 / 3;
}

.video-box video {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.video-box .label {
    position: absolute;
    left: 8px;
    bottom: 8px;
    font-size: var(--text-sm);
    background: rgba(0,0,0,0.55);
    padding: 2px 8px;
    border-radius: var(--radius-pill);
}

.controls {
    display: flex;
    gap: 10px;
    margin: 14px 0;
}

.controls button {
    flex: 1;
    padding: 10px;
    border: none;
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: #fff;
    font-size: var(--text-md);
    cursor: pointer;
}

.controls button:disabled {
    background: var(--border);
    color: var(--muted);
    cursor: not-allowed;
}

#nextBtn { background: #d97a1f; }
#stopBtn { background: #b0303a; }

.chat {
    background: var(--card);
    border-radius: var(--radius-sm);
    padding: 10px;
}

.chat-log {
    height: 180px;
    overflow-y: auto;
    font-size: var(--text-base);
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-bottom: 8px;
}

.chat-log .msg {
    padding: 9px 14px;
    /* Near-pill corners with the one nearest the sender squared off — the
       shape that reads as "spoken by that side" without needing a tail. */
    border-radius: 18px;
    max-width: 78%;
    font-size: var(--text-md);
    line-height: 1.45;
    touch-action: pan-y;
}

.chat-date-divider {
    display: flex;
    justify-content: center;
    margin: 10px 0 4px;
    align-self: stretch;
}

.chat-date-divider span {
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--muted);
    background: var(--card-alt);
    border: 1px solid var(--border);
    padding: 3px 12px;
    border-radius: var(--radius-pill);
}

.chat-log .msg.msg-flash {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.reveal-card {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 180px;
}

.reveal-card-info {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.reveal-card-label {
    font-size: var(--text-xs);
    opacity: 0.8;
}

.reveal-card-name {
    font-weight: 700;
    font-size: var(--text-lg);
}

.reveal-card-bio {
    font-size: var(--text-sm);
    opacity: 0.85;
    max-width: 200px;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.gift-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    min-width: 100px;
    padding: 6px 4px;
}

.gift-card-emoji {
    font-size: 40px;
    line-height: 1;
    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.35));
}

.gift-card-label {
    font-size: var(--text-xs);
    opacity: 0.85;
}

.wave-card {
    display: flex;
    line-height: 1;
    padding: 2px 6px;
}

.wave-card.super {
    filter: drop-shadow(0 0 6px rgba(255, 210, 80, 0.7));
}

.gift-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 10px;
    margin: 14px 0;
}

.gift-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 12px 6px;
    background: var(--card-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    color: var(--text);
    font-family: inherit;
}

.gift-item:hover {
    border-color: var(--accent);
}

.gift-emoji {
    font-size: 26px;
    line-height: 1;
}

.gift-cost {
    font-size: var(--text-xs);
    color: var(--muted);
}

.leaderboard-rank {
    width: 24px;
    text-align: center;
    font-size: var(--text-lg);
    flex-shrink: 0;
}

.leaderboard-total {
    font-size: var(--text-base);
    font-weight: 700;
    color: var(--premium-text);
    flex-shrink: 0;
}

.msg-reply-quote {
    display: block;
    margin-bottom: 4px;
    padding: 4px 8px;
    border-left: 2px solid rgba(var(--overlay-rgb), 0.5);
    background: rgba(0, 0, 0, 0.15);
    border-radius: var(--radius-xs);
    font-size: var(--text-sm);
    opacity: 0.85;
    cursor: pointer;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.reply-preview {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    margin-bottom: 6px;
    background: var(--card-alt);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-xs);
}

.reply-preview-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.reply-preview-label {
    font-size: var(--text-xs);
    color: var(--accent);
    font-weight: 600;
}

.reply-preview-text {
    font-size: var(--text-base);
    color: var(--muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.reply-cancel-btn {
    flex-shrink: 0;
    width: 24px;
    height: 24px;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    font-size: var(--text-md);
}

.reply-cancel-btn:hover {
    background: rgba(var(--overlay-rgb), 0.08);
    color: var(--text-strong);
}

/* Your own messages carry the brand gradient — the same purple-to-pink as
   the icon and the buttons, so the app's one accent shows up in the place
   people spend the most time. */
.chat-log .msg.me {
    align-self: flex-end;
    background: var(--brand-gradient-deep);
    color: #fff;
    border-bottom-right-radius: 6px;
}

.chat-log .msg.them {
    align-self: flex-start;
    background: var(--card-alt);
    border: 1px solid var(--border);
    border-bottom-left-radius: 6px;
}

.chat-log .msg.sys {
    align-self: center;
    color: var(--muted);
    font-style: italic;
    background: none;
}

.chat-form {
    display: flex;
    align-items: center;
    gap: 6px;
}

.chat-form input {
    flex: 1;
    min-width: 0;
    padding: 10px 16px;
    /* Pill, to match the round attach and send buttons either side of it. */
    border-radius: var(--radius-pill);
    border: 1px solid var(--border);
    background: var(--well);
    color: var(--text);
}

.chat-form input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--brand-glow-soft);
}

.chat-send {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    flex-shrink: 0;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: var(--brand-gradient);
    color: #fff;
    cursor: pointer;
    transition: transform 0.12s ease, box-shadow 0.15s ease;
}

.chat-send:hover:not(:disabled) {
    transform: translateY(-1px);
    box-shadow: 0 6px 16px var(--brand-glow);
}

.chat-send:disabled {
    background: var(--border);
    color: var(--muted);
    cursor: not-allowed;
}

/* These five carried hardcoded colours from before the token system, so
   they never followed the theme. On a white card they measured 1.44,
   1.44, 2.43, 1.86 and 2.46 to 1 — the interest tags were all but
   invisible. Routed through the tokens, which are tuned per theme. */

/* ---------- Shared buttons ---------- */

.btn {
    display: inline-block;
    padding: 10px 18px;
    border: none;
    border-radius: var(--radius-sm);
    background: linear-gradient(135deg, var(--btn-from), var(--accent-dark));
    color: #fff;
    font-size: var(--text-md);
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    text-align: center;
    transition: transform 0.12s ease, box-shadow 0.15s ease, filter 0.15s ease;
}

/* Any inline icon inside a modal heading rides the text's centre line
   instead of its baseline, where it would sit a couple of pixels low. */
.modal-box h3 {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.btn:hover {
    filter: brightness(1.08);
    box-shadow: 0 4px 14px var(--accent-glow);
}

.btn:active {
    transform: translateY(1px);
}

.btn-outline {
    background: transparent;
    /* The border keeps --accent (a graphic, 3:1 floor); the label takes the
       readable tone, since --accent as text on the page only reaches 4.44. */
    border: 1px solid var(--accent);
    color: var(--accent-readable);
    box-shadow: none;
}

.btn-outline:hover {
    background: rgba(108, 99, 255, 0.12);
    box-shadow: none;
}

.btn-small {
    padding: 6px 12px;
    font-size: var(--text-base);
}

.btn-danger {
    background: linear-gradient(135deg, #d0394a, #a3283a);
}

.btn-danger:hover {
    box-shadow: 0 4px 14px rgba(208, 57, 74, 0.35);
}

.btn:disabled {
    background: var(--border);
    color: var(--muted);
    cursor: not-allowed;
    filter: none;
    box-shadow: none;
    transform: none;
}

/* ---------- Tinder-style login page ---------- */

/* vh/dvh are unreliable inside the Android WebView this app runs in (some
   WebView builds don't support dvh at all, and fall back to a vh that
   doesn't match the real visible viewport) — so the outer scroll lock uses
   the classic html/body height:100% technique instead, which only needs the
   browser's initial containing block and doesn't depend on either unit. */
body.tinder-body {
    height: 100%;
    overflow: hidden;
}

html:has(body.tinder-body) {
    height: 100%;
    overflow: hidden;
}

.tinder-page {
    height: 100%;
    overflow-y: auto;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
             max(16px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
    background:
        radial-gradient(circle at 15% 20%, var(--brand-glow-soft), transparent 45%),
        radial-gradient(circle at 85% 80%, rgba(255, 96, 54, 0.22), transparent 45%),
        var(--bg);
}

.tinder-card {
    width: 100%;
    max-width: 380px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 28px;
    padding: 36px 28px;
    box-shadow: var(--shadow-lg);
    text-align: center;
}

.tinder-logo {
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background: var(--brand-gradient);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 34px;
    margin: 0 auto 16px;
    box-shadow: 0 8px 24px var(--brand-glow);
}

/* The real app mark, so the login screen matches the launcher icon and the
   store listing. The artwork carries its own gradient, so no background
   here — just the rounded mask and the same lift the emoji badge had. */
.auth-logo {
    display: block;
    width: 76px;
    height: 76px;
    border-radius: var(--radius-lg);
    margin: 0 auto 16px;
    box-shadow: 0 8px 24px rgba(108, 99, 255, 0.35);
}

.tinder-card h1 {
    font-size: var(--text-2xl);
    margin: 0 0 4px;
}

.tinder-tagline {
    color: var(--muted);
    font-size: var(--text-base);
    margin: 0 0 24px;
}

.tinder-card form {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.tinder-input {
    width: 100%;
    padding: 14px 20px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border);
    background: var(--well);
    color: var(--text);
    font-size: var(--text-md);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.tinder-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--brand-glow-soft);
    outline: none;
}

/* The one field on the signup form that can't use a placeholder, so it
   keeps a visible label and is styled to sit alongside the pill inputs. */
.tinder-file {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 12px 20px;
    border-radius: var(--radius-lg);
    border: 1px dashed var(--border);
    background: var(--well);
    text-align: left;
}

.tinder-file span {
    font-size: var(--text-sm);
    color: var(--muted);
}

.tinder-file input[type="file"] {
    font-size: var(--text-sm);
    color: var(--text);
    max-width: 100%;
}

.tinder-btn {
    width: 100%;
    padding: 14px;
    border: none;
    border-radius: var(--radius-pill);
    background: var(--brand-gradient);
    color: #fff;
    font-size: var(--text-lg);
    font-weight: 700;
    letter-spacing: 0.3px;
    cursor: pointer;
    margin-top: 8px;
    transition: transform 0.12s ease, box-shadow 0.15s ease;
    box-shadow: 0 8px 20px var(--brand-glow);
}

.tinder-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 10px 26px var(--brand-glow);
}

.tinder-btn:active {
    transform: translateY(0);
}

.tinder-footer {
    margin-top: 20px;
    font-size: var(--text-base);
    color: var(--muted);
}

.tinder-footer a {
    color: var(--brand-pink-text);
    font-weight: 600;
    text-decoration: none;
}

/* ---------- Language switcher ---------- */

.lang-switch {
    display: flex;
    gap: 2px;
    background: var(--well);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    padding: 3px;
}

.lang-switch a {
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--muted);
    text-decoration: none;
    padding: 4px 9px;
    border-radius: var(--radius-pill);
    line-height: 1.4;
    transition: background 0.15s ease, color 0.15s ease;
}

.lang-switch a:hover {
    color: var(--text-strong);
    background: rgba(var(--overlay-rgb), 0.06);
}

.lang-switch a.active {
    color: #fff;
    background: var(--accent);
}

/* The auth pages render edge-to-edge (viewport-fit=cover), so a plain
   top:16px puts this under the phone's status bar — clock, battery and
   signal sit right on top of it. Inset by the safe area instead. */
.lang-switch-floating {
    position: fixed;
    top: calc(max(16px, env(safe-area-inset-top)) + 8px);
    right: max(16px, env(safe-area-inset-right));
    background: var(--card);
    z-index: 20;
}

/* ---------- Auth pages ---------- */

.auth-wrap {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
}

.auth-card {
    width: 100%;
    max-width: 380px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 28px;
    box-shadow: var(--shadow-lg);
}

.auth-card h1 {
    margin-top: 0;
    font-size: var(--text-xl);
}

.auth-card form {
    display: flex;
    flex-direction: column;
    gap: 14px;
    margin-top: 16px;
}

.auth-card label {
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: var(--text-base);
    color: var(--muted);
}

.auth-card input,
.auth-card select {
    padding: 10px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--well);
    color: var(--text);
    font-size: var(--text-md);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.auth-card input:focus,
.auth-card select:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-glow);
    outline: none;
}

.auth-actions {
    display: flex;
    gap: 10px;
    margin-top: 16px;
}

.muted {
    color: var(--muted);
    font-size: var(--text-base);
}

.alert {
    background: var(--danger-bg);
    color: var(--danger-bg-text);
    padding: 10px 12px;
    border-radius: var(--radius-sm);
    font-size: var(--text-base);
    /* Was margin-top only, which left whatever came next stuck to the
       bottom edge — on the login card the message sat flush against the
       username field with no gap at all. */
    margin: 12px 0;
}

/* On the sign-in and sign-up cards the alert lands directly above the form,
   where the rhythm is already set by the tagline's 24px. At 12px the message
   reads as a label attached to the first field rather than as its own block. */
.tinder-card .alert,
.auth-card .alert {
    margin-bottom: 24px;
}

.alert-success {
    background: var(--success-bg);
    color: var(--success-bg-text);
}

/* ---------- Top nav (logged-in pages) ---------- */

.topnav {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px 16px;
    padding: 12px 20px;
    background: var(--card);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 20;
    box-shadow: var(--shadow);
    backdrop-filter: blur(8px);
    /* A vertical drag starting on the bar is the pull-to-refresh gesture
       (see nav.php), so the browser must not treat it as a scroll. pan-x
       leaves horizontal gestures and every tap alone. Without this the page
       would scroll away under the finger while the indicator was being
       pulled down. */
    touch-action: pan-x;
}

/* The More menu is a list you scroll — it keeps normal panning. */
.topnav .menu-dropdown {
    touch-action: auto;
}

.topnav .brand {
    color: var(--text-strong);
    text-decoration: none;
    font-weight: 700;
    white-space: nowrap;
    background: var(--brand-gradient);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

.topnav-links {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 4px;
    overflow-x: auto;
    white-space: nowrap;
}

.topnav-links a {
    display: inline-block;
    color: var(--muted);
    text-decoration: none;
    font-size: var(--text-base);
    font-weight: 500;
    padding: 6px 12px;
    border-radius: var(--radius-pill);
    transition: color 0.15s ease, background 0.15s ease;
}

.topnav-links a:hover {
    color: var(--text-strong);
    background: var(--card-alt);
}

.topnav-links a.active {
    color: #fff;
    background: var(--accent);
}

.topnav-more {
    flex-shrink: 0;
    margin-left: 4px;
}

.topnav-more > button.active {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

.topnav-more .menu-dropdown a.active {
    color: var(--accent);
    background: none;
    font-weight: 600;
}

.topnav-right {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-left: auto;
    white-space: nowrap;
}

/* On wide screens these wrappers are invisible to layout — their children
   flow directly into .topnav's single flex row exactly as before. The
   mobile media query below turns them into real two-row containers. */
.topnav-row {
    display: contents;
}

/* A stat badge, not an action — visually lighter than the outline buttons
   around it (Daugiau/Atsijungti) so the bar doesn't read as a wall of
   identical bordered boxes. */
#navCoinsBalance.btn {
    background: var(--coin-bg);
    border: 1px solid var(--coin-border);
    color: var(--coin-text);
    font-weight: 600;
    box-shadow: none;
}

#navCoinsBalance.btn:hover {
    filter: brightness(1.08);
    box-shadow: none;
}

/* Logout is a low-frequency action — a plain link keeps it out of the way
   of the bordered-button cluster instead of matching their visual weight. */
.topnav-right a[href="/logout"] {
    border: none;
    background: none;
    color: var(--muted);
    padding: 6px 4px;
    text-decoration: none;
}

.topnav-right a[href="/logout"]:hover {
    color: var(--text-strong);
    background: none;
}

.page {
    max-width: 720px;
    margin: 0 auto;
    padding: 20px 16px;
}

/* ---------- Browse ---------- */

.browse-page {
    max-width: 760px;
}

.promo-arrow {
    margin-left: auto;
    font-size: var(--text-xl);
    opacity: 0.7;
}

.browse-section {
    margin-bottom: 28px;
}

.section-title {
    font-size: var(--text-lg);
    font-weight: 700;
    margin: 0 0 12px;
    color: var(--muted);
    letter-spacing: 0.2px;
}

.section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
}

.section-head .section-title {
    margin: 0;
}

/* One row: the search field takes everything the Filters button doesn't. */
.browse-toolbar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
}

.browse-toolbar .input-with-icon {
    flex: 1;
    min-width: 0;
}

.browse-toolbar .btn-with-icon {
    flex-shrink: 0;
}

/* The count of filters currently narrowing the list. Sits on the button so a
   shut panel can never silently change what you're looking at. */
.filters-dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 17px;
    height: 17px;
    margin-left: 6px;
    padding: 0 4px;
    border-radius: var(--radius-pill);
    background: var(--brand-gradient-deep);
    color: #fff;
    font-size: var(--text-xs);
    font-weight: 700;
    line-height: 1;
}

.browse-filters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: var(--space-3);
    margin-bottom: var(--space-3);
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.browse-filters[hidden] {
    display: none;
}

.browse-filters .pill-select,
.browse-filters .pill-input {
    flex: 1 1 140px;
    min-width: 0;
}

.browse-filters-actions {
    display: flex;
    gap: var(--space-2);
    flex: 1 1 100%;
}

.browse-filters-actions .btn {
    flex: 1;
}

.filters-premium-hint {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    text-decoration: none;
    color: var(--muted);
    flex: 1 1 100%;
}

.pill-select,
.pill-input {
    padding: 8px 14px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border);
    background: var(--card);
    color: var(--text);
    font-size: var(--text-base);
    transition: border-color 0.15s ease;
}

.pill-select:focus,
.pill-input:focus {
    border-color: var(--accent);
    outline: none;
}

.pill-input {
    width: 150px;
}

.browse-count {
    margin: 0 0 12px;
    font-size: var(--text-sm);
}

.people-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: 1fr;
    gap: 10px;
}

@media (min-width: 640px) {
    .people-list {
        grid-template-columns: 1fr 1fr;
    }
}

@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(6px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.fade-in {
    animation: fadeInUp 0.25s ease both;
}

.browse-skeleton {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.skeleton-row {
    height: 66px;
    border-radius: var(--radius-sm);
    background: linear-gradient(90deg, var(--card) 25%, var(--card-alt) 37%, var(--card) 63%);
    background-size: 400% 100%;
    animation: skeletonShine 1.4s ease infinite;
}

@keyframes skeletonShine {
    0% { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 40px 20px;
    color: var(--muted);
    font-size: var(--text-base);
    text-align: center;
}

.empty-state-icon {
    font-size: 32px;
    opacity: 0.6;
    line-height: 0;
}

/* ---------- Icon primitives ---------- */

/* Icons take the colour of whatever text they sit with, and line up on the
   text baseline rather than floating above it. */
.icon {
    flex-shrink: 0;
    vertical-align: -0.15em;
}

.btn-with-icon {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

/* Square icon-only button — used where a word would be the widest thing in
   a row (the Write action in the browse list). The label moves to
   aria-label/title so nothing is lost for screen readers or on hover. */
.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    padding: 0;
    border: none;
    border-radius: var(--radius-pill);
    background: var(--brand-gradient);
    color: #fff;
    cursor: pointer;
    text-decoration: none;
    flex-shrink: 0;
    transition: transform 0.12s ease, box-shadow 0.15s ease;
}

.btn-icon svg {
    width: 19px;
    height: 19px;
}

.btn-icon:hover {
    transform: translateY(-1px);
    box-shadow: 0 6px 16px var(--brand-glow);
}

/* Search field with the magnifier inside it instead of as a placeholder
   emoji — the emoji shifted position as soon as the user typed. */
.input-with-icon {
    position: relative;
    display: flex;
    align-items: center;
    flex: 1 1 180px;
    min-width: 0;
}

.input-with-icon .icon {
    position: absolute;
    left: 12px;
    color: var(--muted);
    pointer-events: none;
}

.input-with-icon input {
    width: 100%;
    padding-left: 34px;
}

.chats-search {
    display: flex;
    margin-bottom: var(--space-4);
}

.pin-mark {
    display: inline-flex;
    color: var(--accent-readable);
    margin-right: var(--space-1);
    vertical-align: -0.1em;
}

.empty-state-icon .icon {
    color: var(--muted);
    opacity: 0.75;
}

/* ---------- "How others see you" preview ---------- */

/* Entry point on your own profile page. */
.preview-cta {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    margin-bottom: 14px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--card);
    color: var(--text);
    text-decoration: none;
}

.preview-cta:hover { border-color: var(--accent); }

.preview-cta-icon { font-size: var(--text-xl); flex-shrink: 0; }

.preview-cta-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1;
    min-width: 0;
    font-size: var(--text-base);
}

.preview-cta-arrow {
    font-size: var(--text-xl);
    color: var(--muted);
    flex-shrink: 0;
}

/* Banner shown on the profile page itself while in preview mode. */
.preview-banner {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    margin-bottom: 14px;
    border-radius: var(--radius);
    border: 1px solid var(--accent);
    background: linear-gradient(135deg, rgba(108, 99, 255, 0.14), transparent 70%), var(--card);
}

.preview-banner-icon { font-size: var(--text-xl); flex-shrink: 0; }

.preview-banner-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1;
    min-width: 0;
    font-size: var(--text-base);
}

@media (max-width: 640px) {
    .preview-banner { flex-wrap: wrap; }
    .preview-banner .btn { width: 100%; }
}

/* ---------- Fullscreen photo viewer ---------- */

.photo-grid img { cursor: pointer; }

.photo-lightbox {
    position: fixed;
    inset: 0;
    z-index: 200;
    background: rgba(0, 0, 0, 0.94);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
             max(16px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
    touch-action: none;
}

.photo-lightbox[hidden] { display: none; }

.photo-lightbox-img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    touch-action: none;
    user-select: none;
    -webkit-user-drag: none;
    border-radius: var(--radius-xs);
}

.photo-lightbox-close,
.photo-lightbox-nav {
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.15);
    color: #fff;
    cursor: pointer;
    z-index: 2;
}

.photo-lightbox-close:hover,
.photo-lightbox-nav:hover {
    background: rgba(255, 255, 255, 0.28);
}

.photo-lightbox-close {
    top: max(16px, env(safe-area-inset-top));
    right: max(16px, env(safe-area-inset-right));
    width: 40px;
    height: 40px;
    font-size: 18px;
}

.photo-lightbox-nav {
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    font-size: var(--text-2xl);
}

.photo-lightbox-nav[hidden] { display: none; }
.photo-lightbox-prev { left: max(10px, env(safe-area-inset-left)); }
.photo-lightbox-next { right: max(10px, env(safe-area-inset-right)); }

.photo-lightbox-dots {
    position: absolute;
    bottom: max(18px, env(safe-area-inset-bottom));
    left: 0;
    right: 0;
    display: flex;
    justify-content: center;
    gap: 6px;
}

.photo-lightbox-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.4);
}

.photo-lightbox-dot.active { background: #fff; }

/* "First in your country" moderator claim banner. */
.founder-banner {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    margin-bottom: 16px;
    border-radius: var(--radius);
    border: 1px solid var(--accent);
    background: linear-gradient(135deg, rgba(108, 99, 255, 0.14), rgba(253, 38, 122, 0.10));
}

.founder-banner-badge {
    font-size: 28px;
    flex-shrink: 0;
}

.founder-banner-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1;
    min-width: 0;
    font-size: var(--text-base);
}

.founder-banner-text strong {
    font-size: var(--text-md);
}

.founder-banner.claimed {
    border-color: var(--success, #1f8f5f);
}

@media (max-width: 640px) {
    .founder-banner {
        flex-wrap: wrap;
    }
    .founder-banner .btn {
        width: 100%;
    }
}

/* Secondary line under an empty-state message — turns a dead end into
   something the user can act on while the app is still filling up. */
.empty-state-hint {
    display: block;
    max-width: 320px;
    margin: 6px auto 0;
    padding: 10px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--card);
    color: var(--text);
    font-size: var(--text-sm);
    line-height: 1.5;
}

.person-actions {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}

.person-row {
    display: flex;
    align-items: center;
    gap: 12px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 10px 14px;
    transition: border-color 0.15s ease, transform 0.15s ease;
    /* Grid items default to min-width:auto, i.e. they refuse to shrink
       below their min-content width. A long nowrap bio inside therefore
       propped the whole 1fr track open and pushed the page wider than the
       viewport, making it scroll sideways. This lets the row shrink so the
       bio's ellipsis can do its job instead. */
    min-width: 0;
}

.person-row:hover {
    border-color: var(--accent);
}

/* ---------- Feed-style list (Browse, Chats) ----------
   An opt-in modifier rather than a change to .person-row itself, which is
   also used by the admin queues, Blocked, Following and the leaderboard —
   those genuinely read better as discrete cards.
   Here the card chrome is the problem: a bordered box per person turns a
   scrolling list of people into a stack of receipts. Dropping to hairline
   dividers, larger avatars and a bigger name puts the weight on the person
   instead of the container, which is how the dating apps present this. */
.people-list.list-feed {
    gap: 0;
}

.people-list.list-feed .person-row {
    background: none;
    border: none;
    border-bottom: 1px solid var(--border);
    border-radius: 0;
    padding: var(--space-3) var(--space-1);
    gap: var(--space-3);
}

.people-list.list-feed .person-row:last-child {
    border-bottom: none;
}

/* Card rows signalled hover with a border; without one, use a soft wash
   that bleeds past the text padding so the whole row reads as the target. */
.people-list.list-feed .person-row:hover {
    background: var(--card);
    border-color: var(--border);
    box-shadow: 0 0 0 var(--space-1) var(--card);
    border-radius: var(--radius-xs);
}

.people-list.list-feed .person-label {
    font-family: 'Nunito', system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: var(--text-lg);
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.25;
}

.people-list.list-feed .person-status,
.people-list.list-feed .person-bio {
    font-size: var(--text-md);
}

.people-list.list-feed .person-info {
    gap: 3px;
}

.person-info {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    color: inherit;
    text-decoration: none;
}

.person-label {
    flex: 1;
}

.settings-card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 18px 20px;
    margin-bottom: 20px;
}

/* A settings-card that groups several related sub-sections behind a native
   <details> disclosure — keeps a long settings page scannable (one line
   per topic) instead of a wall of always-expanded cards, without any JS. */
.settings-group {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: 20px;
    overflow: hidden;
}

.settings-group > summary {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 16px 20px;
    cursor: pointer;
    font-weight: 600;
    font-size: var(--text-lg);
    list-style: none;
    user-select: none;
}

.settings-group > summary::-webkit-details-marker {
    display: none;
}

.settings-group > summary::after {
    content: '';
    width: 8px;
    height: 8px;
    margin-left: auto;
    border-right: 2px solid var(--muted);
    border-bottom: 2px solid var(--muted);
    transform: rotate(-45deg);
    transition: transform 0.15s ease;
    flex-shrink: 0;
}

.settings-group[open] > summary::after {
    transform: rotate(45deg);
}

.settings-group > summary:hover {
    background: var(--card-alt);
}

.settings-group-body {
    padding: 4px 20px 20px;
    display: flex;
    flex-direction: column;
    gap: 22px;
}

.settings-group-body > .settings-subsection > h3 {
    margin: 0 0 4px;
    font-size: var(--text-base);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--muted);
}

.settings-group-body > .settings-subsection:not(:first-child) {
    padding-top: 18px;
    border-top: 1px solid var(--border);
}

.settings-group-danger > summary {
    color: var(--danger-text);
}

.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 12px;
    margin-bottom: 20px;
}

.stat-card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 14px 16px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.stat-value {
    font-size: var(--text-2xl);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.stat-label {
    font-size: var(--text-sm);
    color: var(--muted);
}

.sparkline-bars {
    display: flex;
    align-items: flex-end;
    gap: 10px;
    height: 100px;
    padding-top: 10px;
}

.sparkline-bar-wrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    flex: 1;
}

.sparkline-bar {
    width: 100%;
    max-width: 28px;
    background: var(--accent);
    border-radius: 4px 4px 0 0;
}

.sparkline-label {
    font-size: var(--text-xs);
    color: var(--muted);
}

.settings-card h2 {
    /* inline-flex so a leading icon sits on the text's centre line rather
       than on its baseline, where it hangs a couple of pixels low. */
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0 0 4px;
    font-size: var(--text-lg);
}

.settings-card h2 .icon {
    color: var(--accent-readable);
    flex-shrink: 0;
}

.settings-card .muted {
    margin: 0 0 12px;
}

.form-field {
    width: 100%;
    padding: 12px 14px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--card-alt);
    color: var(--text);
    font-size: var(--text-md);
    font-family: inherit;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.form-field:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-glow);
    outline: none;
}

.form-row {
    display: flex;
    gap: 8px;
}

.form-row .form-field {
    flex: 1;
}

.chat-list-link {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    color: inherit;
    text-decoration: none;
}

.chat-preview {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.chat-time {
    flex-shrink: 0;
    font-size: var(--text-sm);
    color: var(--muted);
}

.person-bio {
    font-size: var(--text-sm);
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

.avatar-wrap {
    position: relative;
    flex-shrink: 0;
    line-height: 0;
}

.avatar-wrap .dot {
    position: absolute;
    right: -1px;
    bottom: -1px;
    border: 2px solid var(--card);
}

.dot {
    width: 11px;
    height: 11px;
    border-radius: 50%;
    flex-shrink: 0;
}

.dot.online { background: #2ecc71; }

/* Offline is a hollow ring, not a filled dot. Filled, the 11px circle sat
   inside its 2px near-black card-coloured border and read as a small dark
   square stuck to the photo — people asked what it was. Hollow says
   "nothing here" and can't be mistaken for a badge. */
.dot.offline {
    background: var(--card);
    box-shadow: inset 0 0 0 2px var(--muted);
}

.premium-badge {
    font-size: var(--text-xs);
    color: var(--premium-text);
}

.person-status {
    font-size: var(--text-sm);
    color: var(--muted);
}

/* Shown next to the online dot when discovery had to look outside the
   viewer's own country, so a distant match is never mistaken for a local. */
.person-country {
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: var(--radius-pill);
    background: var(--country-chip-bg);
    color: var(--country-chip-text);
    font-size: var(--text-xs);
    white-space: nowrap;
}

.crown-badge {
    position: absolute;
    left: -2px;
    top: -4px;
    font-size: var(--text-lg);
    filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.6));
}

.unread-badge {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    border-radius: var(--radius-pill);
    background: #e5484d;
    color: #fff;
    font-size: var(--text-xs);
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px solid var(--card);
}

.story .unread-badge {
    border-color: var(--bg);
}

/* ---------- Premium promo banner ---------- */

.promo-banner {
    display: flex;
    align-items: center;
    gap: 12px;
    background: linear-gradient(135deg, var(--premium), #ff6a3d);
    border-radius: var(--radius);
    padding: 14px 16px;
    margin-bottom: 16px;
    text-decoration: none;
    color: #1a1206;
    box-shadow: 0 6px 20px rgba(255, 106, 61, 0.25);
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.promo-banner:hover {
    transform: translateY(-1px);
    box-shadow: 0 8px 26px rgba(255, 106, 61, 0.35);
}

.promo-icon {
    font-size: 26px;
}

.promo-banner strong {
    display: block;
    font-size: var(--text-md);
}

.promo-sub {
    display: block;
    font-size: var(--text-sm);
    opacity: 0.85;
}

/* ---------- Premium page ---------- */

.premium-hero {
    max-width: 420px;
    margin: 20px auto;
    text-align: center;
    background: linear-gradient(180deg, var(--card-alt), var(--card));
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 32px 24px;
    box-shadow: var(--shadow-lg);
}

.premium-crown {
    font-size: 44px;
    margin-bottom: 8px;
}

.premium-features {
    list-style: none;
    padding: 0;
    margin: 18px 0;
    text-align: left;
    display: flex;
    flex-direction: column;
    gap: 10px;
    font-size: var(--text-md);
}

/* ---------- Stories bar (followed users) ---------- */

.stories-bar {
    display: flex;
    gap: 14px;
    overflow-x: auto;
    padding: 4px 2px 16px;
    margin-bottom: 12px;
    border-bottom: 1px solid var(--border);
}

.story {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    text-decoration: none;
    flex-shrink: 0;
    width: 64px;
    position: relative;
}

.story .avatar {
    border: 2px solid transparent;
    padding: 2px;
}

/* A ring only appears once a follow actually has an active story posted:
   bright accent = unseen, muted gray = already viewed. */
.avatar-wrap.story-ring .avatar {
    border-color: var(--border);
}

.avatar-wrap.story-ring.unseen .avatar {
    border-color: var(--accent);
}

.story-add-badge {
    position: absolute;
    right: -2px;
    bottom: -2px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
    font-size: var(--text-md);
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px solid var(--bg);
}

.story-dot {
    position: absolute;
    top: 44px;
    left: 44px;
    border: 2px solid #12141a;
}

.story-label {
    font-size: var(--text-xs);
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 64px;
}

/* ---------- Story upload modal ---------- */

.story-upload-choices {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 14px 0;
}

.story-upload-choices .btn {
    justify-content: center;
}

/* ---------- Story viewer (full-screen) ---------- */

.story-viewer {
    position: fixed;
    inset: 0;
    z-index: 200;
    background: #000;
    display: flex;
    flex-direction: column;
}

.story-progress-bars {
    display: flex;
    gap: 4px;
    padding: 10px 10px 0;
}

.story-progress-seg {
    flex: 1;
    height: 3px;
    border-radius: var(--radius-pill);
    background: rgba(255, 255, 255, 0.3);
    overflow: hidden;
}

.story-progress-fill {
    height: 100%;
    width: 0;
    background: #fff;
}

.story-viewer-header {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;
    color: #fff;
}

.story-viewer-label {
    flex: 1;
    font-weight: 600;
    font-size: var(--text-md);
}

.story-viewer-delete,
.story-viewer-close {
    background: none;
    border: none;
    color: #fff;
    font-size: 18px;
    cursor: pointer;
    padding: 6px;
}

.story-viewer-media {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
    position: relative;
}

.story-viewer-media img,
.story-viewer-media video {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.story-nav-zone {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 35%;
    background: none;
    border: none;
    cursor: pointer;
}

.story-nav-prev {
    left: 0;
}

.story-nav-next {
    right: 0;
}

/* ---------- Profile photo gallery ---------- */

.photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 12px;
}

.photo-item {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 8px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    transition: border-color 0.15s ease;
}

.photo-item.is-primary {
    outline: 2px solid var(--accent);
}

.photo-item img {
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: var(--radius-sm);
}

.primary-badge {
    font-size: var(--text-sm);
    color: var(--accent-readable);
    text-align: center;
}

.photo-action {
    margin: 0;
}

.photo-action .btn {
    width: 100%;
}

/* ---------- Interest tags ---------- */

.interest-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 8px;
}

.interest-tag {
    font-size: var(--text-sm);
    padding: 4px 10px;
    border-radius: var(--radius-pill);
    background: rgba(108, 99, 255, 0.15);
    color: var(--accent-readable);
}

/* ---------- Admin tables ---------- */

.admin-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-base);
}

.admin-table th,
.admin-table td {
    text-align: left;
    padding: 8px 10px;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

.admin-table th {
    color: var(--muted);
    font-weight: 500;
}

.admin-table select {
    padding: 4px 6px;
    border-radius: var(--radius-xs);
    border: 1px solid var(--border);
    background: var(--well);
    color: var(--text);
}

/* ---------- Report queue ---------- */

.report-card {
    background: var(--card);
    border-radius: var(--radius-sm);
    padding: 12px 14px;
    border-left: 3px solid #b0303a;
    box-shadow: var(--shadow);
}

.report-card.reviewed {
    border-left-color: var(--border);
    opacity: 0.7;
}

/* Small "you have something waiting" marker on a nav item. */
.nav-alert-dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 16px;
    height: 16px;
    margin-left: 5px;
    padding: 0 4px;
    border-radius: var(--radius-pill);
    background: #d93a3a;
    color: #fff;
    font-size: var(--text-xs);
    font-weight: 800;
    line-height: 1;
    vertical-align: middle;
}

/* A case a moderator handed up because they weren't sure what to do. */
.report-card.escalated {
    border-left-color: #d9931f;
    background: linear-gradient(90deg, rgba(217, 147, 31, 0.08), transparent 60%), var(--card);
}

.escalation-note {
    margin: 8px 0;
    padding: 8px 12px;
    border-radius: var(--radius-sm);
    background: var(--well);
    border-left: 3px solid #d9931f;
    font-size: var(--text-base);
}

.escalation-note p {
    margin: 4px 0 0;
    color: var(--text);
}

/* ---------- What's new / release notes ---------- */

.whatsnew-head {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

.whatsnew-head h2 { margin: 0; }

/* Mirrors the versionName shown on the Google Play listing. */
.version-pill {
    padding: 3px 10px;
    border-radius: var(--radius-pill);
    background: var(--country-chip-bg);
    color: var(--country-chip-text);
    font-size: var(--text-sm);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.release-card { margin-bottom: 10px; }

.release-head {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 8px;
}

.release-version {
    font-size: var(--text-lg);
    font-weight: 800;
    font-variant-numeric: tabular-nums;
}

.release-date {
    font-size: var(--text-sm);
    margin-left: auto;
}

.release-list {
    margin: 0;
    padding-left: 18px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.release-list li {
    font-size: var(--text-md);
    line-height: 1.55;
    color: var(--text);
}

/* ---------- Community translations ---------- */

.translate-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin: 14px 0 10px;
}

.translate-toolbar .pill-input { flex: 1 1 180px; }

.translate-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 10px;
    align-items: center;
    padding: 8px 0;
    border-bottom: 1px solid var(--border);
    /* Grid items refuse to shrink below min-content by default; without
       this a long English source string widens the whole page. */
    min-width: 0;
}

.translate-source {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.translate-key {
    font-size: var(--text-xs);
    color: var(--muted);
    font-family: ui-monospace, Consolas, monospace;
    word-break: break-all;
}

.translate-en {
    font-size: var(--text-base);
    color: var(--text);
}

.translate-target {
    font-size: var(--text-base);
    color: var(--text);
    min-width: 0;
    word-break: break-word;
}

.translate-input { width: 100%; min-width: 0; }

.translate-preview {
    max-height: 420px;
    overflow-y: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0 12px;
}

.translate-progress {
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--well);
    overflow: hidden;
    margin: 6px 0 2px;
}

.translate-progress-bar {
    height: 100%;
    background: linear-gradient(90deg, var(--accent), var(--accent-dark));
}

@media (max-width: 640px) {
    .translate-row { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- Audit log ---------- */

/* Severity stripe so destructive actions stand out when scanning. */
.report-card.audit-severe  { border-left-color: #b0303a; }
.report-card.audit-warn    { border-left-color: #d9931f; }
.report-card.audit-good    { border-left-color: #1f8f5f; }
.report-card.audit-neutral { border-left-color: var(--border); }

.audit-icon { margin-right: 2px; }

.audit-role-chip {
    display: inline-block;
    padding: 1px 6px;
    border-radius: var(--radius-pill);
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--muted);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.audit-role-chip.admin {
    color: #ffd166;
    border-color: #ffd166;
}

.audit-state {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin: 6px 0;
    font-size: var(--text-sm);
}

.audit-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    margin-bottom: 14px;
}

.audit-stats {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 10px;
    margin-bottom: 18px;
}

.audit-stat-card {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 12px 14px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--card);
    text-decoration: none;
    color: var(--text);
}

.audit-stat-card:hover { border-color: var(--accent); }
.audit-stat-card.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

.audit-stat-name {
    display: flex;
    align-items: center;
    gap: 6px;
    font-weight: 600;
    font-size: var(--text-base);
}

.audit-stat-total {
    font-size: var(--text-2xl);
    font-weight: 800;
    line-height: 1.1;
}

.audit-stat-sub {
    font-size: var(--text-xs);
    color: var(--muted);
}

.report-meta {
    font-size: var(--text-base);
    margin-bottom: 6px;
}

.report-reason {
    font-size: var(--text-base);
    color: var(--muted);
    margin: 0 0 10px;
}

.report-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

/* ---------- Chat page ---------- */

/* A messaging app's header bar: back, who you're talking to, and the two
   things you can do about it. The text labels that used to sit on the back
   link and the call button ate 170px of a 375px row, leaving the name 125px;
   as icons they cost 40px each and the name gets the rest. */
.chat-header {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}

/* The divider belongs to the real conversation header only — .chat-header
   doubles as a plain page-title row on the admin screens and media.php,
   where a rule under the title would be new furniture nobody asked for. */
.chat-page .chat-header {
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--border);
}

.chat-header h2 {
    flex: 1;
    margin: 0;
    font-size: 18px;
}

.chat-back,
.chat-header-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    padding: 0;
    border: none;
    border-radius: var(--radius-pill);
    background: transparent;
    color: var(--text);
    cursor: pointer;
    text-decoration: none;
    transition: background 0.15s ease, color 0.15s ease;
}

.chat-back:hover,
.chat-header-action:hover:not(:disabled) {
    background: var(--card-alt);
    color: var(--text-strong);
}

.chat-header-action:disabled {
    color: var(--muted);
    opacity: 0.5;
    cursor: not-allowed;
}

.chat-header-identity {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex: 1;
    /* Grid/flex children default to min-content width and refuse to shrink;
       without this a long username pushes the call and menu buttons off. */
    min-width: 0;
    color: inherit;
    text-decoration: none;
    padding: 4px 6px;
    margin: -4px -6px;
    border-radius: var(--radius);
    transition: background 0.15s ease;
}

/* Only the linked version reacts — the anon one goes nowhere, so a hover
   state would promise a profile page that isn't there. */
a.chat-header-identity:hover {
    background: var(--card-alt);
}

.chat-header-text {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}

.chat-header-name {
    display: flex;
    align-items: center;
    gap: 5px;
    font-family: 'Nunito', system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--text-strong);
    line-height: 1.2;
    /* One line, cut with an ellipsis: a wrapped name would change the
       header's height and shove the whole conversation down. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chat-header-status {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: var(--text-sm);
    color: var(--muted);
}

.chat-header-status .dot {
    width: 7px;
    height: 7px;
}

.verified-tick {
    display: inline-flex;
    color: var(--accent-readable);
    flex-shrink: 0;
}

/* In a header the tick sits in a flex row, which centres it for us. In a
   browse or leaderboard row it sits inside a line of text, where an
   inline-flex box rests its bottom edge on the baseline and so hangs below
   the letters. Nudging it up puts it back on the optical centre of the name. */
.person-label .verified-tick {
    margin-left: 4px;
    vertical-align: -0.18em;
}

/* ---------- Viewed profile ---------- */

/* The photo is what someone came here to look at, so it leads: centred and
   large, with the name and status stacked beneath. This used to reuse
   .chat-header — a 40px-tall horizontal bar that gave a person's profile
   the same visual weight as a page title. */
.profile-hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--space-1);
    padding: var(--space-5) var(--space-4) var(--space-4);
    margin-top: var(--space-3);
    background:
        radial-gradient(circle at 50% 0%, var(--brand-glow-soft), transparent 62%),
        var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

.profile-hero-avatar {
    line-height: 0;
    margin-bottom: var(--space-2);
}

.profile-hero-avatar .avatar,
.profile-hero-avatar .avatar-placeholder {
    /* A ring in the brand colour rather than a plain border — it frames the
       photo the way the story rings elsewhere in the app do. */
    border: 3px solid transparent;
    background-image: linear-gradient(var(--card), var(--card)), var(--brand-gradient);
    background-origin: border-box;
    background-clip: content-box, border-box;
    box-shadow: 0 8px 24px var(--brand-glow-soft);
}

.profile-hero-avatar .avatar-placeholder {
    font-size: var(--text-2xl);
}

.profile-hero-name {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    margin: 0;
    font-size: var(--text-2xl);
    font-weight: 800;
    letter-spacing: -0.02em;
    color: var(--text-strong);
    text-wrap: balance;
}

.profile-hero-status {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 0;
    font-size: var(--text-base);
    color: var(--muted);
}

.profile-hero-status .dot {
    width: 8px;
    height: 8px;
}

.profile-hero-stat {
    margin: var(--space-1) 0 0;
    font-size: var(--text-base);
}

.profile-hero-stat strong {
    font-size: var(--text-lg);
    font-weight: 800;
    color: var(--accent-readable);
}

.view-profile-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-4) 0;
}

/* The two labelled buttons split the leftover width evenly; the icon-only
   ones keep their fixed 40px at the end. That way the row stays on one line
   down to 320px instead of wrapping the last button onto its own. */
.view-profile-actions > .btn:not(.btn-icon) {
    flex: 1 1 0;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 10px 12px;
    font-size: var(--text-base);
    white-space: nowrap;
}

.view-profile-actions .menu-wrap {
    flex: 0 0 auto;
}

.view-profile-actions .btn-icon {
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    background: transparent;
    border: 1px solid var(--border);
    color: var(--muted);
}

.view-profile-actions .btn-icon:hover {
    background: var(--card-alt);
    color: var(--text-strong);
    box-shadow: none;
    transform: none;
}

/* Waved already: the tick goes green so the state is visible and not only in
   the tooltip. Stays disabled, so no hover change. */
.view-profile-actions .btn-icon.is-sent,
.view-profile-actions .btn-icon.is-sent:hover {
    color: var(--success-text);
    border-color: var(--success-text);
    background: transparent;
    opacity: 1;
}

/* Following is an ongoing state, not a fresh call-to-action — filled/muted
   reads as "already doing this" instead of inviting another click, the
   same distinction apps make between a Follow and a Following button. */
#followBtn[data-following="1"] {
    background: var(--card-alt);
    border-color: var(--border);
    color: var(--text-strong);
}

/* Block/Report live in the "⋮" dropdown menu (same pattern as chat.php),
   not as inline buttons — five buttons (Write/Wave/Follow/Block/Report)
   don't fit on a narrow phone screen without wrapping mid-row. Highlight
   red only for hover/active-blocked state; the dropdown's own default
   button styling (.menu-dropdown button) covers the rest. */
#blockBtn:hover,
#reportBtn:hover {
    color: var(--danger-text);
}

#blockBtn[data-blocked="1"] {
    color: var(--danger-text);
}

.view-profile-bio {
    white-space: pre-line;
    line-height: 1.5;
}

.back-link {
    color: var(--muted);
    text-decoration: none;
    font-size: var(--text-base);
}

.chat-log.big {
    height: 400px;
}

.typing-indicator {
    font-size: var(--text-sm);
    color: var(--muted);
    font-style: italic;
    padding: 2px 4px 8px;
}

.msg-meta {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    margin-top: 3px;
}

.msg-time {
    font-size: var(--text-xs);
    color: rgba(var(--overlay-rgb), 0.55);
}

.read-receipt {
    font-size: var(--text-xs);
    color: rgba(var(--overlay-rgb), 0.6);
}

/* "me" bubbles always sit on the accent gradient (same in both themes), so
   their metadata text stays white regardless — only "them" bubbles (whose
   background actually flips with the theme) need the theme-aware color. */
.chat-log .msg.me .msg-time,
.chat-log .msg.me .read-receipt {
    color: rgba(255, 255, 255, 0.6);
}

.read-receipt.read {
    color: #7fd9ff;
}

.chat-page .videos {
    position: relative;
    margin-bottom: 14px;
}

.chat-page .videos .btn-danger {
    position: absolute;
    bottom: 10px;
    right: 10px;
}

/* ---------- Post-registration photo step ---------- */

.photo-step {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    background:
        radial-gradient(circle at 50% 0%, var(--brand-glow-soft), transparent 55%),
        var(--bg);
}

.photo-step-card {
    width: 100%;
    max-width: 380px;
    text-align: center;
}

.photo-step-title {
    font-family: 'Nunito', system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: var(--text-2xl);
    font-weight: 800;
    letter-spacing: -0.02em;
    color: var(--text-strong);
    margin: 0 0 var(--space-2);
    text-wrap: balance;
}

.photo-step-body {
    margin: 0 0 var(--space-5);
    color: var(--muted);
    line-height: 1.55;
    text-wrap: pretty;
}

/* The target is deliberately large and circular — it previews exactly the
   crop the avatar will use, so what you see here is what everyone else sees. */
.photo-drop {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    width: 180px;
    height: 180px;
    margin: 0 auto var(--space-4);
    border-radius: 50%;
    border: 2px dashed var(--border);
    background: var(--card);
    color: var(--muted);
    cursor: pointer;
    overflow: hidden;
    transition: border-color 0.15s ease, color 0.15s ease;
}

.photo-drop:hover {
    border-color: var(--accent);
    color: var(--accent-readable);
}

.photo-drop-preview {
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 0;
}

/* Once a file is picked the preview image fills the circle, so the dashed
   ring and caption have nothing left to sit on top of. */
.photo-drop:has(img) {
    border-style: solid;
    border-color: var(--accent);
    position: relative;
}

.photo-drop img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.photo-drop-label {
    font-size: var(--text-base);
    font-weight: 600;
    max-width: 140px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.photo-drop:has(img) .photo-drop-label {
    /* A filename over the picture only obscures what you came here to check. */
    display: none;
}

.photo-step-submit {
    width: 100%;
    padding: 14px;
    font-size: var(--text-lg);
    font-weight: 700;
    background: var(--brand-gradient);
}

.photo-step-submit:disabled {
    background: var(--card-alt);
    color: var(--muted);
    cursor: not-allowed;
    filter: none;
}

.photo-step-skip {
    display: inline-block;
    margin-top: var(--space-4);
    padding: var(--space-2) var(--space-4);
    color: var(--muted);
    font-size: var(--text-md);
    text-decoration: none;
}

.photo-step-skip:hover {
    color: var(--text-strong);
}

.photo-step-note {
    margin: var(--space-4) 0 0;
    font-size: var(--text-sm);
    line-height: 1.5;
}

/* ---------- "Add a photo" nudge ---------- */

.photo-nudge {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-4);
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.photo-nudge-icon {
    display: flex;
    color: var(--accent-readable);
    flex-shrink: 0;
}

.photo-nudge-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1;
    min-width: 0;
    font-size: var(--text-base);
    line-height: 1.4;
}

.photo-nudge-close {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: none;
    color: var(--muted);
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
}

.photo-nudge-close:hover {
    background: var(--card-alt);
    color: var(--text-strong);
}

@media (max-width: 480px) {
    /* Four items in one row leave the message about 160px, so it wraps to
       five lines. Laid out as an explicit two-row grid instead: icon, text
       and close on top, the button under the text. flex-wrap was tried first
       and scattered them across four rows. */
    .photo-nudge {
        display: grid;
        grid-template-columns: auto 1fr auto;
        grid-template-areas:
            "icon text  close"
            ".    button button";
        gap: var(--space-2) var(--space-3);
        align-items: center;
    }

    .photo-nudge-icon {
        grid-area: icon;
        align-self: start;
        margin-top: 2px;
    }

    .photo-nudge-text {
        grid-area: text;
    }

    .photo-nudge-close {
        grid-area: close;
        align-self: start;
    }

    .photo-nudge .btn {
        grid-area: button;
        justify-self: start;
    }
}

/* ---------- Profile prompts ---------- */

.prompt-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

/* The question is the small label and the answer is the content — the other
   way round reads as a form, not as something a person wrote. */
.prompt-card {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: var(--space-3) var(--space-4);
    background: var(--well);
    border-radius: var(--radius);
    border-left: 3px solid var(--accent);
}

.prompt-question {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.prompt-answer {
    font-family: 'Nunito', system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--text-strong);
    line-height: 1.4;
    white-space: pre-line;
}

/* ---- Editing ---- */

.prompt-edit {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3);
    margin-bottom: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.prompt-edit-question {
    font-size: var(--text-sm);
    font-weight: 700;
    color: var(--accent-readable);
}

.prompt-edit textarea {
    resize: vertical;
    font-family: inherit;
    line-height: 1.45;
}

.prompt-edit-actions {
    display: flex;
    gap: var(--space-2);
}

/* ---------- Ban appeals ---------- */

/* Reason and expiry as a label/value pair — the two things a banned person
   was never told before this page existed. */
.appeal-details {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 4px var(--space-3);
    text-align: left;
    margin-bottom: var(--space-4);
}

.appeal-body {
    min-height: 110px;
    resize: vertical;
    font-family: inherit;
    line-height: 1.5;
}

.appeal-card .appeal-meta {
    font-size: var(--text-sm);
    line-height: 1.5;
    margin: 0 0 var(--space-2);
}

.appeal-body-text {
    white-space: pre-line;
    line-height: 1.55;
    padding: var(--space-3);
    background: var(--well);
    border-radius: var(--radius-sm);
    margin: 0 0 var(--space-3);
}

.appeal-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: center;
}

.appeal-actions .pill-input {
    flex: 1 1 180px;
    min-width: 0;
}

/* ---------- Admin activity feed ---------- */

.activity-feed {
    list-style: none;
    padding: 0;
    margin: 0 0 var(--space-3);
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.activity-row {
    display: flex;
    gap: var(--space-3);
    padding: 7px 0;
    border-bottom: 1px solid var(--border);
    font-size: var(--text-base);
    line-height: 1.45;
}

.activity-row:last-child {
    border-bottom: none;
}

.activity-when {
    flex-shrink: 0;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

.activity-text {
    min-width: 0;
}

/* ---------- Who likes you ---------- */

.likes-upsell {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4);
    margin-bottom: var(--space-4);
    background:
        radial-gradient(circle at 0% 0%, var(--brand-glow-soft), transparent 60%),
        var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

.likes-upsell-count {
    flex-shrink: 0;
    min-width: 48px;
    height: 48px;
    padding: 0 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-pill);
    background: var(--brand-gradient-deep);
    color: #fff;
    font-family: 'Nunito', system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: var(--text-xl);
    font-weight: 800;
}

.likes-upsell-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1;
    min-width: 0;
    font-size: var(--text-base);
    line-height: 1.4;
}

.likes-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
    gap: var(--space-3);
    list-style: none;
    padding: 0;
    margin: 0;
}

.likes-tile a {
    display: flex;
    flex-direction: column;
    gap: 4px;
    color: inherit;
    text-decoration: none;
}

.likes-tile-photo {
    position: relative;
    display: block;
    aspect-ratio: 1;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--card-alt);
}

.likes-tile-photo .avatar,
.likes-tile-photo .avatar-placeholder {
    width: 100% !important;
    height: 100% !important;
    border-radius: 0;
    object-fit: cover;
    font-size: var(--text-2xl);
}

.likes-tile-photo .dot {
    position: absolute;
    right: 6px;
    bottom: 6px;
    border: 2px solid var(--card);
}

.likes-tile-name {
    font-size: var(--text-base);
    font-weight: 600;
    color: var(--text-strong);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.likes-tile-when {
    font-size: var(--text-xs);
    color: var(--muted);
}

/* Locked: the shape of who is waiting, without who they are. Blur alone can
   be undone from the markup, so the names and timestamps are simply never
   rendered for a free account — this only softens what is already public. */
.likes-grid.is-locked .likes-tile-photo {
    filter: blur(14px) saturate(0.8);
}

.likes-grid.is-locked .likes-tile a {
    pointer-events: auto;
}

/* A count on a "More" menu entry, so a waiting like is visible while the
   menu is shut. */
.nav-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    margin-left: 6px;
    padding: 0 5px;
    border-radius: var(--radius-pill);
    background: var(--brand-gradient-deep);
    color: #fff;
    font-size: var(--text-xs);
    font-weight: 700;
}

/* display:inline-flex above beats the browser's own [hidden]{display:none},
   so hiding the badge by attribute needs saying explicitly. The likes count
   never needed this — it is only emitted when non-zero — but the messages
   badge is toggled live from JS. */
.nav-count[hidden] {
    display: none;
}

/* ---------- Bets ---------- */

/* Like the reveal card, a bet carries its own frame and buttons, so it sits
   on the page rather than inside a message bubble. */
.chat-log .msg.msg-card {
    background: none;
    border: none;
    padding: 0;
    max-width: 300px;
    width: fit-content;
}

.chat-log .msg.msg-card.me .msg-time,
.chat-log .msg.msg-card.me .read-receipt {
    color: rgba(var(--overlay-rgb), 0.55);
}

.bet-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
}

.bet-head {
    font-weight: 700;
    color: var(--text-strong);
    font-size: var(--text-md);
}

.bet-stake {
    font-size: var(--text-md);
    color: var(--accent-readable);
    font-weight: 600;
    line-height: 1.35;
}

.bet-call,
.bet-waiting {
    font-size: var(--text-sm);
    color: var(--muted);
}

.bet-actions {
    display: flex;
    gap: var(--space-2);
    margin-top: var(--space-2);
}

.bet-actions .btn {
    flex: 1;
}

/* The throw. Runs once, between the call and the result — without it the
   outcome simply appears, which reads as the app deciding rather than
   something being tossed. */
.bet-throw {
    align-self: center;
    margin: var(--space-3) 0;
    font-size: 40px;
    line-height: 1;
    animation: bet-toss 1.2s cubic-bezier(0.3, 0, 0.4, 1);
}

@keyframes bet-toss {
    0%   { transform: translateY(0) rotate(0deg) scale(1); }
    35%  { transform: translateY(-26px) rotate(360deg) scale(1.15); }
    70%  { transform: translateY(-8px) rotate(760deg) scale(1.05); }
    100% { transform: translateY(0) rotate(1080deg) scale(1); }
}

/* A die tumbles rather than spinning flat. */
.bet-throw.dice {
    animation-name: bet-tumble;
}

@keyframes bet-tumble {
    0%   { transform: translateY(0) rotate(0deg); }
    30%  { transform: translateY(-24px) rotate(-140deg); }
    60%  { transform: translateY(-6px) rotate(-290deg); }
    100% { transform: translateY(0) rotate(-360deg); }
}

@media (prefers-reduced-motion: reduce) {
    /* Still takes its moment, so the result doesn't jump out instantly, but
       nothing spins. The script's animationend handler fires either way. */
    .bet-throw {
        animation: bet-fade 0.5s ease-out;
    }
    @keyframes bet-fade {
        from { opacity: 0.4; }
        to   { opacity: 1; }
    }
}

.bet-decline {
    align-self: flex-start;
    margin-top: var(--space-1);
}

.bet-result {
    margin-top: var(--space-2);
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--text-strong);
}

.bet-verdict {
    font-family: 'Nunito', system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: var(--text-xl);
    font-weight: 800;
    letter-spacing: -0.02em;
}

.bet-verdict.won { color: var(--success-text); }
.bet-verdict.lost { color: var(--danger-text); }

.bet-owes {
    font-size: var(--text-sm);
    color: var(--muted);
    line-height: 1.4;
}

.bet-honour {
    margin-top: var(--space-2);
    background: var(--brand-gradient);
}

/* ---- Proposing ---- */

.bet-field-label {
    margin: var(--space-4) 0 var(--space-2);
    font-size: var(--text-sm);
    font-weight: 700;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.bet-option {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 11px var(--space-3);
    margin-bottom: var(--space-1);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    cursor: pointer;
    font-size: var(--text-md);
    line-height: 1.35;
}

.bet-option:hover {
    border-color: var(--accent);
}

/* :has, so the whole row reads as selected rather than just the small dot. */
.bet-option:has(input:checked) {
    border-color: var(--accent);
    background: var(--country-chip-bg);
}

.bet-option input {
    accent-color: var(--accent);
    flex-shrink: 0;
}

.bet-stake-text {
    width: 100%;
    margin-top: var(--space-2);
}

.bet-dice-rule {
    margin: var(--space-3) 0 0;
    font-size: var(--text-sm);
}

/* ---------- Slash commands ---------- */

/* Sits directly above the compose bar, anchored to it — the list has to read
   as belonging to the box being typed in, not float somewhere on the page. */
.command-popup {
    position: absolute;
    left: 8px;
    right: 8px;
    bottom: calc(100% + 6px);
    z-index: 40;
    max-height: 240px;
    overflow-y: auto;
    padding: var(--space-1);
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
}

.command-popup[hidden] {
    display: none;
}

/* The compose bar is the popup's positioning context. */
.chat-compose {
    position: relative;
}

.cmd-item {
    display: flex;
    flex-direction: column;
    gap: 1px;
    width: 100%;
    padding: 9px 12px;
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    text-align: left;
    cursor: pointer;
    color: var(--text);
}

.cmd-item:hover,
.cmd-item.is-active {
    background: var(--card-alt);
}

.cmd-name {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: var(--text-base);
    font-weight: 600;
    color: var(--accent-readable);
}

.cmd-desc {
    font-size: var(--text-sm);
    color: var(--muted);
    line-height: 1.4;
}

/* The help panel is the same rows, just not floating. */
.commands-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    max-height: 55vh;
    overflow-y: auto;
    margin: var(--space-3) 0;
}

/* ---------- Long-press action sheet ---------- */

/* Holding a conversation row opens the sheet, so the browser's own long-press
   behaviours — text selection and the iOS link callout — have to be off, or
   they fire on top of it. */
.person-row[data-chat-id] {
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
}

/* Slides up from the bottom edge on a phone, which is where a thumb already
   is after holding a row; on a wide screen it centres instead, where a
   bottom-anchored panel would be a long way from the pointer. */
.action-sheet {
    position: fixed;
    inset: 0;
    z-index: 80;
    display: flex;
    align-items: flex-end;
    justify-content: center;
}

.action-sheet[hidden] {
    display: none;
}

.action-sheet-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
}

.action-sheet-panel {
    position: relative;
    width: 100%;
    max-width: 420px;
    margin: var(--space-2);
    padding: var(--space-2);
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    animation: sheet-rise 0.18s ease-out;
}

@keyframes sheet-rise {
    from { transform: translateY(12px); opacity: 0; }
    to   { transform: translateY(0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .action-sheet-panel { animation: none; }
}

.action-sheet-title {
    margin: 0;
    padding: var(--space-2) var(--space-3) var(--space-3);
    font-size: var(--text-sm);
    font-weight: 700;
    color: var(--muted);
    text-align: center;
    border-bottom: 1px solid var(--border);
    /* A long username must not stretch the sheet past the screen. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.action-sheet-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    padding: 14px var(--space-3);
    background: none;
    border: none;
    border-radius: var(--radius);
    color: var(--text);
    font-size: var(--text-md);
    text-align: left;
    cursor: pointer;
}

.action-sheet-item:hover {
    background: var(--card-alt);
}

.action-sheet-item .icon {
    color: var(--muted);
    flex-shrink: 0;
}

.action-sheet-item.is-danger,
.action-sheet-item.is-danger .icon {
    color: var(--danger-text);
}

.action-sheet-item.is-cancel {
    justify-content: center;
    margin-top: var(--space-1);
    color: var(--muted);
    font-weight: 600;
    border-top: 1px solid var(--border);
    border-radius: 0 0 var(--radius) var(--radius);
}

/* Destructive entries in the "⋮" dropdown read the same way as in the sheet. */
.menu-dropdown button.is-danger {
    color: var(--danger-text);
}

/* ---------- Chat header menu ---------- */

.menu-wrap {
    position: relative;
}

.menu-dropdown {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    background: var(--card-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    min-width: 180px;
    display: flex;
    flex-direction: column;
    z-index: 20;
    overflow: hidden;
    box-shadow: var(--shadow-lg);
    animation: menu-dropdown-in 0.12s ease;
}

@keyframes menu-dropdown-in {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: translateY(0); }
}

.menu-dropdown button,
.menu-dropdown a {
    background: none;
    border: none;
    color: var(--text);
    text-align: left;
    padding: 10px 14px;
    font-size: var(--text-base);
    cursor: pointer;
    text-decoration: none;
}

.menu-dropdown button:hover,
.menu-dropdown a:hover {
    background: var(--border);
}

.menu-dropdown-divider {
    height: 1px;
    background: var(--border);
    margin: 4px 0;
}

/* ---------- Message long-press context menu ---------- */

.msg-context-menu {
    position: fixed;
    background: var(--card-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    min-width: 170px;
    display: flex;
    flex-direction: column;
    z-index: 50;
    overflow: hidden;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
}

.msg-context-menu button {
    background: none;
    border: none;
    color: var(--text);
    text-align: left;
    padding: 11px 16px;
    font-size: var(--text-base);
    cursor: pointer;
}

.msg-context-menu button:hover {
    background: var(--border);
}

.msg-deleted {
    font-style: italic;
    opacity: 0.7;
    font-size: var(--text-base);
}

.screenshot-card {
    font-style: italic;
    font-size: var(--text-base);
    color: var(--muted);
}

.msg-context-menu-reactions {
    display: flex;
    gap: 2px;
    padding: 8px 10px;
    border-bottom: 1px solid var(--border);
}

.msg-context-menu-reactions button {
    background: none;
    border: none;
    font-size: 18px;
    padding: 4px 6px;
    border-radius: var(--radius-xs);
    cursor: pointer;
}

.msg-context-menu-reactions button:hover {
    background: var(--border);
}

.msg-reactions {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 4px;
}

.reaction-pill {
    background: rgba(var(--overlay-rgb), 0.08);
    border: 1px solid rgba(var(--overlay-rgb), 0.12);
    border-radius: var(--radius);
    padding: 1px 7px;
    font-size: var(--text-sm);
    line-height: 1.6;
    cursor: pointer;
    color: inherit;
}

.reaction-pill.mine {
    background: rgba(122, 108, 255, 0.25);
    border-color: rgba(122, 108, 255, 0.5);
}

.msg-edited {
    font-size: var(--text-xs);
    color: rgba(var(--overlay-rgb), 0.45);
    font-style: italic;
}

.chat-log .msg.me .msg-edited {
    color: rgba(255, 255, 255, 0.45);
}

.pinned-banner {
    display: flex;
    align-items: center;
    gap: 8px;
    background: rgba(var(--overlay-rgb), 0.06);
    border: 1px solid rgba(var(--overlay-rgb), 0.1);
    border-radius: var(--radius-sm);
    padding: 8px 10px;
    margin-bottom: 8px;
    font-size: var(--text-base);
    cursor: pointer;
}

.pinned-banner-text {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pinned-banner-unpin {
    background: none;
    border: none;
    color: rgba(var(--overlay-rgb), 0.6);
    font-size: var(--text-base);
    cursor: pointer;
    padding: 2px 6px;
}

.pinned-banner-unpin:hover {
    color: var(--text-strong);
}

/* ---------- Attach (image) button ---------- */

/* Borderless and round: three bordered boxes in a row read as three more
   input fields sitting next to the real one. As bare icons they recede
   until you look for them, which is what an attachment control should do. */
.attach-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    cursor: pointer;
    border-radius: 50%;
    background: transparent;
    border: none;
    color: var(--muted);
    padding: 0;
    transition: background 0.15s ease, color 0.15s ease;
}

.attach-btn:hover {
    background: var(--card-alt);
    color: var(--accent-readable);
}

/* ---------- Voice messages ---------- */

.voice-recorder {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    background: var(--well);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.voice-recorder-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #ff4d4d;
    animation: voice-pulse 1.2s ease-in-out infinite;
}

@keyframes voice-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.3; }
}

#voiceRecorderTime {
    flex: 1;
    font-variant-numeric: tabular-nums;
    color: var(--muted);
}

/* A voice message drops the chat bubble entirely: the audio player already
   has its own chrome, and wrapping it in the accent gradient looked like a
   box inside a box. Alignment (left/right) still comes from .me/.them.
   The timestamp sits under the player rather than inside a bubble, and
   follows the message's own side — right for yours, left for theirs — so
   it reads as belonging to that message instead of floating off in the
   far corner the way a always-right timestamp did. */
/* Keeps its bubble now, like every other message — the previous rules
   stripped it because the browser's <audio> widget wouldn't fit in one. */
.chat-log .msg.msg-voice {
    padding: 8px 10px;
    max-width: 100%;
    width: fit-content;
}

.chat-log .msg.msg-voice .msg-meta {
    margin-top: 4px;
}

.chat-log .msg.msg-voice.them .msg-meta {
    justify-content: flex-start;
}

.voice-msg {
    display: flex;
    align-items: center;
    gap: 10px;
    /* Wide enough for 28 bars to read as a waveform rather than a dotted
       line, narrow enough to stay inside the bubble's 78% cap. */
    width: 210px;
    max-width: 100%;
}

.voice-msg audio {
    display: none;
}

.voice-play {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    flex-shrink: 0;
    padding: 0;
    border: none;
    border-radius: 50%;
    cursor: pointer;
    transition: transform 0.12s ease;
}

.voice-play:active {
    transform: scale(0.92);
}

.voice-wave {
    display: flex;
    align-items: center;
    gap: 2px;
    flex: 1;
    min-width: 0;
    height: 26px;
    cursor: pointer;
}

.voice-bar {
    flex: 1;
    min-width: 2px;
    border-radius: var(--radius-pill);
    /* Bars start dim and light up as the clip passes them, so progress is
       readable at a glance without a separate progress track. */
    transition: background-color 0.1s linear;
}

.voice-time {
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    flex-shrink: 0;
}

/* ---- Default: theme-aware, accent for the played part. Covers incoming
       bubbles and the standalone voice intro on a profile card alike. ---- */

.voice-play {
    background: var(--brand-gradient-deep);
    color: #fff;
}

.voice-play:hover {
    filter: brightness(1.1);
}

/* The unplayed track sat at 0.25 alpha — 2.3:1 on the bubble, faint enough
   that the waveform trailed off into nothing. 0.38 clears the 3:1 floor for
   a graphic while staying clearly behind the played bars, which differ in
   hue as well as brightness. */
.voice-bar {
    background: rgba(var(--overlay-rgb), 0.38);
}

.voice-bar.played {
    background: var(--accent-readable);
}

.voice-time {
    color: var(--muted);
}

/* ---- Your own bubble: everything sits on the brand gradient, so the
       controls go white rather than fighting it with a second colour. ---- */

.msg.me .voice-play {
    background: rgba(255, 255, 255, 0.22);
    color: #fff;
}

.msg.me .voice-play:hover {
    background: rgba(255, 255, 255, 0.32);
    filter: none;
}

.msg.me .voice-bar {
    background: rgba(255, 255, 255, 0.55);
}

.msg.me .voice-bar.played {
    background: #fff;
}

.msg.me .voice-time {
    color: rgba(255, 255, 255, 0.75);
}

/* The profile intro isn't in a bubble, so it gets a surface of its own. */
.voice-intro-player .voice-msg {
    width: 260px;
    padding: 8px 12px;
    background: var(--well);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
}

/* ---------- Image messages with blur-until-reveal ---------- */

.img-wrap {
    position: relative;
    max-width: 220px;
    border-radius: var(--radius-sm);
    overflow: hidden;
    line-height: 0;
}

.img-wrap img {
    width: 100%;
    display: block;
    border-radius: var(--radius-sm);
}

.img-wrap.blurred img {
    filter: blur(18px);
}

.img-reveal {
    display: none;
    position: absolute;
    inset: 0;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.35);
    color: #fff;
    font-size: var(--text-base);
    cursor: pointer;
}

.img-wrap.blurred .img-reveal {
    display: flex;
}

/* ---------- View-once photos ---------- */

/* Unopened or already-viewed, on the recipient's side. A <button>, not an
   .img-wrap — the whole point is that no image bytes exist in the DOM until
   it's tapped, so this has nothing in common with the blurred-reveal card
   above beyond living in the same bubble slot. */
.view-once-card {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 160px;
    padding: 12px 16px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--well);
    color: inherit;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    text-align: left;
}

.view-once-card:not(.is-viewed):active {
    filter: brightness(1.08);
}

.view-once-card.is-viewed {
    cursor: default;
    opacity: 0.6;
    font-weight: 400;
}

.view-once-card-icon {
    font-size: 20px;
    line-height: 1;
}

/* The moment it's actually open, it's just a photo again. */
.img-wrap.view-once-open {
    box-shadow: 0 0 0 1.5px var(--accent);
}

/* On the sender's own bubble, marking a send as view-once and, once the
   other side has opened it, saying so. */
.view-once-sent-badge {
    position: absolute;
    top: 6px;
    left: 6px;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-size: var(--text-xs);
    font-weight: 700;
    backdrop-filter: blur(2px);
}

/* ---------- Photo send choice modal ---------- */

.photo-send-preview {
    display: flex;
    justify-content: center;
    margin: 4px 0 14px;
}

.photo-send-preview img {
    max-width: 100%;
    max-height: 260px;
    border-radius: var(--radius-sm);
    object-fit: contain;
}

.photo-send-choices {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.photo-send-choice {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--well);
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.photo-send-choice:hover,
.photo-send-choice:focus-visible {
    border-color: var(--accent);
}

.photo-send-choice-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    flex-shrink: 0;
    font-size: 20px;
}

.photo-send-choice-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* ---------- Media gallery ---------- */

.media-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
    gap: 10px;
}

.media-item {
    position: relative;
    aspect-ratio: 1;
    border-radius: var(--radius-sm);
    overflow: hidden;
    cursor: pointer;
    line-height: 0;
}

.media-item img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.media-item.blurred img {
    filter: blur(14px);
}

.media-item .img-reveal {
    display: none;
    position: absolute;
    inset: 0;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.3);
    color: #fff;
    font-size: var(--text-xl);
}

.media-item.blurred .img-reveal {
    display: flex;
}

/* ---------- Report modal ---------- */

.modal {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 50;
}

.modal-box {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 20px;
    width: 100%;
    max-width: 360px;
    box-shadow: var(--shadow-lg);
}

.modal-box h3 {
    margin-top: 0;
    font-size: var(--text-lg);
}

.modal-box textarea {
    width: 100%;
    min-height: 90px;
    padding: 10px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--well);
    color: var(--text);
    font-family: inherit;
    resize: vertical;
}

.modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 14px;
}

.match-modal-box {
    text-align: center;
}

.match-modal-emoji {
    font-size: 48px;
    margin-bottom: 8px;
    animation: match-pop 0.4s ease;
}

@keyframes match-pop {
    from {
        transform: scale(0.4);
        opacity: 0;
    }
    to {
        transform: scale(1);
        opacity: 1;
    }
}

.ban-modal-label {
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: var(--text-base);
    color: var(--muted);
    margin-bottom: 12px;
}

.ban-modal-label select {
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--well);
    color: var(--text);
    font-size: var(--text-md);
}

.ban-modal-label textarea {
    min-height: 60px;
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--well);
    color: var(--text);
    font-family: inherit;
    resize: vertical;
}

/* ---------- Incoming call notification ---------- */

.incoming-call-modal {
    position: fixed;
    top: 16px;
    right: 16px;
    z-index: 100;
}

.incoming-call-box {
    background: var(--card);
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    padding: 14px 16px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}

.incoming-call-box p {
    margin: 0 0 10px;
    font-size: var(--text-md);
}

.incoming-call-actions {
    display: flex;
    gap: 8px;
}

/* ---------- Swipe / discovery page (standalone, distinct from Browse) ---------- */

.swipe-stage {
    max-width: 420px;
    margin: 0 auto;
    padding: 20px 16px 32px;
    display: flex;
    flex-direction: column;
    background:
        radial-gradient(circle at 20% 10%, rgba(108, 99, 255, 0.14), transparent 40%),
        radial-gradient(circle at 80% 90%, rgba(253, 38, 122, 0.10), transparent 40%);

    /* Height of everything that isn't this stage: the sticky top nav plus,
       on mobile, the body padding that clears the fixed bottom nav. This
       used to be a flat 130px, which ignored the bottom 64px and left the
       page scrollable by exactly that much on a screen where nothing
       actually overflowed. */
    --stage-chrome: 130px;
    min-height: calc(100vh - var(--stage-chrome));
    /* dvh tracks the real visible area as a phone's address bar hides and
       shows; 100vh assumes the bar is always hidden and overshoots. */
    min-height: calc(100dvh - var(--stage-chrome));

    /* Everything the card has to share the screen with: --stage-chrome, this
       stage's own 20+32 padding, the 33px filter bar and its 8px gap, and the
       58px action buttons above their 24px gap. Consumed by .swipe-card. */
    --card-chrome: calc(var(--stage-chrome) + 175px);
}

.swipe-topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8px;
    flex-wrap: wrap;
    gap: 8px;
}

.swipe-filters {
    display: flex;
    gap: 6px;
    min-width: 0;
}

.swipe-filters .swipe-country-pill {
    min-width: 0;
    max-width: 40vw;
}

.swipe-heading {
    font-size: var(--text-xl);
    margin: 0;
}

.swipe-country-pill {
    padding: 6px 14px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border);
    background: var(--card);
    color: var(--text);
    font-size: var(--text-base);
}

.swipe-stage-cards {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* No floor here: a 400px minimum plus a fixed-ratio card pushed the
       action buttons off a short screen. The card sizes itself from
       whatever height is actually left instead. */
    min-height: 0;
}

/* Photo-first card: the picture fills the whole surface and the details sit
   on top of it, rather than a small avatar centred in a padded box. This is
   what makes a swipe deck read as a deck — the person is the card, not the
   contents of one. */
.swipe-card {
    position: relative;
    width: 100%;
    /* Two caps, whichever bites first. 340px is the design width; the calc
       is the width at which the card's 4:5 height still leaves room for the
       action buttons. Without it a 425px-tall card pushes the buttons off a
       640px screen. It has to be expressed as a *width* rather than a
       max-height because every child here is absolutely positioned — the
       card has no content height of its own to fall back on, so anything
       that leaves the height unresolved collapses it to zero. */
    max-width: min(340px, calc((100dvh - var(--card-chrome)) * 0.8));
    /* Portrait, close to the 4:5 most phone photos are shot in. */
    aspect-ratio: 4 / 5;
    background: var(--card-alt);
    border: none;
    border-radius: 28px;
    padding: 0;
    overflow: hidden;
    display: block;
    box-shadow: var(--shadow-lg);
    touch-action: none;
    cursor: grab;
    user-select: none;
}

.swipe-card:active {
    cursor: grabbing;
}

/* The photo layer: fills the card edge to edge regardless of the source
   image's own aspect ratio. Applies to the initials placeholder too, so a
   user with no photo still gets a full-bleed card instead of a small circle
   floating in an empty box. */
.swipe-avatar {
    position: absolute;
    inset: 0;
    line-height: 0;
    pointer-events: none;
}

.swipe-avatar .avatar,
.swipe-avatar .avatar-placeholder {
    width: 100% !important;
    height: 100% !important;
    border-radius: 0;
    object-fit: cover;
    font-size: 64px;
}

/* Scrim so white text stays readable over an unpredictable photo — a
   bright image would otherwise swallow the name entirely. */
.swipe-card::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(to top,
        rgba(0, 0, 0, 0.85) 0%,
        rgba(0, 0, 0, 0.55) 22%,
        rgba(0, 0, 0, 0) 55%);
    pointer-events: none;
}

.swipe-details {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-4) var(--space-4) var(--space-5);
    pointer-events: none;
}

.swipe-crown {
    font-size: var(--text-xl);
}

.swipe-label {
    font-family: 'Nunito', system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: 26px;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.15;
    color: #fff;
    pointer-events: none;
}

.swipe-status {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    color: rgba(255, 255, 255, 0.85);
    font-size: var(--text-md);
    pointer-events: none;
}

/* Tags sit on the photo, so they need their own translucent chip rather
   than the page-surface colours the browse list uses. */
.swipe-details .interest-tags {
    margin-top: var(--space-1);
}

.swipe-details .interest-tag {
    background: rgba(255, 255, 255, 0.18);
    border-color: rgba(255, 255, 255, 0.28);
    color: #fff;
}

.swipe-details .person-country {
    background: rgba(255, 255, 255, 0.2);
    color: #fff;
}

.swipe-fabs {
    display: flex;
    gap: 20px;
    justify-content: center;
    margin-top: 24px;
}

.swipe-fab {
    width: 58px;
    height: 58px;
    border-radius: 50%;
    border: none;
    font-size: var(--text-xl);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: var(--shadow-lg);
    transition: transform 0.12s ease;
    background: var(--card);
}

.swipe-fab:hover {
    transform: translateY(-2px);
}

.swipe-fab:active {
    transform: translateY(0);
}

.swipe-fab-skip {
    color: var(--muted);
    border: 1px solid var(--border);
}

.swipe-fab-message {
    background: linear-gradient(135deg, var(--accent), var(--accent-dark));
    color: #fff;
    width: 48px;
    height: 48px;
    align-self: center;
    font-size: 18px;
}

.swipe-fab-like {
    background: var(--brand-gradient);
    color: #fff;
}

/* "N people like you" teaser above the deck. */
.likes-you {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    margin: 0 auto 12px;
    padding: 7px 16px;
    width: fit-content;
    max-width: 100%;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border);
    background: var(--card);
    color: var(--text);
    font-size: var(--text-base);
    text-align: center;
}

.likes-you-count {
    font-weight: 800;
    font-size: var(--text-lg);
    color: var(--brand-pink-text);
    font-variant-numeric: tabular-nums;
}

.swipe-fab-rewind {
    background: linear-gradient(135deg, #ffb020, #ff8a00);
    color: #fff;
    width: 48px;
    height: 48px;
    align-self: center;
    font-size: 18px;
}

.swipe-fab:disabled {
    opacity: 0.35;
    cursor: not-allowed;
    box-shadow: none;
}

.swipe-fab:disabled:hover {
    transform: none;
}

/* ---------- Avatars ---------- */

.avatar {
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    display: block;
}

.avatar-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-weight: 600;
    letter-spacing: 0.5px;
}

/* ---------- Mobile ---------- */

/* iOS Safari zooms the page in on focus for any text input under 16px;
   these classes previously set a smaller size, so re-declare it here
   (same specificity, later in the cascade wins) rather than touching
   every individual rule above. */
.tinder-input,
.auth-card input,
.auth-card select,
.form-field,
.chat-form input,
.pill-select,
.pill-input,
.swipe-country-pill {
    font-size: var(--text-lg);
}

/* ---------- Bottom nav (mobile app-style tab bar) ---------- */

.bottom-nav {
    display: none;
}

@media (max-width: 640px) {
    .topnav {
        padding: 10px 12px;
        gap: 10px;
        /* Two deliberate rows instead of an accidental flex-wrap break —
           row/order/flex-basis tricks left an inconsistent gap between the
           lines depending on content height. Stacking two real row
           containers is predictable regardless of how tall either is. */
        flex-direction: column;
        align-items: stretch;
    }

    .topnav-row {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
    }

    /* "Daugiau" now sits at the far-left edge of its row (row 2) instead of
       mid-row like on desktop — the dropdown's default right:0 anchor
       extends it leftward from there, pushing it off-screen entirely.
       Anchor from the left edge instead so it opens rightward, into the
       viewport. */
    .topnav-more .menu-dropdown {
        left: 0;
        right: auto;
    }

    .topnav-right .muted {
        display: none;
    }

    .topnav-links {
        display: none;
    }

    /* Mobile reserves more chrome than desktop: the top nav wraps to two
       rows, and the body gains bottom padding to clear the fixed bottom
       nav. Both have to come out of the stage's height or the page
       scrolls by exactly that difference with nothing overflowing. */
    .swipe-stage {
        --stage-chrome: 181px;
    }

    body {
        padding-bottom: 64px;
    }

    .bottom-nav {
        display: flex;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 30;
        background: var(--card);
        border-top: 1px solid var(--border);
        box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.35);
        padding-bottom: env(safe-area-inset-bottom, 0);
    }

    .bottom-nav a {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: var(--space-1);
        padding: 10px var(--space-1) 8px;
        color: var(--muted);
        text-decoration: none;
        /* Comfortably past the 44px minimum touch target. */
        min-height: 60px;
        transition: color 0.15s ease;
    }

    .bottom-nav-icon {
        display: block;
        position: relative;
        width: 26px;
        height: 26px;
        line-height: 0;
    }

    .bottom-nav-icon svg {
        display: block;
        width: 100%;
        height: 100%;
    }

    /* Unread count, sat on the icon's top-right corner the way an app icon
       badge does — on the icon rather than beside the label, so it can't
       reflow the label or push the tab out of alignment with its neighbours. */
    .bottom-nav-badge {
        position: absolute;
        top: -5px;
        left: 60%;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 17px;
        height: 17px;
        padding: 0 4px;
        border-radius: var(--radius-pill);
        background: var(--brand-gradient-deep);
        color: #fff;
        font-size: 10px;
        font-weight: 700;
        line-height: 1;
        /* Separates the badge from whatever icon stroke sits under it. */
        box-shadow: 0 0 0 2px var(--bg);
    }

    .bottom-nav-badge[hidden] {
        display: none;
    }

    .bottom-nav-label {
        font-size: var(--text-sm);
        font-weight: 600;
        letter-spacing: -0.01em;
    }

    /* The current tab is the one thing here that has to read without a
       second look, so it changes three ways at once: brand colour, heavier
       label, and a filled icon instead of the outline one (swapped in PHP).
       Colour alone would be too weak a signal at this size. */
    .bottom-nav a.active {
        /* Not raw --accent: as text on the bar it measured 4.0:1, i.e. worse
           than the inactive items it's meant to stand out from. */
        color: var(--accent-readable);
    }

    .bottom-nav a.active .bottom-nav-label {
        font-weight: 800;
    }

    .btn-small {
        padding: 9px 14px;
        font-size: var(--text-md);
    }

    .attach-btn,
    #voiceBtn {
        width: 44px;
        height: 44px;
        font-size: var(--text-xl);
        flex-shrink: 0;
    }

    .chat-form button[type="submit"] {
        flex-shrink: 0;
    }

    #menuBtn {
        width: 40px;
        height: 40px;
        font-size: 18px;
    }

    .back-link {
        padding: 6px 0;
        font-size: var(--text-md);
    }

    .chat-header {
        gap: 8px;
    }

    .chat-header h2 {
        font-size: var(--text-lg);
    }

    /* Three attachment buttons plus a send button left the text field only
       161px on a 375px screen — about twenty visible characters. Shaving
       4px off each control and halving the gaps buys the field back ~40px
       without making anything smaller than a 34px tap target. */
    .chat-form {
        gap: 4px;
    }

    .attach-btn {
        width: 34px;
        height: 34px;
    }

    .chat-send {
        width: 40px;
        height: 40px;
    }

    .chat-form input,
    .chat-form button,
    #voiceBtn,
    .attach-btn {
        min-height: 44px;
    }

    /* On the chat page, the message-compose bar takes over the very bottom
       of the screen like a real messaging app, replacing the generic app
       tab bar (which would otherwise sit redundantly beneath it). */
    /* Both of these hide the bottom nav, so the padding that reserves room
       for it is dead space — it made the welcome screen scroll by exactly
       64px with nothing below the fold. */
    body.chat-mode,
    body.no-nav {
        padding-bottom: 0;
    }

    body.chat-mode .bottom-nav {
        display: none;
    }

    body.chat-mode .chat-page {
        padding-bottom: var(--compose-height, 76px);
        padding-left: 0;
        padding-right: 0;
    }

    /* The page itself is edge-to-edge now; individual rows get their own
       small inset so buttons/text aren't flush against the screen edge. */
    body.chat-mode .chat-header,
    body.chat-mode .videos,
    body.chat-mode #callStatus,
    body.chat-mode .alert,
    body.chat-mode #typingIndicator,
    body.chat-mode .chat-log.big {
        padding-left: 8px;
        padding-right: 8px;
    }

    body.chat-mode .chat-compose {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 30;
        background: var(--bg);
        border-top: 1px solid var(--border);
        padding: 8px 8px calc(8px + env(safe-area-inset-bottom, 0));
        box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.35);
    }

    body.chat-mode .chat-compose .chat-form {
        margin: 0;
    }

    /* The chat log was a fixed 400px box (fine on desktop) that left a big
       dead zone below it on a phone screen. In chat-mode it now stretches
       to fill the real space between the header and the fixed compose bar. */
    body.chat-mode {
        display: flex;
        flex-direction: column;
        min-height: 100dvh;
    }

    body.chat-mode .chat-page {
        flex: 1;
        display: flex;
        flex-direction: column;
        min-height: 0;
        /* .page normally centers itself with auto side margins under its
           max-width; inside this flex column that fights align-items:stretch
           and shrinks the page to content width instead of filling it. */
        margin-left: 0;
        margin-right: 0;
    }

    body.chat-mode .chat-log.big {
        flex: 1;
        min-height: 0;
        height: auto;
    }

    .page {
        padding: 14px 12px;
    }

    .modal-box {
        width: 100%;
        max-width: none;
        margin: 0 12px;
    }

    .view-profile-actions .btn,
    .view-profile-actions .btn-small {
        min-height: 40px;
    }
}

/* ---------- Theme-aware color utilities ----------
   Replace one-off inline colors scattered across admin/profile pages —
   keeps danger/success/emphasis text readable in both the dark and light
   theme instead of a literal hex baked into a template that only looked
   right in dark mode. Placed last so they win ties against earlier
   same-specificity component rules (e.g. .premium-badge's own color). */
.text-danger { color: var(--danger-text); }
.text-success { color: var(--success-text); }
.text-emphasis { color: var(--text-strong); }

.settings-card-danger {
    border-color: var(--danger-border);
}

/* ---------- Group chats ----------
   Reuses .chat-page, .chat-log, .msg and .chat-form wholesale — a group is
   the same conversation surface with more than one person in it, and giving
   it its own bubble styling would have meant two sets to keep in step. Only
   what a two-person chat has no need for is new here. */

/* Stands in for the avatar a group hasn't got. */
.group-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--brand-gradient-deep);
    color: #fff;
    flex-shrink: 0;
}

.group-avatar-lg {
    width: 60px;
    height: 60px;
}

/* The header block is a <button> here (it opens the member list) rather than
   the <a> a private chat uses, so it needs the button chrome removed. */
.group-header-identity {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.group-topic {
    margin: 0;
    padding: 6px var(--space-4);
    font-size: var(--text-sm);
    color: var(--muted);
    border-bottom: 1px solid var(--border);
}

/* Said once at the top of every group: the rest of the app hands out an
   anonymous label and this room cannot, so it should not be a surprise
   discovered halfway down the conversation. */
.group-name-notice {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 0;
    padding: 8px var(--space-4);
    font-size: var(--text-xs);
    color: var(--muted);
    background: var(--brand-glow-soft);
}

/* Who wrote it — a line a two-person chat never needs. */
.msg-author {
    display: block;
    font-size: var(--text-xs);
    font-weight: 700;
    color: var(--accent-readable);
    text-decoration: none;
    margin-bottom: 2px;
}

/* "X joined" — centred and quiet, so the room's events don't read as
   somebody's message. */
.msg-system {
    align-self: center;
    max-width: 80%;
    margin: 6px auto;
    padding: 4px 12px;
    border-radius: 999px;
    background: var(--card);
    border: 1px solid var(--border);
    font-size: var(--text-xs);
    color: var(--muted);
    text-align: center;
}

/* The badge sat flush against the name in the JS-built member list, which
   unlike the server-rendered one has no incidental whitespace between them.
   Spacing it here fixes both paths rather than depending on markup layout. */
.person-label .primary-badge {
    margin-left: 6px;
}

.group-create-form {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: var(--space-2);
}

.group-create-form .pill-input {
    flex: 1 1 200px;
    min-width: 0;
}

.group-member-list {
    max-height: 50vh;
    overflow-y: auto;
}

/* The code is meant to be read aloud or typed by hand, so it gets room to
   breathe rather than sitting inline at body size. */
.group-invite-code {
    margin: var(--space-2) 0 0;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: var(--text-lg);
    letter-spacing: 0.18em;
    text-align: center;
    color: var(--text-strong);
}

/* ---------- Login history (profile → security) ---------- */

.login-history {
    list-style: none;
    margin: var(--space-2) 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.login-entry {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--well);
}

/* An address seen only once is the one worth a second look, so it is the
   one thing here that changes colour. */
.login-entry.is-new {
    border-color: var(--accent);
}

.login-entry-main {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1;
    min-width: 0;
}

.login-entry-when {
    font-size: var(--text-md);
    font-weight: 600;
}

.login-entry-meta {
    font-size: var(--text-sm);
    color: var(--muted);
    /* The address must be able to wrap on a narrow phone rather than
       propping the row open past the viewport. */
    overflow-wrap: anywhere;
}

.login-entry-meta code {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

.login-entry-badge {
    padding: 2px 8px;
    border-radius: var(--radius-pill);
    background: var(--brand-gradient-deep);
    color: #fff;
    font-size: var(--text-xs);
    font-weight: 700;
    white-space: nowrap;
}

.login-entry-flag {
    font-size: var(--text-sm);
    color: var(--muted);
    white-space: nowrap;
}

.login-entry-report {
    margin: 0;
}

.login-history-note {
    margin-top: var(--space-2);
}

/* A row of status filters on an admin queue. Its own class rather than one
   borrowed from Browse: those filters collapse behind a button on a phone,
   which is wrong for three short tabs that should always be visible. */
.status-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: var(--space-3);
}

/* A chat photo opens fullscreen, so the pointer should say so. Only once it
   is past the blur — while blurred the reveal overlay owns the click, and a
   zoom cursor there would promise the wrong thing. */
.img-wrap:not(.blurred) img.is-zoomable {
    cursor: zoom-in;
}

.media-item:not(.blurred) img {
    cursor: zoom-in;
}

/* ---------- Self-requested verification ---------- */

.verify-status {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    font-weight: 600;
    margin: 0;
}

.verify-status-approved {
    background: var(--success-bg);
    color: var(--success-bg-text);
}

.verify-status-pending {
    background: var(--well);
    color: var(--muted);
}

.verify-status-rejected {
    background: var(--danger-bg);
    color: var(--danger-bg-text);
    display: block;
    margin-bottom: var(--space-2);
}

/* ---------- Verification review queue (admin) ---------- */

.verify-review-photos {
    display: flex;
    gap: 16px;
    flex-wrap: wrap;
    margin: var(--space-2) 0;
}

.verify-review-photo {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.verify-review-photo img {
    width: 160px;
    height: 200px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
}

.verify-review-label {
    font-size: var(--text-xs);
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.verify-review-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: var(--space-2);
}

.verify-reject-note {
    flex: 1 1 200px;
    min-width: 0;
}

/* ---------- Shared interests ---------- */

.shared-interests-line {
    font-size: var(--text-sm);
    color: var(--muted);
    margin: 0 0 var(--space-2);
}

/* Picks out, within the full tag list, the handful the viewer also has —
   same pill shape, just enough weight added that they read as highlighted
   rather than as a whole second visual language. */
.interest-tag-shared {
    background: var(--brand-gradient-deep);
    color: #fff;
    font-weight: 700;
}

.person-shared-interests {
    display: inline-flex;
    align-items: center;
    gap: 3px;
}

/* ---------- Floating refresh button ----------
   Sits above the bottom nav on a phone and in the corner on desktop, where
   there is no nav bar to clear. Only rendered once the page is scrolled far
   enough that the browser's own pull-to-refresh is out of reach. */

.floating-refresh {
    position: fixed;
    right: 16px;
    /* Clears the 60px tab bar plus the home-indicator inset. */
    bottom: calc(76px + env(safe-area-inset-bottom, 0px));
    z-index: 25;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 46px;
    height: 46px;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--card);
    color: var(--text);
    box-shadow: var(--shadow-lg);
    cursor: pointer;
    /* The icon is the whole control; no padding to fight with. */
    padding: 0;
}

.floating-refresh[hidden] {
    display: none;
}

.floating-refresh svg {
    width: 22px;
    height: 22px;
}

.floating-refresh:active {
    transform: scale(0.94);
}

/* Turns while the reload is in flight, so a slow connection still shows the
   tap registered. */
.floating-refresh.is-spinning svg {
    animation: floating-refresh-spin 0.7s linear infinite;
}

@keyframes floating-refresh-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .floating-refresh.is-spinning svg {
        animation: none;
    }
    .floating-refresh:active {
        transform: none;
    }
}

/* No tab bar above 640px, so the button can sit lower. */
@media (min-width: 641px) {
    .floating-refresh {
        bottom: 24px;
    }
}

/* The "≈" that marks an IP-derived location as approximate. Small and muted:
   it qualifies the place name without competing with it, and the same glyph
   appears in the hint text above the list so its meaning is stated once. */
.location-approx {
    /* Sits in front of the place name — "≈ Vilnius" reads as "roughly
       Vilnius", where a trailing "Vilnius≈" just looks like a typo. */
    margin-right: 3px;
    font-size: 0.9em;
    color: var(--muted);
    cursor: help;
}

/* The network an address belongs to, under the sign-in line. A plain ISP name
   is quiet context; a datacenter is the thing worth noticing, so only that
   case takes colour. */
.login-entry-network {
    display: block;
    margin-top: 2px;
    font-size: var(--text-xs);
    color: var(--muted);
}

.login-entry-network.is-hosting {
    color: var(--danger-bg-text);
    background: var(--danger-bg);
    display: inline-block;
    padding: 2px 8px;
    border-radius: var(--radius-pill);
    font-weight: 600;
}

/* The little icon in front of a preview — "Photo", "Voice message", a pinned
   line, a reply quote. Sits on the text baseline rather than the line box, so
   a row with an icon keeps the same height as one without and the chat list
   doesn't jitter between rows. */
.preview-icon {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    vertical-align: -2px;
    margin-right: 5px;
    opacity: 0.75;
}

/* A wave is drawn as the message, not as a label, so it takes the accent
   colour and drops the preview icon's dimming. */
.wave-card .icon {
    width: 56px;
    height: 56px;
    opacity: 1;
    margin: 0;
    color: var(--accent);
    stroke-width: 1.4;
}

.wave-card.super .icon {
    color: #ffd250;
}

/* The shield next to a datacenter sign-in, sized to sit inside the pill. */
.login-entry-network .icon,
.text-danger .icon {
    width: 12px;
    height: 12px;
    vertical-align: -1px;
    margin-right: 4px;
}

/* ---------- Admin server-health page ---------- */

/* The headline verdict. Colour carries it, because the point of this card is
   to be readable at a glance from a phone at an awkward hour. */
.sys-summary.is-good {
    border-left: 4px solid var(--success, #2f9e5f);
}

.sys-summary.is-bad {
    border-left: 4px solid var(--danger, #d9534f);
}

.sys-checks {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.sys-check {
    display: grid;
    grid-template-columns: 14px 1fr auto;
    align-items: center;
    gap: 10px;
    padding: 9px 0;
    border-bottom: 1px solid var(--border);
}

.sys-check:last-child {
    border-bottom: 0;
}

.sys-check-dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    justify-self: center;
    background: var(--muted);
}

.sys-check.is-ok .sys-check-dot {
    background: var(--success, #2f9e5f);
}

.sys-check.is-bad .sys-check-dot {
    background: var(--danger, #d9534f);
}

.sys-check-name {
    font-weight: 600;
}

.sys-check-detail {
    color: var(--muted);
    font-size: var(--text-sm);
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.sys-actions {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-top: 12px;
}

.sys-action {
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.sys-action .btn {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: 7px;
}

.sys-action-hint {
    color: var(--muted);
    font-size: var(--text-xs);
}

/* Command output and log lines: monospace, and allowed to scroll sideways
   rather than push the page wider than the screen. */
.sys-output {
    margin: 8px 0 0;
    padding: 10px 12px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    font-size: var(--text-xs);
    line-height: 1.55;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    overflow-x: auto;
    max-height: 340px;
    overflow-y: auto;
}

/* ---------- House ads ---------- */

/* An ad sits in the same list as people, so it must not read as one. The
   dashed edge, the tint and the label are all doing that one job. */
.ad-row {
    display: grid;
    grid-template-columns: 88px 1fr auto;
    grid-template-areas:
        "label label label"
        "media info cta";
    align-items: center;
    gap: 6px 12px;
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    background: color-mix(in srgb, var(--card) 88%, var(--accent) 12%);
    padding: 10px 12px;
}

.ad-label {
    grid-area: label;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--muted);
    font-weight: 700;
}

.ad-media {
    grid-area: media;
    width: 88px;
    height: 88px;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg);
    display: block;
}

.ad-media img,
.ad-media video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.ad-info {
    grid-area: info;
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

.ad-title {
    font-weight: 600;
    color: var(--text-strong);
}

.ad-body {
    font-size: var(--text-sm);
    color: var(--muted);
}

.ad-cta {
    grid-area: cta;
    white-space: nowrap;
}

@media (max-width: 420px) {
    /* The button drops under the text rather than squeezing both. */
    .ad-row {
        grid-template-columns: 72px 1fr;
        grid-template-areas:
            "label label"
            "media info"
            "cta   cta";
    }

    .ad-media {
        width: 72px;
        height: 72px;
    }

    .ad-cta {
        justify-self: stretch;
        text-align: center;
    }
}

/* ---------- Ads admin ---------- */

.ad-admin-row {
    display: grid;
    grid-template-columns: 76px 1fr auto;
    gap: 12px;
    align-items: center;
    padding: 12px 0;
    border-bottom: 1px solid var(--border);
}

.ad-admin-row:last-child {
    border-bottom: 0;
}

.ad-admin-media {
    width: 76px;
    height: 76px;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg);
}

.ad-admin-media img,
.ad-admin-media video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.ad-admin-info {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.ad-admin-stats {
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
}

.ad-state {
    text-transform: uppercase;
    font-size: var(--text-xs);
    letter-spacing: 0.05em;
}

.ad-state.is-live {
    color: var(--success, #2f9e5f);
    font-weight: 700;
}

.ad-admin-actions {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.ads-row-fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 12px;
}

@media (max-width: 520px) {
    .ad-admin-row {
        grid-template-columns: 60px 1fr;
    }

    .ad-admin-media {
        width: 60px;
        height: 60px;
    }

    .ad-admin-actions {
        grid-column: 1 / -1;
        flex-direction: row;
    }
}

/* Reserve-a-role form on the admin users page. Wraps rather than squeezing:
   an email field needs room and this sits below a wide table. */
.invite-form {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 12px 0;
}

.invite-form input[type="email"] {
    flex: 2 1 220px;
}

.invite-form input[type="text"] {
    flex: 1 1 160px;
}

.invite-form select {
    flex: 0 1 150px;
}

/* ---------- Stacked form field ----------
   Label above, control below, hint under that. The admin forms had been
   writing <label><span>text</span><input></label> with no layout behind it,
   so the label and the input ran together on one line and every field ran
   into the next. */
.field {
    display: flex;
    flex-direction: column;
    gap: 5px;
    margin-bottom: 16px;
}

.field > span {
    font-weight: 600;
    font-size: var(--text-sm);
}

.field input[type="text"],
.field input[type="url"],
.field input[type="email"],
.field input[type="number"],
.field input[type="datetime-local"],
.field input[type="file"],
.field select,
.field textarea {
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
    padding: 10px 12px;
    /* 16px keeps iOS from zooming the page in when the field is focused. */
    font-size: 16px;
    color: var(--text);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.field small,
.field-hint {
    font-size: var(--text-xs);
    color: var(--muted);
    line-height: 1.5;
}

/* A checkbox belongs beside its label, not above it. */
.checkbox-row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin-bottom: 16px;
    cursor: pointer;
}

.checkbox-row input[type="checkbox"] {
    width: 20px;
    height: 20px;
    margin: 1px 0 0;
    flex-shrink: 0;
}

/* The optional settings, folded away until asked for. */
.field-advanced {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 12px 14px;
    margin-bottom: 16px;
}

.field-advanced > summary {
    cursor: pointer;
    font-weight: 600;
    font-size: var(--text-sm);
}

.field-advanced[open] > summary {
    margin-bottom: 14px;
}
