﻿/* ==========================================================================
   LooksApp - Production Chat App Styles
   WhatsApp-inspired layout with blue color scheme
   ========================================================================== */

/* Portrait lock: JS adds .portrait-locked class and sets --lock-w / --lock-h */
html.portrait-locked {
    overflow: hidden;
}
html.portrait-locked body {
    position: fixed;
    transform: rotate(-90deg);
    transform-origin: top left;
    width: var(--lock-w);
    height: var(--lock-h);
    top: var(--lock-w);
    left: 0;
    overflow: hidden;
    min-height: 0;
}
html.portrait-locked .app-container {
    min-height: var(--lock-h) !important;
    height: var(--lock-h) !important;
    width: var(--lock-w) !important;
}
/* Camera video: undo body rotation so it shows natural landscape view */
html.portrait-locked .camera-overlay {
    width: var(--lock-h);
    height: var(--lock-w);
    top: 50%; left: 50%;
    transform: translate(-50%, -50%) rotate(90deg);
}

/* color-scheme set per theme so native UI (scrollbars) matches */
[data-theme="dark"] { color-scheme: dark; }

/* ==========================================================================
   1. CSS VARIABLES - Design Tokens
   ========================================================================== */

:root {
    /* Light Theme Colors */
    --bg-primary: #FFFFFF;
    /* The Look spots read as recessed panels, so a photo that does not fill
       one sits on a surface rather than on the page itself */
    --look-spot-bg: #DFE7EE;
    /* What separates a photo from the spot holding it. Not a drawn line but
       the shadow the picture would cast if it were lying in there: darkest
       against its own edge and gone a few pixels in. The hairline is left
       underneath it at a tenth of its old weight, so the edge stays definite
       where a picture is pale enough to lose it. */
    /* The grey the camera switch is drawn in, at about the weight of the logo
       in the middle of a spot: there to be read once and then looked past. */
    --mode-grey: color-mix(in srgb, var(--text-secondary) 40%, transparent);
    --photo-shade: rgba(0, 0, 0, 0.20);
    /* The edge round a wide spot.

       Heavier than --photo-line, which is seven per cent black: that reads
       against the empty spot's pale ground and vanishes over a photograph,
       which is most of what a spot holds. */
    --spot-edge: rgba(0, 0, 0, 0.20);
    --photo-line: rgba(0, 0, 0, 0.07);
    --bg-chat: #E8EFF5;
    --bg-chat-pattern: rgba(0, 0, 0, 0.04);
    --bg-header: #1F5C94;
    --bg-header-text: #FFFFFF;
    --bubble-outgoing: #D6E8FF;
    --bubble-incoming: #FFFFFF;
    --text-primary: #111B21;
    --text-secondary: #667781;
    --text-timestamp: rgba(0, 0, 0, 0.45);
    --text-header: #FFFFFF;
    --divider: #E9EDEF;
    --badge-unread: #3073AB;
    --badge-text: #FFFFFF;
    --accent: #1F5C94;
    --selfie-accent: #1F5C94;
    --interest-accent: #396E3E;
    /* Sampled from the sticker art so the controls read as the same drawing */
    --sticker-ink: #201E1B;
    --sticker-red: #F2665C;
    --sticker-green: #4BB04A;
    --sticker-cream: #F2EFE8;
    /* The green a spot's button wears when it is ready or listening, and now
       also the edge of a listening spot and of a card that reached us because
       we were listening. One home, at the root, so an edge and the button
       under it cannot drift apart. */
    --action-green: #1F7A4E;
    /* The same green at brightness(1.5): the countdown filling a button, and
       the light walking round a listening spot. Brighter than the edge it
       comes off, which is what light does. */
    --action-bright: #2EB775;
    /* How full the wallet is. The green is the one the ready button wears,
       so a healthy balance and a spot ready to call are the same colour.
       All three carry white text at 4.7:1 or better. */
    --credit-green: #1F7A4E;
    --credit-orange: #A85F0A;
    --credit-red: #A82D1E;
    --check-sent: rgba(0, 0, 0, 0.4);
    --check-read: #53BDEB;
    --typing-text: #3073AB;
    --input-bg: #FFFFFF;
    --input-border: #E9EDEF;
    --dropdown-bg: #FFFFFF;
    --dropdown-shadow: rgba(0, 0, 0, 0.15);
    --ripple: rgba(0, 0, 0, 0.05);

    /* Spacing System (8px base) */
    --space-xs: 4px;
    --space-sm: 8px;
    --space-md: 12px;
    --space-lg: 16px;
    --space-xl: 20px;
    --space-2xl: 24px;

    /* Typography - WhatsApp uses Segoe UI / SF Pro / Roboto */
    --font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif;
    --font-xs: 11px;
    --font-sm: 14px;
    --font-md: 15px;
    --font-lg: 17px;
    --font-xl: 19px;
    --font-weight-normal: 450;
    --font-weight-medium: 550;
    --font-weight-bold: 650;
    --line-height-tight: 1.2;
    --line-height-normal: 1.35;

    /* Component Dimensions */
    --header-height: 56px;
    --chatlist-item-height: 72px;
    --avatar-size-list: 54px;
    --avatar-size-chat: 40px;
    --avatar-size-header: 40px;
    --input-bar-height: 60px;
    --bubble-max-width: 75%;
    --bubble-radius: 10px;
    --unread-badge-size: 20px;
    --min-touch-target: 44px;

    /* Transitions */
    --transition-fast: 150ms ease;
    --transition-normal: 300ms ease-out;

    /* Spring animation timing */
    --photo-spring: linear(
        0, 0.006, 0.025 2.8%, 0.101 6.1%, 0.539 18.9%, 0.721 25.3%, 0.849 31.5%,
        0.937 38.1%, 0.968 41.8%, 0.991 45.7%, 1.006 50.1%, 1.015 55%, 1.017 63.9%,
        1.001
    );
}

@supports not (animation-timing-function: linear(0, 1)) {
    :root {
        --photo-spring: cubic-bezier(0.32, 0.72, 0, 1);
    }
}

/* Dark Theme */
[data-theme="dark"] {
    --bg-primary: #111B21;
    --look-spot-bg: #22313B;
    --mode-grey: color-mix(in srgb, var(--text-secondary) 46%, transparent);
    --photo-shade: rgba(0, 0, 0, 0.42);
    /* The edge round a wide spot, in the dark. */
    --spot-edge: rgba(255, 255, 255, 0.28);
    --photo-line: rgba(255, 255, 255, 0.10);
    --bg-chat: #0B141A;
    --bg-chat-pattern: rgba(255, 255, 255, 0.03);
    --bg-header: #1E3A5F;
    --bg-header-text: #E9EDEF;
    --bubble-outgoing: #1E3A5F;
    --bubble-incoming: #202C33;
    --text-primary: #E9EDEF;
    --text-secondary: #8696A0;
    --text-timestamp: rgba(255, 255, 255, 0.5);
    --text-header: #E9EDEF;
    --divider: #222D34;
    --badge-unread: #5C9AC7;
    --badge-text: #FFFFFF;
    --accent: #5C9AC7;
    --selfie-accent: #5C9AC7;
    --interest-accent: #72AD77;
    --check-sent: rgba(255, 255, 255, 0.5);
    --check-read: #64B5F6;
    --typing-text: #5C9AC7;
    --input-bg: #202C33;
    --input-border: #2A3942;
    --dropdown-bg: #233138;
    --dropdown-shadow: rgba(0, 0, 0, 0.3);
    --ripple: rgba(255, 255, 255, 0.05);
}

/* Dark mode wallpaper override */
[data-theme="dark"] .chat-messages {
    background-image: url("data:image/svg+xml,%3Csvg width='250' height='250' viewBox='0 0 250 250' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23FFFFFF' stroke-opacity='0.06' stroke-width='1.2'%3E%3C!-- Circles --%3E%3Ccircle cx='25' cy='35' r='14'/%3E%3Ccircle cx='180' cy='25' r='10'/%3E%3Ccircle cx='95' cy='100' r='18'/%3E%3Ccircle cx='210' cy='120' r='12'/%3E%3Ccircle cx='50' cy='190' r='16'/%3E%3Ccircle cx='160' cy='200' r='11'/%3E%3Ccircle cx='130' cy='60' r='8'/%3E%3C!-- Squares --%3E%3Crect x='120' y='150' width='20' height='20' rx='3'/%3E%3Crect x='15' y='120' width='16' height='16' rx='2'/%3E%3Crect x='200' cy='170' width='22' height='22' rx='3'/%3E%3Crect x='70' y='220' width='14' height='14' rx='2'/%3E%3Crect x='230' y='50' width='12' height='12' rx='2'/%3E%3C!-- Triangles --%3E%3Cpath d='M65 25 L78 50 L52 50 Z'/%3E%3Cpath d='M220 75 L238 105 L202 105 Z'/%3E%3Cpath d='M30 80 L42 102 L18 102 Z'/%3E%3Cpath d='M140 135 L158 165 L122 165 Z'/%3E%3Cpath d='M105 215 L118 240 L92 240 Z'/%3E%3Cpath d='M185 230 L195 248 L175 248 Z'/%3E%3C!-- Diamonds --%3E%3Cpath d='M170 90 L182 108 L170 126 L158 108 Z'/%3E%3Cpath d='M85 65 L95 78 L85 91 L75 78 Z'/%3E%3Cpath d='M225 195 L235 208 L225 221 L215 208 Z'/%3E%3Cpath d='M40' y='145 L48 155 L40 165 L32 155 Z'/%3E%3C!-- Spirals --%3E%3Cpath d='M55 140 Q60 135 65 140 Q70 145 65 150 Q58 155 52 148 Q48 142 55 138'/%3E%3Cpath d='M195 145 Q202 138 210 145 Q218 152 210 160 Q200 168 192 158 Q186 150 195 143'/%3E%3Cpath d='M110 20 Q115 15 120 20 Q125 25 120 30 Q113 35 108 28 Q104 22 110 18'/%3E%3C!-- Wiggles --%3E%3Cpath d='M10 50 Q20 45 30 50 Q40 55 50 50 Q60 45 70 50'/%3E%3Cpath d='M150 5 Q160 0 170 5 Q180 10 190 5 Q200 0 210 5'/%3E%3Cpath d='M5 200 Q15 195 25 200 Q35 205 45 200'/%3E%3Cpath d='M240 130 Q245 125 250 130 Q245 135 240 130'/%3E%3C!-- Small hexagons --%3E%3Cpath d='M40 230 L48 224 L56 230 L56 242 L48 248 L40 242 Z'/%3E%3Cpath d='M145 85 L153 79 L161 85 L161 97 L153 103 L145 97 Z'/%3E%3C!-- Plus signs --%3E%3Cpath d='M235 25 L235 35 M230 30 L240 30'/%3E%3Cpath d='M75 175 L75 185 M70 180 L80 180'/%3E%3Cpath d='M15 165 L15 175 M10 170 L20 170'/%3E%3C/g%3E%3C/svg%3E");
}

/* ==========================================================================
   2. BASE RESET & TYPOGRAPHY
   ========================================================================== */

*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

/* Scrollbar styling */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.25); }
[data-theme="dark"] * {
    scrollbar-color: rgba(255,255,255,0.2) transparent;
}
[data-theme="dark"] ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); }
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.35); }

html, body {
    height: 100%;
    width: 100%;
    overflow: clip;
    overscroll-behavior: none;
}

body {
    font-family: var(--font-family);
    font-size: var(--font-md);
    line-height: var(--line-height-normal);
    color: var(--text-primary);
    background-color: var(--bg-primary);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    -webkit-tap-highlight-color: transparent;
    min-height: 100vh;
    display: flex;
    justify-content: center;
}

/* ==========================================================================
   3. APP CONTAINER
   ========================================================================== */

.app-container {
    height: 100%;
    width: 100%;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    position: relative;
    overflow: clip;
}

/* Web browsers only: use dvh so the input bar stays visible regardless of the
   browser URL bar or on-screen keyboard. Capacitor app uses the vh fallback. */
@supports (height: 100dvh) {
    html.web-browser,
    html.web-browser body {
        min-height: 100dvh;
    }
    html.web-browser .app-container {
        min-height: 100dvh;
        height: 100dvh;
    }
}

/* ---- The desktop frame ----
   A phone, at the size a phone actually is. The frame used to take its width
   from the window's height, which meant a tall monitor drew a 500px-wide
   "phone" and every fixed size in the app — a 38px button, an 18px word, a
   65px chat button — came out looking small against it. Capped at the width
   the app is designed and tested at instead, so what a laptop shows is what a
   phone shows, and the three-up bench (390px panes) agrees with both.

   Height follows from the same cap, keeping the 450:930 shape: whichever of
   the two constraints binds, the other gives way, so the ratio holds. */
@media (min-width: 500px) {
    :root { --frame-w: 390px; }

    body {
        padding: 20px 0;
        background: #8a9aa8;
        align-items: center;
    }
    [data-theme="dark"] body {
        background: #0a0a0a;
    }
    .app-container {
        height: min(calc(100vh - 40px), calc(var(--frame-w) * 930 / 450));
        min-height: auto;
        width: min(calc((100vh - 40px) * 450 / 930), var(--frame-w));
        max-width: 100%;
        border-radius: 24px;
        overflow: hidden;
        box-shadow: 0 0 40px rgba(0,0,0,0.2);
        border: 1px solid rgba(0,0,0,0.1);
        transform: translateZ(0); /* makes fixed children position relative to this container */
        background-color: var(--bg-primary);
    }
    /* The dvh rule further up is written as `html.web-browser .app-container`,
       which outranks the block above and was handing the frame the whole
       window height — a 390px phone 945px tall. Matched here, later in the
       file, so a browser gets the same shape as everything else. */
    html.web-browser .app-container {
        height: min(calc(100vh - 40px), calc(var(--frame-w) * 930 / 450));
        min-height: auto;
    }

    [data-theme="dark"] .app-container {
        box-shadow: 0 0 60px rgba(0,0,0,0.5);
        border: 1px solid rgba(255,255,255,0.06);
    }

    /* Constrain overlay screens to the same phone frame */
    .signin-screen,
    .loading-screen {
        width: min(calc((100vh - 40px) * 450 / 930), var(--frame-w));
        max-width: 100%;
        height: min(calc(100vh - 40px), calc(var(--frame-w) * 930 / 450));
        top: 0; left: 0; right: 0; bottom: 0;
        margin: auto;
        border-radius: 24px;
        overflow: hidden;
    }
}

/* ==========================================================================
   4. HEADER
   ========================================================================== */

.header {
    height: var(--header-height);
    background-color: var(--bg-header);
    color: var(--text-header);
    display: flex;
    align-items: center;
    padding: 0 var(--space-sm);
    flex-shrink: 0;
    position: relative;
    z-index: 100;
}

.header-title {
    font-size: var(--font-xl);
    font-weight: var(--font-weight-bold);
    flex: 1;
    padding-left: var(--space-sm);
}

/* Tab header — two equal halves */
.tab-half {
    flex: 1;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    position: relative;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
}

/* Dimming lives on the icon, not the tab: a parent opacity would fade the
   unread badge along with it, and an unread count must always read clearly. */
.tab-half > .tab-star-icon,
.tab-half > .tab-logo-icon {
    opacity: 0.45;
    transition: opacity 200ms ease;
}

.tab-half.active > .tab-star-icon,
.tab-half.active > .tab-logo-icon {
    opacity: 1;
}

.tab-star-icon {
    width: 30px;
    height: 30px;
    color: var(--text-header);
    transition: color 200ms ease;
}

.tab-logo-icon {
    width: 26px;
    height: 26px;
    filter: brightness(0) invert(1);
    transition: filter 200ms ease;
}

/* Total unread, pinned to the chats tab icon. The tab itself dims when
   inactive, so the badge lifts its own opacity back to full. */
.tab-unread-badge {
    position: absolute;
    top: 6px;
    left: 50%;
    margin-left: 6px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    box-sizing: border-box;
    border-radius: 9px;
    background: #E5493A;
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    opacity: 1;
    pointer-events: none;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}


/* Underline per tab half — 72% width centered, slightly inside header, soft color */
.tab-half-underline {
    position: absolute;
    bottom: 4px;
    left: 14%;
    width: 72%;
    height: 2.5px;
    background: rgba(255, 255, 255, 0.5);
    border-radius: 2px;
    opacity: 0;
    transition: opacity 300ms ease;
}

.tab-half.active .tab-half-underline {
    opacity: 1;
}


.header-back {
    width: var(--min-touch-target);
    height: var(--min-touch-target);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    border-radius: 50%;
    transition: background-color var(--transition-fast);
}

.header-back:active {
    background-color: rgba(255, 255, 255, 0.1);
}

.header-back svg {
    width: 24px;
    height: 24px;
    fill: var(--text-header);
}

/* Chat header with avatar */
.header-chat-info {
    display: flex;
    align-items: center;
    flex: 1;
    min-width: 0;
    margin-left: var(--space-sm);
}

.header-avatar {
    width: var(--avatar-size-header);
    height: var(--avatar-size-header);
    border-radius: 50%;
    object-fit: cover;
    margin-right: var(--space-md);
    flex-shrink: 0;
    background-color: rgba(255, 255, 255, 0.2);
}

/* Square avatar for Look chats */
.header-avatar.square-avatar {
    border-radius: 8px;
    border: 2px solid var(--selfie-accent, #4A8CB8);
}

/* Green border for interest Look avatar */
.header-avatar.square-avatar.interest-avatar {
    border-color: var(--interest-accent, #5A9E60);
}

.header-text {
    min-width: 0;
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

.header-name-row {
    display: flex;
    align-items: center;
    gap: 6px;
    max-width: 100%;
}

.header-name {
    font-size: var(--font-lg);
    font-weight: var(--font-weight-bold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--text-header);
    line-height: 1.2;
    flex: 1;
    min-width: 0;
}

.header-name-input {
    font-size: var(--font-lg);
    font-weight: var(--font-weight-bold);
    color: var(--text-header);
    background: var(--surface);
    border: 2px solid var(--primary);
    border-radius: 6px;
    padding: 4px 8px;
    flex: 1;
    min-width: 0;
    outline: none;
    line-height: 1.2;
}

.header-edit-btn {
    background: none;
    border: none;
    padding: 4px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.7;
    transition: opacity 0.2s;
    flex-shrink: 0;
}

.header-edit-btn:hover {
    opacity: 1;
}

.header-edit-btn svg {
    width: 20px;
    height: 20px;
    fill: var(--text-header);
}

.header-status {
    font-size: var(--font-xs);
    opacity: 0.9;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--text-header);
    line-height: 1.2;
}

.header-status.typing {
    opacity: 1;
}

.header-status.processing {
    opacity: 1;
    color: var(--accent);
    animation: pulse 2s ease-in-out infinite;
}

.header-status.error {
    opacity: 1;
    color: #f44336;
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.6; }
}

/* Menu button */
.header-menu-btn {
    width: var(--min-touch-target);
    height: var(--min-touch-target);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    border-radius: 50%;
    transition: background-color var(--transition-fast);
}

.header-menu-btn:active {
    background-color: rgba(255, 255, 255, 0.1);
}

.header-avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    object-fit: cover;
}

/* ==========================================================================
   5. DROPDOWN MENU
   ========================================================================== */

.dropdown-overlay {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 200;
    background-color: transparent;
}

.dropdown-overlay.active {
    display: block;
}

.dropdown-menu {
    position: absolute;
    top: calc(var(--header-height) - 4px);
    right: 8px;
    background-color: var(--dropdown-bg);
    border-radius: 4px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26), 0 2px 10px rgba(0, 0, 0, 0.16);
    min-width: 200px;
    z-index: 201;
    opacity: 0;
    transform: scale(0.9) translateY(-8px);
    transform-origin: top right;
    transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
    pointer-events: none;
    overflow: hidden;
}

.dropdown-menu.active {
    opacity: 1;
    transform: scale(1) translateY(0);
    pointer-events: auto;
}

.dropdown-item {
    display: flex;
    align-items: center;
    padding: 14px 20px;
    font-size: 15px;
    font-weight: var(--font-weight-normal);
    color: var(--text-primary);
    cursor: pointer;
    transition: background-color 100ms ease;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
}

.dropdown-item:hover {
    background-color: rgba(0, 0, 0, 0.03);
}

[data-theme="dark"] .dropdown-item:hover {
    background-color: rgba(255, 255, 255, 0.05);
}

.dropdown-item:active {
    background-color: var(--ripple);
}

.dropdown-item svg {
    width: 22px;
    height: 22px;
    margin-right: 16px;
    fill: var(--text-secondary);
    flex-shrink: 0;
}

.dropdown-item span {
    flex: 1;
}

.dropdown-divider {
    height: 1px;
    background-color: var(--divider);
    margin: 8px 0;
}

/* ==========================================================================
   6. CHAT LIST VIEW
   ========================================================================== */

.chatlist-view {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: relative;
}

/* Nothing to show yet: a quiet line rather than a blank page. */
.chatlist-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 22vh 24px;
    color: var(--text-secondary, #7C8896);
    text-align: center;
}

.chatlist-empty svg {
    width: 54px;
    height: 54px;
    opacity: 0.28;
}

.chatlist-empty p {
    margin: 0;
    font-size: var(--font-md, 15px);
}

.chatlist-scroll {
    flex: 1;
    min-height: 0;             /* an empty list must not push the spots up */
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    padding-top: 8px;
    padding-bottom: 16px;
}

/* ==========================================================================
   Look Squares — main-page Look spots
   Two stacked squares dividing the space under the header:
   Interest ("Call a Look") on top, Selfie ("Set my Look") below.
   ========================================================================== */

/* The Looks page is nothing but the two spots: full width, and everything
   below the status bar. The profile button floats over the top spot. */
/* The whole page, top to bottom. There is no chat list here: everything to
   do with chats lives behind the chat button, on the messages page. */
.look-squares {
    /* The arrows are placed against this, not against a slide: they reach
       down over the spots, and the strip clips. */
    position: relative;
    flex: 0 0 auto;
    height: 100%;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 10px;
    box-sizing: border-box;
    min-height: 0;
}

/* ---- The page, cut in half ----
   Above: the slides, and the cards that arrive over them. Below: the
   two spots side by side, call on the left, listen on the right. */
.looks-top {
    /* Takes whatever the spots do not. The two halves used to be an even
       split, which on a tall screen left the explainer stretched over empty
       room and on a short one squeezed the spots. The spots have a shape to
       keep; the explainer only has to fit. */
    flex: 1 1 auto;
    position: relative;
    min-height: 0;
    border-radius: 16px;
    overflow: hidden;
    /* What is left when the explainer is put away. It sits under the slides
       the whole time and is only ever seen once they have gone.

       The page's own background, so putting the explainer away leaves nothing
       at all rather than a panel. --look-spot-bg was tried here and reads as a
       grey rectangle on a white page: the spots below are meant to look like
       recesses cut into the page, and an empty half at the top is not a recess,
       it is empty. */
    background: var(--bg-primary);
}

.looks-bottom {
    /* As tall as the spots and their buttons, and no taller. */
    flex: 0 0 auto;
    display: flex;
    gap: 10px;
    min-height: 0;
}

/* The layer cards land in. It covers the whole half, so it must be deaf
   itself — otherwise it swallows every tap and swipe meant for the slides
   underneath. The cards it holds listen for themselves. */
.looks-cards {
    position: absolute;
    inset: 0;
    z-index: 20;
    pointer-events: none;
}

.looks-cards > * { pointer-events: auto; }

/* ---- The slides ----
   Four pages on a track, always up unless a card is over them. */
.look-slides {
    position: absolute;
    inset: 0;
    background: #fff;
    overflow: hidden;
    transition: opacity 240ms ease;
    /* The strip is dragged sideways, so the browser must not claim the
       gesture for a scroll of its own. */
    touch-action: none;
}

/* One row per page, under that page's line, so the whole caption rides with
   the picture it belongs to. Every row says the same thing — which slide
   is up — and they are only ever seen one at a time, but the row
   coming in behind a drag is already in the right place. */
.look-slide-dots {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 10px;
    display: flex;
    justify-content: center;
    gap: 6px;
    pointer-events: none;
    z-index: 2;
}

.look-slide-dots i {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: rgba(28, 38, 48, 0.22);
    transition: background-color 200ms ease;
}

.look-slide-dots i[data-on="1"] { background: rgba(28, 38, 48, 0.62); }

/* Thin white edge, echoing the die-cut outline on the stickers */
/* One half of the bottom: the spot with its button beneath. The button used
   to lie on the picture and take a strip of every Look with it; standing it
   underneath costs the spot 46px of height and gives back all of its width. */
.look-spot {
    flex: 1;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
    position: relative;        /* the credit count stands on the Call button */
}

.look-square {
    /* 2:3, and the shape the picture is uploaded in. The spot is the crop, so
       the two cannot be allowed to disagree: whatever proportion this box has
       is the proportion the AI is handed. Wider than 9:16 because people sit
       down, and a seated person in a tall narrow frame is mostly pavement and
       knees — and because a phone sensor is 3:4, so every step narrower than
       that is sensor thrown away before anything else happens.

       Height leads, being the scarce axis with two spots side by side; the
       width follows and the box centres in what is left over. */
    /* A starting shape only. The exact one is set in script, because the
       ratio has to hold on the inside of the border — that is where the
       picture lives — and because a box that is told its height by flex and
       its width by a ratio has no way to shrink when the width will not fit.
       On a taller phone it simply stopped being 2:3. */
    box-sizing: border-box;
    /* A 2:3 box the width of its column, with no help from anything: the
       height follows the width, so it needs neither a container of known
       height nor a script to have run. Script then trims it — to make the
       ratio exact on the inside of the border, and to stop it eating the
       screen where a column is wide. Wrong-by-a-little beats gone. */
    flex: none;
    width: 100%;
    aspect-ratio: 2 / 3;
    margin-inline: auto;
    position: relative;
    min-width: 0;
    min-height: 0;
    border: 3px solid #fff;
    border-radius: 16px;
    overflow: hidden;
    background: var(--look-spot-bg);
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4);
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
}

[data-theme="dark"] .look-square {
    border-color: rgba(255, 255, 255, 0.34);
}

/* A Look is shown whole. The bracket in the adjust step took the spot's own
   shape, so it normally fills it — but a differently shaped screen would
   otherwise crop what was deliberately framed, and losing that is worse than
   a band of ground at the edge. */
.look-square-img {
    position: absolute;
    /* Right to the edge. It used to stand six pixels inside it, and those six
       pixels were the spot's own colour — a broad blue mount around every
       photograph. A Look is the picture, not a picture on a card, so the
       picture goes to the edge and a hairline says where it stops. Sized
       rather than left to `auto`: an absolutely positioned image reads `auto`
       as its own intrinsic size, not as the box the insets describe, and
       comes out at full resolution clipped to a fragment. */
    /* Edge to edge of the spot's inside, and that is the whole of it: the
       3px border is the white edge, and there is nothing between it and the
       picture.

       It used to stand 2px in as well, which cost twice. The white read as
       5px, not 3 — border plus margin, the spot being painted white behind a
       Look — and, worse, it broke the fit. Script sizes the spot so its
       INSIDE is exactly 2:3. Taking 2px off each side of a 2k x 3k box gives
       (2k-4) x (3k-4), which is not 2:3, so `contain` fitted the picture to
       the width and left (3k-4)-(3k-6) = 2px over: a pixel of white above the
       photograph and a pixel below it. The picture is the spot's own shape,
       so the box it goes in has to be too. */
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    /* Still white behind it, for the moment between the spot filling and the
       file arriving. Nothing of it shows once it has. */
    background: #fff;
    border-radius: 13px;              /* 16 outside, less the 3px border */
    display: none;
}

.look-square.has-look .look-square-img { display: block; }

/* The two pixels the shadow falls on are paper, not the spot's own blue —
   and it stays paper while a called Look shakes itself apart, which is when
   most of the spot is showing through. */
.look-square.has-look,
.look-square.sending,
.look-square.demoing { background: #fff; }

/* Nothing framing the pieces on their way out. The picture's own line and
   the white edge of the spot both stayed put while it shrank away from them,
   so what was left was a Look breaking apart inside a box drawn around
   nothing. Both go for the duration, and the box keeps its size because the
   border is only made invisible, not taken away. */
.look-square.sending { border-color: transparent; }

/* ---- The line round a Look ----
   On the spot rather than on the picture. An inset shadow on an image is
   painted underneath the image itself, so it only ever showed across the
   strips a letterboxed picture left bare — a line round three sides of a
   drawing and none of a photograph. Here it is a layer of its own, over
   whatever the spot is holding, and it closes. */
.look-square::after {
    content: "";
    position: absolute;
    /* Two pixels in, so there is somewhere for the shadow to fall. The spot
       clips at its own edge, so a shadow thrown from the picture's own line
       would have nowhere to go and would end up thrown inward over the
       picture instead — which is what it was doing. */
    inset: 2px;
    border-radius: 11px;
    box-shadow: 0 1px 3px var(--photo-shade),
                0 0 0 1px var(--photo-line);
    pointer-events: none;
    opacity: 0;
    transition: opacity 220ms ease;
    z-index: 4;
}
/* A demonstration only. A real Look now runs to the inside of the border,
   so the white edge is already the line saying where the picture stops and
   this would be a second one drawn 2px in on top of the photograph — which
   is the mount it was put there to remove. The drawings on the slides do
   still stand on white inside the spot, and there it earns its place. */
.look-square.demoing::after { opacity: 1; }

/* Except while it is being thrown away. Written after the rule that turns it
   on rather than before it: the two are as specific as each other, so the
   later one is the one that counts, and earlier it simply lost. */
.look-square.sending::after { opacity: 0; }

/* ---- What the explainer is pointing at ----
   While a slide is being read, the spot it is about shows what would land
   there. Only ever on an empty spot, and it fades rather than snaps, so
   moving through the slides reads as one thing changing rather than two
   pictures being dealt.

   `contain` rather than `cover`: this is a whole look, head to shoes, and
   cover would crop away whichever side is tight. None of it is spare. */
.look-square-demo {
    position: absolute;
    inset: 2px;
    width: calc(100% - 4px);
    height: calc(100% - 4px);
    object-fit: contain;
    /* The drawing stands on white, and so does the panel it sits in: contain
       leaves a margin either side, and grey there was the whole reason the
       white ground went into the file in the first place. White here rather
       than on the spot, which is what used to swallow the border. */
    background: #fff;
    border-radius: 11px;
    opacity: 0;
    transition: opacity 220ms ease;
    pointer-events: none;
}

.look-square.demoing .look-square-demo { opacity: 1; visibility: visible; }

/* Out of the way entirely when it is not the one being explained. Opacity
   alone still renders it, and an image with nothing to show renders the
   broken-picture mark — which is what flashed in the corner of the spot on
   the way off a slide. */
.look-square-demo { visibility: hidden; }

/* The eye steps aside for it: two things offering to fill the same spot */
.look-square.demoing .look-square-shutter { opacity: 0; }
/* A photo lies on paper, not on slate. This was a dark mat for pictures that
   did not fill the spot, from before they were all cut to the spot's own 2:3
   — and what it actually showed was the two pixels around every Look, and the
   whole spot while a called one shook itself apart. Neither wants to be dark. */
.look-square.viewing { background: #fff; }

/* Notice shown inside a spot when a button is pressed too early */
.look-square-hint {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 12px 12px 54px;
    text-align: center;
    font-size: var(--font-sm, 13px);
    line-height: 1.4;
    color: var(--text-secondary);
    pointer-events: none;
}

.look-square-hint.notice {
    color: var(--text-primary);
    font-weight: 600;
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
}

/* ---- The eye ----
   It hands over to the camera. The mark is the tint, with nothing behind it,
   so it shades the spot rather than covering it. */
.look-square-shutter {
    transition: opacity 220ms ease;
    position: absolute;
    left: 50%;
    /* The middle of the spot. It used to sit a little high, which put it
       under the switch that now lives at the top. */
    top: 50%;
    transform: translate(-50%, -50%);
    width: 38%;                /* a square mark, smaller than the eye's box */
    aspect-ratio: 1;
    max-height: 44%;
    border: 0;
    background: none;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 4;
    -webkit-tap-highlight-color: transparent;
}

.look-square.sending .look-square-shutter { display: none; }

/* A spot holding a Look shows the Look and nothing else. The eye that used to
   stand here was a fifth of a shade of ink and disappeared into a photograph
   on its own; the logo does not, and the cross is the way back anyway. */
.look-square.has-look .look-square-shutter {
    opacity: 0;
    pointer-events: none;
}

/* The logo, grey while the spot is empty and gold for half a second when it
   is used. Greyed by filter rather than by a second flat drawing, so what
   flashes is the real artwork — ring, inner circle and embossed G keep their
   own tones and the button still reads as a button.

   All three states carry the same three filter functions in the same order,
   which is what lets the browser interpolate between them instead of
   snapping: a list against `none` is a discrete change. */
.look-shutter-logo {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
    /* Fainter than it was. It is a mark saying what the spot is for, not a
       thing to press — the spot is that — and at its old weight it argued
       with the chat button in the corner. */
    filter: grayscale(1) brightness(1) opacity(0.34);
    transition: filter 320ms ease;
}

/* On an empty spot the mark has only the spot's own colour to work against,
   and in the dark theme that colour is dark. */
[data-theme="dark"] .look-square:not(.viewing) .look-shutter-logo {
    filter: grayscale(1) brightness(1.3) opacity(0.3);
}

/* Gold in a tenth of a second, back to grey over a third: an answer, not a
   state. The theme rule is repeated so it cannot win by being more specific. */
.look-shutter-logo.awake,
[data-theme="dark"] .look-square:not(.viewing) .look-shutter-logo.awake {
    filter: grayscale(0) brightness(1) opacity(1);
    transition-duration: 90ms;
}

.look-square-shutter:active .look-shutter-logo { filter: grayscale(1) brightness(1) opacity(0.75); }

[data-theme="dark"] .look-square:not(.viewing) .look-square-shutter:active .look-shutter-logo {
    filter: grayscale(1) brightness(1.3) opacity(0.7);
}

/* ---- The cross, top left ---- */
.look-square-tools {
    position: absolute;
    top: 8px;
    left: 8px;
    display: flex;
    gap: 6px;
    /* Above everything drawn for looks. The way out of a spot cannot be under
       a shadow, least of all while a countdown is running in it. */
    z-index: 9;
}

.look-square-tools button {
    width: 39px;               /* a cross worth hitting: 15% up from 34 */
    height: 39px;
    padding: 0;
    border: 0;
    border-radius: 11px;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(20, 26, 32, 0.45);
    color: #C9D2DA;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.look-square-tools button svg { width: 21px; height: 21px; }
.look-square-tools button:active { opacity: 0.6; }
.look-square.clearable .look-square-clear { display: flex; }

/* ---- How long this Listen look has left ----
   Along the foot of the spot: green laid down first, red eating it from the
   left, and when the red reaches the end the Look puts itself to sleep and the
   line goes with it.

   The two colours are the two the button already uses — the green it wears
   while listening and the red it wears once it has stopped — so the line is
   saying, slowly, the thing the button is about to say. Nothing is written
   anywhere: a number invites arithmetic, and the only thing worth knowing is
   whether there is plenty left or nearly none. */
.look-listen-life {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 6px;
    z-index: 7;                /* over the picture, under the hand at 8 */
    display: none;
    overflow: hidden;
    background: var(--action-green);
}

/* Only while it is actually listening. A spot that is empty, being aimed, or
   asleep has no time to run down. */
.look-spot.listening .look-listen-life { display: block; }

.look-listen-life i {
    display: block;
    width: 0;
    height: 100%;
    background: #94331F;       /* the red the button turns when it stops */
    /* Long, because it moves about a pixel a minute and a step every twenty
       seconds would otherwise read as a twitch. */
    transition: width 1200ms linear;
}

/* ---- Optional camera chooser ----
   Kept for when the phone's fullscreen camera toggle returns. It leaves the
   left corner entirely to the clear button, so
   the two are never in the same place even for a moment.

   Lifted off the spot rather than coloured against it: the same surface with
   a little of the text mixed in, which reads as a control on both themes
   without becoming a second thing competing with the shutter. */
.look-spot-mode {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 39px;               /* the tools' size, so the corners agree */
    height: 39px;
    padding: 0;
    border: 1px solid color-mix(in srgb, var(--text-secondary) 22%, transparent);
    border-radius: 11px;
    background: color-mix(in srgb, var(--look-spot-bg) 86%, var(--text-secondary) 14%);
    color: color-mix(in srgb, var(--text-secondary) 88%, transparent);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    /* Above the spot's own picture and canvas, which carry no z-index, and
       BELOW the explainer's hand at 6. The hand hangs off the page rather than
       off a spot and .look-square opens no stacking context, so the two numbers
       are compared directly -- a 9 here put a button over the drawing that was
       pointing at it. */
    z-index: 5;
    -webkit-tap-highlight-color: transparent;
    transition: transform 120ms ease, filter 120ms ease;
}

.look-spot-mode svg {
    width: 23px;
    height: 23px;
    display: block;
    position: absolute;
    /* The marks are filled shapes, not strokes, so there is no line weight to
       turn up. Stroking them in their own colour fattens the fill from the
       outside instead -- 0.7 of a 24-unit box on each edge, which is about a
       third heavier and holds together at this size where the hairline
       original went thin and grey. */
    stroke: currentColor;
    stroke-width: 0.7;
    stroke-linejoin: round;
}

/* One or the other. The expanding arrows mean the phone's own camera; the
   gathering ones mean the small camera inside the spot. */
.look-spot-mode .look-mode-spot { display: none; }
.look-spot-mode.is-spot .look-mode-full { display: none; }
.look-spot-mode.is-spot .look-mode-spot { display: block; }

.look-spot-mode:active {
    transform: scale(0.92);
    filter: brightness(0.92);
}

/* Nothing to choose once the spot holds a picture, is aiming, or is sending —
   or while the explainer is borrowing the spot to demonstrate on. */
/* ---- Turning the camera round ----
   The same corner as the mode switch, and never at the same time: that one is
   for choosing a camera before anything opens, this one for turning it round
   once it has. So the corner always holds exactly one control, and which one
   tells you whether the preview is live without having to look at the picture.

   Darker than the mode switch and unbordered, because it sits over a moving
   photograph rather than over an empty spot -- a pale plate that reads well on
   a grey panel disappears over a bright sky. */
.look-spot-flip {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 39px;
    height: 39px;
    padding: 0;
    border: 0;
    border-radius: 11px;
    /* The cross's plate and the cross's grey, to the number. Two controls on
       one picture that were nearly the same shade would read as a mistake; the
       same shade reads as a set. */
    background: rgba(20, 26, 32, 0.45);
    color: #C9D2DA;
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 9;
    -webkit-tap-highlight-color: transparent;
}

.look-spot-flip svg { width: 21px; height: 21px; display: block; }

/* Only while there is a camera to turn round. */
.look-square.live .look-spot-flip { display: flex; }

/* And the same press: the whole plate dims rather than shrinking. */
.look-spot-flip:active { opacity: 0.6; }

.look-square.clearable .look-spot-mode,
.look-square.live .look-spot-mode,
.look-square.adjusting .look-spot-mode,
.look-square.demoing .look-spot-mode,
.look-square.sending .look-spot-mode { display: none; }
.look-square.sending .look-square-tools { display: none; }

/* ---- What the spot does ----
   Sized off the spot rather than a fixed floor, since two of them share the
   width of one. The word is fitted in JS, both buttons taking the size the
   longer of them needs. */
.look-square-action,
.look-explainer-button {
    border: 0;
    background: var(--action-fill, #5E6B78);
    color: #fff;
    font-family: var(--font-family);
    font-weight: 600;
    /* Barely any tracking: the words are words now, not capitals, and
       capitals are the only thing that needs opening up. */
    letter-spacing: 0.02em;
    text-indent: 0.02em;
    line-height: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4);
    -webkit-tap-highlight-color: transparent;
}

.look-square-action {
    position: relative;      /* the countdown fill and the mark sit inside it */
    display: flex;
    align-items: center;
    justify-content: center;
    align-self: center;
    flex: none;
    width: 100%;
    max-width: 168px;
    height: 38px;
    padding: 0 8px;
    border-radius: 19px;
    font-size: 13px;
    cursor: pointer;
    overflow: hidden;        /* keeps the fill inside the rounded ends */
    transition: background-color 200ms ease;
}

.look-square-action[data-state="idle"]   { --action-fill: #5E6B78; }
.look-square-action[data-state="ready"]  { --action-fill: var(--action-green); }
.look-square-action[data-state="on"]     { --action-fill: var(--action-green); }
.look-square-action[data-state="paused"] { --action-fill: #94331F; }

/* Set on the square, which is the button's sibling now rather than its
   parent — hence the combinator. */
.look-square.sending + .look-square-action { visibility: hidden; }
.look-square-action:active { filter: brightness(0.9); }

/* ---- The wait before a call ----
   A framed Look calls by itself, and the button shows the time left as the
   brighter green filling in from the right. Same green the heartbeat flashes,
   so the two read as one colour doing two jobs. */
/* Both ride over the countdown fill, which is laid inside the button. */
.look-square-action > span {
    position: relative;
    z-index: 1;
}

/* ---- Which way the listen button will go ----
   Play while it is paused, pause while it is listening, and neither before
   there is a Look to listen with. Only the listen button has them; the call
   button keeps the room so the two stay one size.

   Taken out of the flow and hung on the right-hand end, because the word is
   the thing that has to stay in the middle of the button: laid out in the row
   it would push "Listen" off centre by half a mark, and off centre by a
   different amount from "Call" beside it. */
.look-action-mark {
    position: absolute;
    right: 11px;
    top: 50%;
    transform: translateY(-50%);
    width: 19px;
    height: 19px;
    fill: currentColor;
    display: none;
    z-index: 1;             /* over the countdown fill, like the word */
}

.look-action-toggle[data-state="ready"]  .look-action-mark.is-play,
.look-action-toggle[data-state="paused"] .look-action-mark.is-play,
.look-action-toggle[data-state="on"]     .look-action-mark.is-pause {
    display: block;
}

.look-square-action.counting::before,
.look-square-action.counted::before {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--action-bright);
    transform-origin: 100% 50%;
}

.look-square-action.counting::before {
    transform: scaleX(0);
    animation: lookCallGrace 4s linear forwards;
}

/* Full and green: the call is on its way */
.look-square-action.counted::before { transform: scaleX(1); }

@keyframes lookCallGrace {
    to { transform: scaleX(1); }
}

/* ---- Listening ----
   A light walking round the outside of the spot, and nothing on the edge
   itself. The button used to beat instead, which put the news on the control
   rather than on the Look that is out there being called.

   The border stays the white it always is. Green went from it because the two
   were saying the same thing twice and the line was the louder of them — it
   drew a hard rectangle round the picture and took the Look's own edge with
   it, while the glow says the same by lying outside the frame and leaving the
   photograph alone. What says a Look is out there is the light, not the line.

   The beat is a heartbeat, not a wander. Two flashes and a rest: the first
   flash strong, the second softer behind it, then nothing for most of the
   cycle — which is what makes it read as a pulse rather than as a light
   that has been left on. The button lifts to a lighter green on each flash
   and the glow around it swells with them, so the colour and the light say
   the same thing at the same moment.

   Every keyframe carries the button's own drop shadow as well: a box-shadow
   animation replaces the whole property, so anything left out of a step
   disappears at that step. */
.look-spot.listening .look-square-action {
    animation: lookHeartbeat 2.6s ease-in-out infinite;
}

@keyframes lookHeartbeat {
    /* rest */
    0% {
        background: var(--action-green);
        box-shadow: 0 0 7px 1px rgba(46, 183, 117, 0.28),
                    0 3px 10px rgba(0, 0, 0, 0.4);
    }
    /* the first flash, the strong one */
    8% {
        background: #3FD98C;
        box-shadow: 0 0 20px 5px rgba(46, 183, 117, 0.72),
                    0 3px 10px rgba(0, 0, 0, 0.4);
    }
    16% {
        background: var(--action-green);
        box-shadow: 0 0 8px 1px rgba(46, 183, 117, 0.30),
                    0 3px 10px rgba(0, 0, 0, 0.4);
    }
    /* and the second, softer, close behind it */
    24% {
        background: #35C47D;
        box-shadow: 0 0 15px 3px rgba(46, 183, 117, 0.52),
                    0 3px 10px rgba(0, 0, 0, 0.4);
    }
    34% {
        background: var(--action-green);
        box-shadow: 0 0 7px 1px rgba(46, 183, 117, 0.28),
                    0 3px 10px rgba(0, 0, 0, 0.4);
    }
    /* then the long rest that makes the two read as one beat */
    100% {
        background: var(--action-green);
        box-shadow: 0 0 7px 1px rgba(46, 183, 117, 0.28),
                    0 3px 10px rgba(0, 0, 0, 0.4);
    }
}

/* Asked for less movement: the beat stops and the light sits steady. The
   colour is what carries the meaning, and it still does. */
@media (prefers-reduced-motion: reduce) {
    .look-spot.listening .look-square-action {
        animation: none;
        box-shadow: 0 0 12px 2px rgba(46, 183, 117, 0.45),
                    0 3px 10px rgba(0, 0, 0, 0.4);
    }
}

/* ---- The adjust step ----
   Full screen, because picking one person out of a crowd needs room. The
   dark beyond the bracket is a ring of shadow cast by the window itself, so
   there is no second element to keep in step with it. */
.look-adjust {
    position: fixed;
    inset: 0;
    z-index: 4000;
    background: #171C21;
    display: flex;
    flex-direction: column;
}

.look-adjust[hidden] { display: none; }

.look-adjust-stage {
    flex: 1;
    position: relative;
    overflow: hidden;
    touch-action: none;
    min-height: 0;
}

.look-adjust-img {
    position: absolute;
    left: 50%;
    top: 50%;
    transform-origin: 50% 50%;
    outline: 1px solid rgba(255, 255, 255, 0.28);
    will-change: transform;
}

.look-adjust-window {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    pointer-events: none;
    box-shadow: 0 0 0 9999px rgba(12, 16, 20, 0.72);
    border-radius: 4px;
}

.look-adjust-corner {
    position: absolute;
    width: 26px;
    height: 26px;
    border: 2px solid #fff;
    opacity: 0.9;
    filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.85));
}

.look-adjust-corner.tl { left: -6px;  top: -6px;    border-right: 0; border-bottom: 0; border-radius: 5px 0 0 0; }
.look-adjust-corner.tr { right: -6px; top: -6px;    border-left: 0;  border-bottom: 0; border-radius: 0 5px 0 0; }
.look-adjust-corner.br { right: -6px; bottom: -6px; border-left: 0;  border-top: 0;    border-radius: 0 0 5px 0; }
.look-adjust-corner.bl { left: -6px;  bottom: -6px; border-right: 0; border-top: 0;    border-radius: 0 0 0 5px; }

.look-adjust-bar {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 48px;
    padding: 16px 0 calc(16px + env(safe-area-inset-bottom, 0px));
}

.look-adjust-btn {
    width: 62px;
    height: 62px;
    border: 0;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.look-adjust-btn svg { width: 26px; height: 26px; }
.look-adjust-btn:active { filter: brightness(0.9); }
.look-adjust-cancel { background: #5E6B78; }
.look-adjust-ok { background: var(--action-green); }


/* ---- A slide ----
   All of them on one track, so stepping is a slide and every picture is
   already loaded when asked for. */
/* The width is set in the markup: the slides plus a copy at each end.
   Every page is one windowful, whatever that adds up to. */
.look-explainer-track {
    display: flex;
    height: 100%;
    transition: transform 320ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* The picture takes the whole page, and the caption stands on top of it.

   It used to be a column — a picture with room kept above it and a line and
   dots below — which meant the picture was given whatever was left after the
   words, minus 44px of gutter on each side for the chevrons. A square picture
   came out barely half the width of the card it was sitting in, on a page
   that was mostly empty.

   Now the picture has the page and the words lie over it. Nothing is reserved
   for them, so nothing is taken from the picture; the words carry their own
   white so they are legible over whatever they land on. */
.look-explainer-page {
    flex: 0 0 auto;            /* the width comes from the markup, with the count */
    position: relative;        /* everything below is placed against its own page */
    height: 100%;
    box-sizing: border-box;
}

/* The caption, lying across the foot of the picture.

   The <p> is the full width of the page and is what the fitter measures
   against; the white is on the <span> inside it, which is only as wide as the
   words. So the pill hugs the line however long the language makes it, and
   there is no white bar across a picture on the short ones.

   It sits just above the dots at the foot of the strip. It used to be lifted
   clear of the messages button by a figure worked out in JS, which stopped
   being anybody's business when that button went to the top of the page. */
.look-explainer-text {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 19px;              /* the dots keep the bottom ten, and their own five */
    margin: 0;
    padding: 0 10px;
    box-sizing: border-box;
    text-align: center;
    white-space: nowrap;
    font-family: var(--font-family);
    font-weight: var(--font-weight-bold);
    font-size: 18px;
    /* Fixed rather than a multiple: the words are shrunk to whatever fits the
       narrowest page, and a line that changed height with them would take the
       picture off centre by however much it had shrunk. */
    line-height: 23px;
    color: #1C2630;
    /* A hard white outline, not a glow. Four solid one-pixel copies of the
       letters offset to the corners draw an unbroken edge round every stroke;
       a blurred shadow only thins out over a busy drawing, which is where it
       was needed. The soft one behind them lifts the whole line off the
       picture. */
    text-shadow:
        -1px -1px 0 #fff,  1px -1px 0 #fff,
        -1px  1px 0 #fff,  1px  1px 0 #fff,
         0   -1px 0 #fff,  0    1px 0 #fff,
        -1px  0   0 #fff,  1px  0   0 #fff,
         0    0   4px #fff;
    z-index: 2;
    pointer-events: none;      /* the strip underneath is what a finger wants */
}

/* The one word in a caption that can be pressed. A button, because it does
   something, dressed as the word it sits in — the paragraph's own font, colour
   and white outline, underlined so it reads as somewhere to go.

   The paragraph turns pointer events off so that a finger anywhere across it
   reaches the strip underneath and swipes; this one word turns them back on,
   which is the whole of the exception. */
.look-explainer-link {
    display: inline;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
    pointer-events: auto;
}

.look-explainer-text span {
    display: inline-block;
    max-width: 100%;
    padding: 5px 14px;
    border-radius: 14px;
    /* White glass. Plain white rather than the faintly blue-grey it was, and
       nearly opaque — but only nearly: the drawing still carries on behind
       the words instead of being cut in half by them, which is the whole
       reason the panel is not simply solid. */
    background: rgba(255, 255, 255, 0.88);
    box-sizing: border-box;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The picture, given the whole page — corner to corner, gutters included.
   The chevrons and the caption stand on top of it rather than beside it. */
.look-explainer-frame {
    position: absolute;
    inset: 0;
    /* A white margin all the way round: six per cent of the page's width and
       twelve pixels on top of it.

       Percentage padding resolves against the containing block's WIDTH on
       every side, so the same amount goes round the top and bottom as the
       sides. The twelve pixels are flat rather than proportional on purpose —
       they are there to keep the drawing off the edge, and that is a fixed
       distance for the eye however wide the phone is.

       The white is the strip's own background showing through.

       A slide may ask for more underneath with --room, and underneath only.
       The picture is contained and centred, so height taken off the bottom
       both shrinks it and lifts it: a pixel of --room buys a pixel of
       clearance. Taken off all four sides it would cost two pixels of picture
       for every one of clearance, which on the slide that needs this is the
       difference between a 137px drawing and a 113px one.

       Only listen1 asks, because the caption lands across the bottom of its
       mirror — the part of the picture the slide is about. */
    padding: calc(6% + 12px);
    padding-bottom: calc(6% + 12px + var(--room, 0px));
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    /* The last slide is drawn rather than loaded, and everything in it is
       sized against the square it is drawn in. Naming the frame a size
       container is what lets that square be worked out in CSS: cqw and cqh
       here are the frame's CONTENT box, so min(100cqw, 100cqh) is exactly the
       biggest square that fits inside the white margin — the same box
       object-fit: contain would have given a picture.

       Safe to contain: the frame is inset: 0 on its page, so its size comes
       from above and never from what is inside it. */
    container-type: size;
}

.look-explainer-img {
    width: 100%;
    height: 100%;
    /* Contain, not cover: these are drawings that mean something, and a
       picture cropped to fill the box loses the half of it being pointed at.
       Given the full width and the full height it comes out as large as it
       can be without losing anything. */
    object-fit: contain;
}

/* ---- The last slide: the invite, being answered ----

   Drawn out of elements instead of loaded as a picture, because three of its
   parts change: the face on the card is the reader's own profile photo, the
   name is theirs, the tab is a translated sentence — and the card swipes
   itself, which no picture does.

   The card is .invite-card from further down this file, restated as a share of
   its own width so it is the same card at any size, and it does what a hand
   would do with one: out to the left with the red cross up, out to the right
   with the green message mark up, then home. Its dashed outline stays where
   the card lives, in the middle, and shows through whenever the card is away
   from it.

   TWO SETS OF UNITS, and it matters which is which. The stage is a size
   container, so cqw on the stage's own children — the outline, the card's box
   — is a share of the SQUARE. The card is a container in turn, so cqw inside
   it is a share of the CARD, which is how everything the card carries stays in
   the real card's proportions however big the slide comes out. Never put a
   cqw length in a custom property that both levels read: it would mean two
   different things in the two places.

   The stage's shape follows from the swipe. The card travels 0.26 of its own
   width each way and leans 10.5 degrees at the ends, which reaches 0.843 of a
   card either side of the middle and 0.687 of one above it, the tab included.
   That is 1.685 by 1.270 cards — the 1.327 below — with the card 59.33% of the
   width and its middle 54.11% of the way down. */
.look-invite {
    position: relative;
    /* A fifth over the biggest box of that shape the white margin leaves —
       what object-fit: contain would have given a picture of it, and then
       some. The margin was sized for a picture, which had to fit inside it;
       this is a card being thrown about, and the swing is allowed to reach
       past the margin, behind the chevrons and a little over the edge. The
       card at rest still lands well inside.

       Scaling the box rather than the card keeps every proportion below
       exactly as it was worked out: the drawing simply arrives bigger. */
    /* The second and third are floors under the over-reach, not sizes. The
       frame clips at the page edge, so the drawing may reach past the white
       margin — that is the point — but not past the page, where it would be cut
       off rather than merely overhanging. And the box IS the reach: it was
       worked out as exactly twice the swing's sweep across and exactly its
       tab-to-corner height down, so holding the box inside the page holds the
       card inside it.

       The page can be named from here. The frame's margin is 6% of the PAGE
       plus 12px a side and 100cqw is what is left of it, so the page is
       (100cqw + 24px) / 0.88 across, one margin is 6.82cqw + 13.64px, and the
       page comes to 113.64cqw + 27.3px across and 100cqh + 13.64cqw + 27.3px
       down. Six pixels of air on each side leaves the two below. */
    width: min(
        calc(min(100cqw, 100cqh * 1.32738) * 1.38),
        calc(113.64cqw + 15px),
        calc((100cqh + 13.64cqw + 15px) * 1.32738)
    );
    aspect-ratio: 1.32738;
    /* The frame centres it; without this, flex would shrink it back to the
       width it was allowed rather than let it over-reach. */
    flex: none;
}

/* The stage is its own size container, so the cqw inside are the drawing's and
   not the frame's. It is inset: 0 on a parent that already has a size, so
   containing it decides nothing. */
.look-invite-stage {
    position: absolute;
    inset: 0;
    container-type: size;
    /* Fifteen per cent larger, applied here rather than to twenty separate
       numbers below. Everything inside is measured in cqw against this stage,
       and a transform does not change the stage's layout width — so the units
       stay exactly as they were worked out and the drawing simply arrives
       bigger, every proportion intact. */
    transform: scale(1.15);
}

/* Where the card lives: an empty outline, dead centre, in the card's shape.

   The card's own size and the card's own corner, so it reads as the outline of
   a card rather than as a slot a size too big for one. It was a twentieth
   wider, which showed as a loose box the card rattled around in.

   Heavier and paler than the card's white edge. Heavier because a hairline of
   dashes at this size disappears into the photograph behind it; paler because
   this is the ABSENCE of a card, and an outline drawn as firmly as the thing
   itself argues with it. */
.look-invite-ghost {
    position: absolute;
    left: 50%;
    top: 54.11%;
    width: 59.33cqw;             /* the card's width, exactly */
    aspect-ratio: 1 / 1;
    transform: translate(-50%, -50%);
    box-sizing: border-box;
    border: max(2px, 0.82cqw) dashed #DCE3E9;
    border-radius: 3.47cqw;      /* and the card's corner, exactly */
}

/* The card itself: square, a thin white edge, a 20-on-342 corner, a cover-fit
   photo and the name across the top over a dark gradient — .invite-card, to
   scale. What is NOT here is the pair of answer buttons at its foot; the two
   marks the swipe raises say the same two things, and this slide is about the
   answer being given rather than about the buttons that give it.

   A filter rather than a box-shadow, because the tab is part of the card and
   has to be lit as part of it. box-shadow traces the border box, so it would
   have run straight across the foot of the tab and drawn a line where the join
   is; drop-shadow traces what is actually painted, tab included, so card and
   tab throw one shadow and read as one piece of card. */
.look-invite-card {
    --travel: 15.43cqw;          /* 0.26 of a card, each way */
    position: absolute;
    left: 50%;
    top: 54.11%;
    width: 59.33cqw;
    aspect-ratio: 1 / 1;
    box-sizing: border-box;
    transform: translate(-50%, -50%);
    --card-edge: max(1px, 0.52cqw);
    border: var(--card-edge) solid #fff;
    /* Square where the tab lands, rounded everywhere else.
       The tab has a straight foot and the corner beneath it was curving away
       from it, which left a notch of the page showing between the two — a card
       and a label that had drifted apart rather than one piece of card. A
       square corner there means the tab's foot and the card's edge meet along
       their whole width, and what fills the join is the card's own white
       border, which is what the eye expects a folded tab to be made of. */
    border-radius: 0 3.47cqw 3.47cqw 3.47cqw;
    background: #fff;
    filter: drop-shadow(0 1.5cqw 4.5cqw rgba(20, 28, 36, 0.28));
    /* From here down, cqw is a share of the CARD. */
    container-type: inline-size;
}

/* What the card holds, inside its white edge — the photo, and the name over
   it. inset: 0 on a bordered card is its padding box, so this is exactly the
   area the real card's photo fills.

   The grey and the silhouette are what shows if even the head shot will not
   load. That is the card still saying "a person", which is the part that
   matters here. */
.look-invite-face {
    position: absolute;
    inset: 0;
    display: block;
    /* Follows the card, including the square corner under the tab: left
       rounded, the photograph would curve out from behind the white border and
       put a crescent of picture into the join the border had just closed. */
    border-radius: 0 4.97cqw 4.97cqw 4.97cqw;
    overflow: hidden;
    background:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cg fill='%23B9C4CF'%3E%3Ccircle cx='50' cy='36' r='19'/%3E%3Cpath d='M50 60c-19 0-34 13-34 30v10h68V90c0-17-15-30-34-30z'/%3E%3C/g%3E%3C/svg%3E")
        center / 78% no-repeat,
        #E4EAF0;
}

.look-invite-photo {
    display: block;
    width: 100%;
    height: 100%;
    /* Cover, not contain: a profile photo is a portrait and the card is
       square, so filling it is right and the trim costs nothing. */
    object-fit: cover;
}

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

/* The name across the top of the card, over a gradient that darkens into the
   top edge — the real card's own treatment, at its own proportions (14/18/44
   of 342 for the padding, 21 for the words).

   One line, always. A name long enough to reach the far edge is cut with an
   ellipsis rather than allowed to wrap: two lines here would push the card's
   whole top block down over the face and stop looking like the card it is
   drawing. */
.look-invite-name {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    padding: 4.09cqw 5.26cqw 12.87cqw;
    box-sizing: border-box;
    background: linear-gradient(rgba(0, 0, 0, 0.65), transparent);
    color: #fff;
    font-family: var(--font-family);
    font-size: 6.6cqw;
    font-weight: 600;
    line-height: 1.15;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The two answers, as a swipe raises them: the one being carried towards comes
   up on the edge leading the way — the cross out to the left, the message mark
   out to the right. Bigger and softer than the buttons draw them, and
   see-through, so the face underneath still reads. Both are hidden until the
   card leans; the keyframes below bring up whichever way it went.

   The colour is on the disc rather than on the whole mark, because the mark
   inside it is opaque white and an opacity on the pair would fade that too. */
.look-invite-mark {
    position: absolute;
    top: 74%;
    width: 44cqw;
    aspect-ratio: 1 / 1;
    transform: translate(-50%, -50%) scale(0.7);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
}

.look-invite-mark.nope { left: 30%; background: rgba(148, 51, 31, 0.82); }
.look-invite-mark.like { left: 70%; background: rgba(31, 122, 78, 0.82); }

.look-invite-mark svg {
    width: 56%;
    height: 56%;
}

/* The tab on top of the card, saying what the card is.

   Straight down the left, slanted on the right, sitting on the card's top edge
   and going wherever the card goes. It is the card's own material rather than
   a label stuck on it: the same white, and its foot lands under the card's
   white border so the two meet with no join to draw. Dark grey words on it,
   not white on a colour — the only things on this card allowed to shout are
   the two marks the swipe raises.

   Only as wide as its words: absolutely positioned with a left and no width,
   so it shrinks to fit whatever the language makes of it, and never past three
   quarters of the card. Past that it ellipsises rather than growing, because a
   tab wider than that stops looking like a tab.

   The left edge stands clear of the card's rounded corner, so its foot lands
   on the straight part of the top edge. */
.look-invite-tab {
    position: absolute;
    /* bottom: 100% on a bordered card is the inside of its top edge, so the
       tab's foot lands under the white border and the two merge. */
    bottom: 100%;
    /* Two per cent in, which is where two wrong answers put it.

       Four was visibly right of the card; zero was a few pixels left of it.
       Zero is not the card's edge, because an absolutely positioned child sits
       against its parent's PADDING box — so zero is the inside of the white
       border, and the eye reads the tab against the border's outer edge, which
       is a border-width further out. Two lands between the two edges and looks
       like one line.

       The straight side is on the left precisely so it can line up with
       something. The slant is on the right, where there is nothing to meet. */
    /* Flush with the card's outer edge, exactly.
       Two per cent was a third guess after four and zero, and it is still a
       guess — it drifts with the card's width, so the one width it was tuned
       on is the only one where it lines up. The right answer is not a
       proportion at all: an absolutely positioned child sits against its
       parent's PADDING box, so the card's outer edge is one border-width
       further out, and that is the whole of the correction. Each card declares
       its own --card-edge; the fallback keeps a card that forgot to at zero
       rather than at NaN. */
    left: calc(-1 * var(--card-edge, 0px));
    max-width: 75%;
    height: 11.35cqw;
    box-sizing: border-box;
    padding: 0 7.12cqw 0 3.69cqw;   /* the right side clears the slant */
    display: flex;
    align-items: center;
    background: #fff;
    color: #3A4652;
    font-family: var(--font-family);
    font-weight: var(--font-weight-bold);
    font-size: 7.12cqw;
    line-height: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    border-radius: 2.11cqw 0 0 0;
    clip-path: polygon(0 0, calc(100% - 5.28cqw) 0, 100% 100%, 0 100%);
}

/* ---- The card answering itself ----

   What a hand does with one, on a loop: a beat at rest, out to the left with
   the cross up, out to the right with the message mark up, a pause there, and
   home. The lean is the real card's own — it turns a tenth of a degree per
   pixel carried, which at this reach is about ten and a half.

   Only while this is the slide being looked at. The strip keeps every page
   built, so without the class this would be an animation running behind five
   other slides for the whole life of the app. */
@keyframes look-invite-swipe {
    0%, 10%   { transform: translate(-50%, -50%); }
    22%, 33%  { transform: translate(-50%, -50%) translateX(calc(-1 * var(--travel))) rotate(-10.5deg); }
    46%, 60%  { transform: translate(-50%, -50%) translateX(var(--travel)) rotate(10.5deg); }
    72%, 100% { transform: translate(-50%, -50%); }
}

@keyframes look-invite-nope {
    0%, 12%   { opacity: 0; transform: translate(-50%, -50%) scale(0.7); }
    22%, 33%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
    41%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.7); }
}

@keyframes look-invite-like {
    0%, 38%   { opacity: 0; transform: translate(-50%, -50%) scale(0.7); }
    47%, 60%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
    68%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.7); }
}

.look-invite.swiping .look-invite-card {
    animation: look-invite-swipe 6.4s ease-in-out infinite;
}

.look-invite.swiping .look-invite-mark.nope {
    animation: look-invite-nope 6.4s ease-in-out infinite;
}

.look-invite.swiping .look-invite-mark.like {
    animation: look-invite-like 6.4s ease-in-out infinite;
}

/* Nothing moves, so the card is parked where the swipe was going: leaning
   right with the message mark up, off its outline, which is the same thing
   said as a picture. */
@media (prefers-reduced-motion: reduce) {
    .look-invite.swiping .look-invite-card,
    .look-invite.swiping .look-invite-mark.nope,
    .look-invite.swiping .look-invite-mark.like {
        animation: none;
    }

    .look-invite .look-invite-card {
        transform: translate(-50%, -50%) translateX(var(--travel)) rotate(10.5deg);
    }

    .look-invite .look-invite-mark.like {
        opacity: 1;
        transform: translate(-50%, -50%) scale(1);
    }
}

/* ---- Which spot the slide means ----
   The two slides where a Look is added raise an arrow down the outside edge,
   bending away from the middle and reaching over the spot they are about:
   left for listening, right for calling, the sides those spots are on. The
   other three raise none — by then which spot is meant is no longer in
   question.

   It hangs off the page rather than off a slide. It has to cross the gap into
   the spot below, and both the strip and the half it sits in clip anything
   that leaves them; there is one of each rather than one per slide, shown for
   whichever slide is up. That means it fades in and out with the slide rather
   than sliding with it — the same way the drawing in the spot below arrives,
   which is the half of the page it is pointing at.

   Its gold lives in the drawing itself, assets/spot-arrow.svg.

   One drawing, turned two ways. It was drawn pointing up and to the right;
   flipped top to bottom it curves one way, turned right round it curves the
   other. Mirroring both horizontally swaps which side gets which — the same
   two turnings, exchanged. */
.look-explainer-arrow {
    position: absolute;
    /* Low enough to lie across the top of the spot it points at. The halves
       are equal, so 50% is the line between them, and this hangs the arrow
       across it. A turned box is bigger than the box it was — 15deg turns
       78x138 into 111x153 — which the placement allows for. */
    bottom: calc(50% - 76px);
    width: 78px;
    height: 138px;
    /* Enough shadow to hold it off a busy picture, not enough to look stuck
       on: a tight contact shadow and a soft halo behind it. */
    filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.34))
            drop-shadow(0 0 7px rgba(0, 0, 0, 0.28));
    /* It lies over a spot, so it must not take a press meant for one. */
    pointer-events: none;
    z-index: 6;
    opacity: 0;
    transition: opacity 220ms ease;
}

/* Up only for the slide that raised it */
.look-explainer-arrow.showing { opacity: 1; }

.look-explainer-arrow.left {
    left: 38px;
    transform: rotate(15deg) scaleY(-1);
}

.look-explainer-arrow.right {
    right: 38px;
    transform: rotate(165deg);
}/* Under the picture, a small copy of the control the page is about */
/* Dark, with a white halo, so a bracket stays legible whatever it sits on */
.look-explainer-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    /* Grown with the chevron rather than left behind it: a 42px glyph in a
       42px box has its round stroke ends flush against the edge, and the
       tap target should stay bigger than the thing it is drawn around. */
    width: 55px;
    height: 68px;
    padding: 0;
    border: 0;
    background: none;
    color: #1C2630;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 2;
    -webkit-tap-highlight-color: transparent;
}

/* The chevron stands on a disc of its own.
   Drawn on the svg rather than on the button, so the tap target keeps its
   full 55x68 while the thing you can see is a 40px circle. The padding
   insets the drawing inside that circle: an SVG's viewport is its content
   box, so six pixels of it brings the glyph in off the rim, which a 42px
   chevron in a 42px box was pressed right up against.

   A hairline edge and two shadows — one tight and dark to seat the disc on
   the picture, one wide and soft to lift it off — because a plain white
   circle on a drawing that is mostly white is not a circle at all. */
.look-explainer-nav svg {
    width: 40px;
    height: 40px;
    padding: 6px;
    box-sizing: border-box;
    background: #fff;
    border: 1px solid rgba(28, 38, 48, 0.16);
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(28, 38, 48, 0.20),
                0 2px 10px rgba(28, 38, 48, 0.14);
}

/* The whole disc presses, not just the glyph on it. */
.look-explainer-nav:active svg {
    background: #f1f3f5;
    box-shadow: 0 1px 2px rgba(28, 38, 48, 0.24);
    transform: scale(0.94);
}
/* Hard against the sides. The tap target keeps its 55px so it is still
   easy to hit, but the glyph is pushed to the end of it rather than
   centred, which puts the arrow itself out on the edge where it belongs
   and leaves the middle of the picture clear. */
.look-explainer-prev { left: 0; justify-content: flex-start; padding-left: 6px; }
.look-explainer-next { right: 0; justify-content: flex-end; padding-right: 6px; }

/* There is nothing behind the first slide, so nothing points that way */
.look-explainer-nav[hidden] { display: none; }

/* ---- Putting the explainer away ----
   A red cross while the slides are up, a grey question mark once they have
   gone, both in the same corner: the way back has to be exactly where the way
   out was, because that is the only place anybody will look for it.

   The two are coloured on opposite principles, because they stand on
   different things. The cross is on the slides, which are white whatever the
   theme, so its colours are written out: a token there would follow the page
   rather than the thing the mark is actually on. The question mark is on the
   panel the slides leave behind, which does follow the theme -- so it takes
   the theme's own grey and is legible either way round. */
.look-explainer-toggle {
    position: absolute;
    bottom: 8px;
    right: 8px;
    z-index: 15;                /* over the slides, under a card that arrives */
    width: 40px;                /* tap target, bigger than the mark on it */
    height: 40px;
    padding: 0;
    border: 0;
    background: none;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.look-explainer-toggle svg {
    position: absolute;         /* the two sit on each other and cross-fade */
    width: 26px;
    height: 26px;
    transition: opacity 200ms ease, transform 120ms ease;
    /* The slides are photographs, and a thin ring on a busy one disappears */
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.25));
}

/* Faded red, on a disc pale enough to lift it off whatever picture is under */
.look-toggle-x { color: rgba(178, 52, 38, 0.48); }
.look-toggle-x .look-toggle-disc { fill: rgba(255, 255, 255, 0.62); }

/* Grey, from the theme, because the panel it stands on is themed too. No disc
   and no shadow: both were there to hold the mark off a photograph, and once
   the slides are gone there is no photograph, only a flat surface. */
.look-toggle-q {
    /* It stands on the album now, not on a flat panel — bottom right, over a
       photograph that may be any colour at all. So it gets its disc back and
       its own shadow: the grey-on-grey version was legible against the empty
       half it was drawn for and vanished the moment there was a picture
       underneath. */
    color: rgba(17, 27, 33, 0.72);
    filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.45));
}

.look-toggle-q .look-toggle-disc { fill: rgba(255, 255, 255, 0.88); }


/* Only ever one of the two */
.look-toggle-q { opacity: 0; }
.looks-explainer-off .look-toggle-x { opacity: 0; }
.looks-explainer-off .look-toggle-q { opacity: 1; }

.look-explainer-toggle:active svg { transform: scale(0.92); }

/* Gone, but still in place: fading rather than removing keeps the strip where
   it was, so bringing it back does not rebuild it. Deaf while it is invisible,
   or it would go on taking the swipes now meant for nothing at all. */
.looks-explainer-off .look-slides {
    opacity: 0;
    pointer-events: none;
}

/* The pointers hang off the page rather than off a slide, so they outlive it
   unless they are told not to. */
.looks-explainer-off .look-pointer { opacity: 0; }

/* ---- The badges ----
   Nine numbered squares behind the explainer. They sit at the bottom of the
   stack in the top half: the slides lie over them on an opaque white ground
   and the arriving cards lie over both, so the grid is only ever seen once
   the explainer has been put away. Nothing had to move to make room for it.

   Deaf all the way through. There is nothing to press here yet — a badge
   leaves from the chat, not from the grid — and a layer this size would
   otherwise swallow taps meant for whatever is under it. */
.looks-badges {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Down as far as the slide dots and the cross that puts the explainer
       away, since none of that is on screen while the album is. Only the top
       is still held off: the avatar row reaches 50px in and the messages
       button 60, and a face with a profile picture sitting on it is worse
       than a slightly shorter grid. */
    padding: 60px 12px 14px;
    box-sizing: border-box;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity 240ms ease, visibility 0s linear 240ms;
}

.looks-explainer-off .looks-badges {
    opacity: 1;
    visibility: visible;
    transition: opacity 240ms ease, visibility 0s;
}

/* No hand yet — signed out, or before the server's first answer. Last, and
   at the same weight as the rule above, so it wins the tie. */
.looks-badges[hidden] { display: none; }

/* As big as the space allows, in three rows of three. The cells come out a
   little taller than wide, which is what a sticker slot looks like anyway —
   the ratio is the space's, not something imposed on it. */
.badge-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(3, 1fr);
    /* Close enough to read as one printed page rather than nine cards on a
       table. The border does the separating now. */
    gap: 2px;
    height: 100%;
    width: 100%;
    max-height: 100%;
    min-height: 0;
}

/* An empty slot is a dashed outline with a faint number in it — the printed
   ghost a paper album leaves in the space, which is the whole reason anybody
   counts what is missing. */
.badge-slot {
    position: relative;
    overflow: hidden;
    border-radius: 3px;
    border: 1px solid color-mix(in srgb, var(--text-secondary) 30%, transparent);
    background: var(--look-spot-bg);
    box-sizing: border-box;
}

/* The picture. Full colour once it is in the album; drained and dimmed
   until then, which is the printed ghost a paper album leaves in the space —
   you can see who is missing, which is the whole reason anybody counts. */
.badge-face {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.badge-slot:not(.has) .badge-face {
    filter: grayscale(1) brightness(1.18) contrast(0.72);
    opacity: 0.52;
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.9) 24%, rgba(0, 0, 0, 0.28) 100%);
    mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.9) 24%, rgba(0, 0, 0, 0.28) 100%);
}



/* Held. One copy is enough to fill the slot, and it never comes out again. */
.badge-slot.has {
    /* Nothing over the picture. This used to fill with the accent colour and
       carry the number in the middle, from before there was anything to look
       at. Held is now simply the photograph in full colour, against the
       drained one next to it. */
    border-color: color-mix(in srgb, var(--text-secondary) 45%, transparent);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
}


/* How many are SPARE — one fewer than held, because the first of each stays
   in the grid. It is the only number here that decides anything now that the
   players carry their own names in the photographs, so it takes the room the
   1-to-9 chip used to have. */
.badge-count {
    position: absolute;
    top: 0;
    right: 0;
    min-width: 20px;
    padding: 1px 5px;
    border-radius: 0 0 0 6px;
    background: var(--accent);
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    font-style: normal;
    line-height: 18px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}


/* ---- Throwing the called Look away ----
   It leaves its spot for the middle of the screen, drawing back off the
   edges, shaking as it is called out, screwing itself up and bursting. The
   travel is set per throw in --throw-x/--throw-y, since where the middle
   lies depends on which spot it started from. */
/* The calling stays in its own spot: the picture screws itself up and bursts
   where it lay, and the spot goes on clipping it as it always does. */
.look-square.sending {
    overflow: hidden;
}

/* ---- Destroying a called Look ----
   Draw back a fifth, shake where it stands for two seconds, then shrink to a
   fifth of the spot. The breaking apart is done in JS from there, since every
   piece has to go its own way. */
.look-square-img.vanishing {
    animation: lookVanish 2680ms linear forwards;
    transform-origin: 50% 50%;
    z-index: 30;
    will-change: transform;
}

@keyframes lookVanish {
    /* drawn back a fifth */
    0%     { transform: scale(1) rotate(0deg); }
    9.7%  { transform: scale(0.8) rotate(0deg) translate(0, 0); }

    /* shaken, held at that size */
    12.01%  { transform: scale(0.8) rotate(-2.2deg) translate(-2.0px, -1.6px); }
    14.33%  { transform: scale(0.8) rotate(2.5deg) translate(3.0px, 0.0px); }
    16.64%  { transform: scale(0.8) rotate(-2.8deg) translate(-4.0px, 1.6px); }
    18.96%  { transform: scale(0.8) rotate(2.2deg) translate(2.0px, 0.0px); }
    21.27%  { transform: scale(0.8) rotate(-2.5deg) translate(-3.0px, -1.6px); }
    23.58%  { transform: scale(0.8) rotate(2.8deg) translate(4.0px, 0.0px); }
    25.90%  { transform: scale(0.8) rotate(-2.2deg) translate(-2.0px, 1.6px); }
    28.21%  { transform: scale(0.8) rotate(2.5deg) translate(3.0px, 0.0px); }
    30.52%  { transform: scale(0.8) rotate(-2.8deg) translate(-4.0px, -1.6px); }
    32.84%  { transform: scale(0.8) rotate(2.2deg) translate(2.0px, 0.0px); }
    35.15%  { transform: scale(0.8) rotate(-2.5deg) translate(-3.0px, 1.6px); }
    37.46%  { transform: scale(0.8) rotate(2.8deg) translate(4.0px, 0.0px); }
    39.78%  { transform: scale(0.8) rotate(-2.2deg) translate(-2.0px, -1.6px); }
    42.09%  { transform: scale(0.8) rotate(2.5deg) translate(3.0px, 0.0px); }
    44.40%  { transform: scale(0.8) rotate(-2.8deg) translate(-4.0px, 1.6px); }
    46.72%  { transform: scale(0.8) rotate(2.2deg) translate(2.0px, 0.0px); }
    49.03%  { transform: scale(0.8) rotate(-2.5deg) translate(-3.0px, -1.6px); }
    51.34%  { transform: scale(0.8) rotate(2.8deg) translate(4.0px, 0.0px); }
    53.66%  { transform: scale(0.8) rotate(-2.2deg) translate(-2.0px, 1.6px); }
    55.97%  { transform: scale(0.8) rotate(2.5deg) translate(3.0px, 0.0px); }
    58.28%  { transform: scale(0.8) rotate(-2.8deg) translate(-4.0px, -1.6px); }
    60.60%  { transform: scale(0.8) rotate(2.2deg) translate(2.0px, 0.0px); }
    62.91%  { transform: scale(0.8) rotate(-2.5deg) translate(-3.0px, 1.6px); }
    65.22%  { transform: scale(0.8) rotate(2.8deg) translate(4.0px, 0.0px); }
    67.54%  { transform: scale(0.8) rotate(-2.2deg) translate(-2.0px, -1.6px); }
    69.85%  { transform: scale(0.8) rotate(2.5deg) translate(3.0px, 0.0px); }
    72.16%  { transform: scale(0.8) rotate(-2.8deg) translate(-4.0px, 1.6px); }
    74.48%  { transform: scale(0.8) rotate(2.2deg) translate(2.0px, 0.0px); }
    76.79%  { transform: scale(0.8) rotate(-2.5deg) translate(-3.0px, -1.6px); }
    79.10%  { transform: scale(0.8) rotate(2.8deg) translate(4.0px, 0.0px); }
    81.42%  { transform: scale(0.8) rotate(-2.2deg) translate(-2.0px, 1.6px); }
    84.33%  { transform: scale(0.8) rotate(0deg) translate(0, 0); }

    /* and down to a fifth of the spot, where it breaks */
    100%   { transform: scale(0.2) rotate(0deg); }
}

/* The pieces it breaks into. Each carries its own square of the picture and
   is thrown by JS, so nothing here says where any of them goes. */
.look-shatter {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 31;
}

.look-shatter i {
    position: absolute;
    background-repeat: no-repeat;
    will-change: transform, opacity;
}

/* ---- Page furniture ----
   The floating profile button and the messages button. They sit over the
   spots but belong to the page, and are unchanged by the new layout. */
/* Profile button, floating top-right over the top spot */
/* Both of the things that are about you, together in the top right, the
   count on the left of the picture. They used to sit at opposite ends of the
   bar, which left the count alone in the corner the messages button now
   stands in. */
.looks-topbar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 30;
    display: flex;
    /* Tops level, not centres. The pill is 27px and the avatar 39, so centring
       them on the line hung the pill six pixels below everything else up here
       — including the messages button opposite, which is 54 and starts at the
       same place they do. What the eye reads along this row is the top edge. */
    align-items: flex-start;
    justify-content: flex-end;
    gap: 10px;
    /* 16 down, because the card underneath starts at 10 and these were sitting
       two pixels above its edge. 10 across, to stand on the same edge the card
       does rather than inset from it. */
    padding: 16px 10px 0 10px;
    pointer-events: none;
}

/* Held to the right end by the row, not by a margin of its own: with the two
   of them side by side there is nothing to push apart, and when there is no
   pill — signed out, or before the balance is known — the avatar has to stay
   in its corner rather than slide across to the empty one. */
.looks-topbar .header-menu-btn {
    pointer-events: auto;
    filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45));
}

.looks-topbar .header-avatar {
    width: 39px;               /* the tools' size, in .look-square-tools */
    height: 39px;
    margin-right: 0;
}

/* ---- What is left to call with ----
   A number on a pill, opposite the avatar. It turns from green to orange to
   red as the wallet empties, but the number is what carries the meaning —
   the colour only says how hard to look at it, which is why it is never
   replaced by a dot and why the element carries a spelt-out aria-label.
   Solid colours rather than themed ones: it floats over pictures, so it
   brings its own ground in both themes. */
.credits-pill {
    /* On the left end of the Call button, which is what it is about: what is
       left to call with, said where calling is done. It used to sit up in the
       top row beside the profile ball, which put a number about calling as far
       from the Call button as the page allows.

       Placed against the column rather than the button because the button is
       centred in that column and capped at 168px, so its left edge is
       (column - 168) / 2 in, or hard against the side once the column is
       narrower than the cap. The max() says exactly that and needs no script.

       Both insets are 5.5px, which is (38 - 27) / 2 and not a coincidence: the
       button is a 38px capsule so its end arc centres 19px in, the pill is a
       27px capsule with a 13.5px radius, and 19 - 13.5 puts the two arcs on the
       same centre. Concentric, so the gap holds the same width the whole way
       round the end instead of pinching at the corners. */
    position: absolute;
    z-index: 2;
    bottom: 5.5px;
    left: calc(max(0px, (100% - 168px) / 2) + 5.5px);
    /* A ball, not a pill. 27 across at 5.5 in puts its centre exactly 19px
       from the corner in both directions -- which IS the button end arc centre,
       so a circle nests in a capsule end the way nothing else quite does. */
    width: 27px;
    height: 27px;
    padding: 0;
    border: none;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-family);
    font-size: 13px;           /* three digits still fit the ball */
    font-weight: var(--font-weight-bold);
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: #fff;
    background: var(--credit-green);
    /* No shadow. It had one to hold it off the pictures it used to float over;
       on the flat of the Call button that shadow only made it look stuck on. */
    transition: background-color 260ms ease;
    cursor: pointer;
}

.credits-pill[hidden] { display: none; }
.credits-pill.green  { background: var(--credit-green); }
.credits-pill.orange { background: var(--credit-orange); }
.credits-pill.red    { background: var(--credit-red); }
.credits-pill:active { filter: brightness(0.9); }

/* Messages page keeps a normal header; back returns to the Looks page */
.messages-title {
    flex: 1;
    font-size: var(--font-lg);
    font-weight: var(--font-weight-bold);
    color: var(--text-header);
    margin-left: 8px;
}

/* Above the Call spot rather than across the bottom of it, which is where
   that spot keeps its shutter now. The exact height is set in script: the
   spots are sized there and no fixed number clears them on every phone. */
/* Top left, and smaller than it was.
   It stood above the Call spot, where it landed on the slide caption and had
   to be measured around. Up here the only thing it can reach is the corner of
   the first slide's picture, and 54px keeps it off the speech balloon drawn
   there. The top right belongs to the profile button, so this takes the left. */
.looks-fab {
    position: absolute;
    left: 10px;                /* the card's own edge, opposite the ball */
    top: 16px;                 /* the line the ball opposite stands on */
    width: 54px;
    height: 54px;
    border: 0;
    border-radius: 50%;
    /* The page's blue. The green belongs to the spots' own buttons — Listen
       and Call — and a third green circle beside them read as a third thing
       to press to take a picture with. Blue says this is a way to somewhere
       else, and leaves green to mean the camera. */
    background: var(--badge-unread);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 30;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
    -webkit-tap-highlight-color: transparent;
    transition: transform 140ms ease;
}

.looks-fab:active { transform: scale(0.93); }
.looks-fab svg { width: 26px; height: 26px; }
/* How many are waiting, in green on the blue of the button — the pair the
   other way round from what it was, now the button itself is blue. Green for
   something new is the green the spots already use for something live, and it
   is as far from the blue underneath it in hue as it is close in weight.

   The ball is set into the button rather than laid on top of it: a border of
   nothing, with the fill held inside it, so what shows around the ball is the
   blue it is sitting on rather than an edge drawn round it. */
.looks-fab-badge {
    position: absolute;
    top: -2px;
    right: -2px;
    min-width: 21px;
    height: 21px;
    padding: 0 5px;
    box-sizing: border-box;
    border: 2px solid transparent;
    border-radius: 11px;
    background: var(--action-green);
    background-clip: padding-box;
    color: var(--badge-text);
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* In the dark the blue underneath is the pale one and the green stands off it
   by itself, so the ball is left whole there and given a shadow instead. */
[data-theme="dark"] .looks-fab-badge {
    border-width: 0;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

/* Dropdown items swap with sign-in state */
.signed-out-only { display: none; }
body.signed-out .signed-out-only { display: flex; }
body.signed-out .signed-in-only { display: none; }

/* ==========================================================================
   Invite swipe card — two-sided call confirmation
   Square card centered over the page: caller's profile photo with the name
   on it. Swipe right = accept, left = refuse. Look borrowed from the
   original swipe interface (profile-card / card-selfie / swipe-indicator).
   ========================================================================== */

.invite-card-overlay {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    z-index: 3000;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Room at the top for the tab, which stands above the card and is the one
       part of it that reaches the bar.
       The z-index above does not settle this: #looksCards is positioned and has
       a z-index of its own, so it opens a stacking context and 3000 is a rank
       INSIDE it — the whole container still sits under .looks-topbar, whose
       buttons therefore paint over anything near the top of a card. Rather than
       raise the container over the bar for the life of the app, the card moves
       down far enough that its tab clears it. */
    padding: 68px 10px 10px;
    box-sizing: border-box;
}

.invite-card {
    /* Square, and square on every screen.
       aspect-ratio settles the shape from ONE given dimension. With a height
       and a max-width both set, a narrow phone let the max-width bind, the box
       stopped being square, and the photograph — square, and covering — had its
       sides cut off. Width is now the only constraint that can bind: the height
       term keeps the card inside its half of the screen and the vw term keeps
       it inside the width, so whichever is smaller still leaves a square. */
    height: min(90%, 342px, 88vw);
    aspect-ratio: 1 / 1;
    max-width: 100%;
    --card-edge: 3px;
    border: var(--card-edge) solid #fff;   /* same thin white edge as the spots */
    /* Square under the tab, rounded everywhere else — the same rule the
       explainer's card follows. The left side then runs straight from the
       tab's foot all the way down to the bottom-left corner, which stays
       round; a curve at the top would pull away from the tab's straight foot
       and open a notch between the two. */
    border-radius: 0 20px 20px 20px;
    /* The tab stands above the top edge and must not be clipped; the
       photograph is clipped by .invite-card-clip instead. The same division of
       labour as .look-invite-card, which is the card this one has to match. */
    overflow: visible;
    /* The tab is sized in cqw so it scales with the card it sits on, which
       needs the card to be the container those units are measured against. */
    container-type: inline-size;
    position: relative;
    background: var(--bg-primary);
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
    touch-action: none;
    cursor: grab;
    -webkit-user-select: none;
    user-select: none;
}

#inviteCard { z-index: 5; }

/* Waiting invitations are real cards under the first one, not just a queue
   off-screen. Their stable offsets keep the pile visible without jitter. */
.invite-card-stack-item {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: calc(3 - var(--stack-depth));
    transform: translate(calc(-50% + var(--stack-x)), calc(-50% + var(--stack-y))) rotate(var(--stack-turn));
    pointer-events: none;
    cursor: default;
    opacity: calc(0.9 - (var(--stack-depth) * 0.12));
}

.invite-card-stack-item .invite-card-name { opacity: 0.8; }

/* A card that came because somebody's call reached our listening Look wears
   the listening green on its edge, so it is recognisable on sight as somebody
   who heard us rather than somebody we called. The spot itself no longer
   greens its border — it has the walking light to say that, and the two
   together were saying it twice — but a card has no glow of its own, so here
   the edge is still where the colour goes. The light does not walk here
   either: the card is a thing being decided, and a moving edge would pull
   against the marks the swipe raises. */
.invite-card.from-listening {
    border-color: var(--action-green);
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
}

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

.invite-card.animating {
    transition: transform 0.5s ease, opacity 0.5s ease;
}

/* What the card clips, now that the card itself does not. Square corner under
   the tab for the same reason .look-invite-face has one: a rounded corner there
   curves out from behind the tab's straight foot and shows a notch of the page
   through the join. */
.invite-card-clip {
    position: absolute;
    inset: 0;
    display: block;
    border-radius: 0 17px 17px 17px;
    overflow: hidden;
}

.invite-card-photo {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    pointer-events: none;
}

/* The tab wears .look-invite-tab for its shape and its type; this only says
   the things that are true here and not on the settings card. */
.invite-card-tab {
    z-index: 2;
    pointer-events: none;
}

/* The name sits at the top, leaving the foot of the card to the two buttons */
.invite-card-name {
    position: absolute;
    left: 0; right: 0; top: 0;
    padding: 14px 18px 44px;
    background: linear-gradient(rgba(0, 0, 0, 0.65), transparent);
    color: #fff;
    font-size: 21px;
    font-weight: 600;
    pointer-events: none;
}

/* ---- The two answers, as buttons ----
   The same marks the swipes raise, so pressing one and carrying the card that
   way are plainly the same act. They step aside while the card is moving:
   the mark coming up says it already, and two of each would be a muddle. */
.invite-card-actions {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    padding: 40px 0 16px;
    display: flex;
    justify-content: center;
    gap: 34px;
    background: linear-gradient(transparent, rgba(0, 0, 0, 0.55));
    transition: opacity 140ms ease;
    z-index: 6;
}

/* Leaning one way drops the button for the other, so the card shows a single
   mark of what it is about to do rather than two saying it at once. Past the
   lean it is gone and no longer takes a press. */
.invite-card.to-chat .invite-card-btn.nope,
.invite-card.to-drop .invite-card-btn.like {
    opacity: 0;
    transform: scale(0.8);
    pointer-events: none;
}

.invite-card-btn {
    width: 58px;
    height: 58px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.45));
    -webkit-tap-highlight-color: transparent;
    /* Opacity was left out of this, which is why the button used to vanish in
       a single frame instead of going. */
    transition: transform 120ms ease, opacity 160ms ease;
}

/* How far gone the button for the answer being left behind is, set on the
   card by leanInviteCard as the finger moves. Fading it here rather than at
   the threshold means the card answers the whole way through the gesture. */
.invite-card-btn.nope { opacity: calc(1 - var(--drop-nope, 0)); }
.invite-card-btn.like { opacity: calc(1 - var(--drop-like, 0)); }

/* While a finger is on the card the fade is the finger's; the transition
   would only make it trail. It comes back for the release and for a press on
   one of the buttons, both of which should be watched rather than jumped. */
.invite-card.dragging .invite-card-btn { transition: none; }

.invite-card-btn svg { display: block; width: 100%; height: 100%; }
.invite-card-btn:active { transform: scale(0.92); }

.invite-card-btn.like { color: var(--action-green); }
.invite-card-btn.nope { color: #94331F; }

/* Working overlay — registering a Look takes a while server-side, so the
   spot dims and a ring turns until the server reports it done. */
.look-square-spinner {
    position: absolute;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.35);
    pointer-events: none;
    z-index: 4;
}

.look-square-spinner::before {
    content: '';
    width: 54px;
    height: 54px;
    border: 4px solid rgba(255, 255, 255, 0.3);
    border-top-color: #fff;
    border-radius: 50%;
    animation: lookSquareSpin 900ms linear infinite;
}

.look-square.processing .look-square-spinner {
    display: flex;
}

@keyframes lookSquareSpin {
    to { transform: rotate(360deg); }
}

/* Chat list item - NO DIVIDER LINES */
/* Fresh matches sit above the chat list as balls until they earn a row */
.ball-row {
    display: flex;
    gap: 14px;
    overflow-x: auto;
    padding: 12px var(--space-lg);
    border-bottom: 1px solid var(--divider);
    scrollbar-width: none;
}

.ball-row::-webkit-scrollbar {
    display: none;
}

.ball-item {
    position: relative;
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    width: 68px;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}

/* A match you have not looked at yet, in the same blue as the count on the
   messages button — one colour for everything waiting to be read. It sits
   where the cross sits, since the two never show at once: holding the ball to
   reveal the cross is itself a way of having looked. */
.ball-new {
    position: absolute;
    top: -1px;
    right: 1px;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    background: var(--badge-unread);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

.ball-item.armed .ball-new { display: none; }

/* Hold a ball and this appears on its shoulder; tap it to throw the match
   away. It sits over the avatar's edge, the way a notification dot does. */
.ball-cross {
    position: absolute;
    top: -2px;
    right: 0;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: #D6482B;
    color: #fff;
    display: none;
    align-items: center;
    justify-content: center;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
}

.ball-cross svg {
    width: 12px;
    height: 12px;
}

.ball-item.armed .ball-cross { display: flex; }

.ball-avatar {
    width: 60px;
    height: 60px;
    border-radius: 50%;
    object-fit: cover;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}

.ball-name {
    font-size: 12px;
    color: var(--text-secondary);
    max-width: 68px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.chatlist-item {
    height: var(--chatlist-item-height);
    display: flex;
    align-items: center;
    padding: 0 var(--space-lg) 0 6px;
    cursor: pointer;
    transition: background-color var(--transition-fast);
    position: relative;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    touch-action: pan-y;
    margin-bottom: 2px;
}

.chatlist-item:active {
    background-color: var(--ripple);
}

/* Avatar */
.chatlist-avatar {
    width: 62px;
    height: 62px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    margin-right: var(--space-md);
}

/* Content area */
.chatlist-content {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

.chatlist-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
}

.chatlist-name-container {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    margin-right: var(--space-sm);
}

.chatlist-name {
    font-size: var(--font-lg);
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Unread indicator is the badge only — no bold name */

.chatlist-look-date {
    font-size: var(--font-xs);
    color: var(--text-secondary);
    opacity: 0.8;
    margin-top: 2px;
}

.chatlist-time-container {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-shrink: 0;
}

/* Right column: time row + icon stacked vertically, aligned top-right */
.chatlist-right-col {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    align-self: flex-start;
    flex-shrink: 0;
    margin-left: 8px;
    padding-top: 8px;
    gap: 4px;
}

/* Time row: time + pin + mute icons inline */
.chatlist-time-row {
    display: flex;
    align-items: center;
    gap: 3px;
}

.chatlist-time {
    font-size: 12px;
    color: var(--text-secondary);
    flex-shrink: 0;
    white-space: nowrap;
}

/* Unread indicator is the badge only — no colored time */

.chatlist-pin-icon {
    display: flex;
    align-items: center;
    color: var(--text-secondary);
    opacity: 0.55;
}

.chatlist-pin-icon svg {
    width: 16px;
    height: 16px;
    fill: currentColor;
}

/* Latest selfie pin stays blue */
.chatlist-pin-icon.selfie-pin {
    color: var(--selfie-accent, #4A8CB8);
    opacity: 0.8;
}

/* Message preview row */
.chatlist-preview-row {
    display: flex;
    align-items: center;
    margin-top: 3px;
}

.chatlist-preview {
    font-size: var(--font-sm);
    font-weight: var(--font-weight-normal);
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1;
    margin-right: var(--space-sm);
}

/* Checkmarks tighter together */
.chatlist-preview .check {
    color: var(--check-sent);
    letter-spacing: -3px;
    margin-right: 4px;
}

.chatlist-preview .check.read {
    color: var(--check-read);
}

.chatlist-preview.typing {
    color: var(--typing-text);
    font-style: italic;
}

.chatlist-preview .preview-sender {
    color: var(--text-primary);
    font-weight: var(--font-weight-medium);
}

/* Unread badge */
.chatlist-badge {
    min-width: var(--unread-badge-size);
    height: var(--unread-badge-size);
    border-radius: calc(var(--unread-badge-size) / 2);
    background-color: var(--badge-unread);
    color: var(--badge-text);
    font-size: var(--font-xs);
    font-weight: var(--font-weight-bold);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 6px;
    flex-shrink: 0;
}

/* ==========================================================================
   7. CHAT VIEW (Slides in from right)
   ========================================================================== */

.chat-view {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: var(--bg-primary);
    display: flex;
    flex-direction: column;
    transform: translateX(100%);
    transition: transform var(--transition-normal), visibility 0s var(--transition-normal);
    z-index: 150;
    overflow: hidden;
    visibility: hidden;
}

.chat-view.active {
    transform: translateX(0);
    visibility: visible;
    transition: transform var(--transition-normal), visibility 0s 0s;
}

/* Messages container with geometric wallpaper pattern */
.chat-messages {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    background-color: var(--bg-chat);
    background-image: url("data:image/svg+xml,%3Csvg width='250' height='250' viewBox='0 0 250 250' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23000000' stroke-opacity='0.08' stroke-width='1.2'%3E%3C!-- Circles --%3E%3Ccircle cx='25' cy='35' r='14'/%3E%3Ccircle cx='180' cy='25' r='10'/%3E%3Ccircle cx='95' cy='100' r='18'/%3E%3Ccircle cx='210' cy='120' r='12'/%3E%3Ccircle cx='50' cy='190' r='16'/%3E%3Ccircle cx='160' cy='200' r='11'/%3E%3Ccircle cx='130' cy='60' r='8'/%3E%3C!-- Squares --%3E%3Crect x='120' y='150' width='20' height='20' rx='3'/%3E%3Crect x='15' y='120' width='16' height='16' rx='2'/%3E%3Crect x='200' cy='170' width='22' height='22' rx='3'/%3E%3Crect x='70' y='220' width='14' height='14' rx='2'/%3E%3Crect x='230' y='50' width='12' height='12' rx='2'/%3E%3C!-- Triangles --%3E%3Cpath d='M65 25 L78 50 L52 50 Z'/%3E%3Cpath d='M220 75 L238 105 L202 105 Z'/%3E%3Cpath d='M30 80 L42 102 L18 102 Z'/%3E%3Cpath d='M140 135 L158 165 L122 165 Z'/%3E%3Cpath d='M105 215 L118 240 L92 240 Z'/%3E%3Cpath d='M185 230 L195 248 L175 248 Z'/%3E%3C!-- Diamonds --%3E%3Cpath d='M170 90 L182 108 L170 126 L158 108 Z'/%3E%3Cpath d='M85 65 L95 78 L85 91 L75 78 Z'/%3E%3Cpath d='M225 195 L235 208 L225 221 L215 208 Z'/%3E%3Cpath d='M40' y='145 L48 155 L40 165 L32 155 Z'/%3E%3C!-- Spirals --%3E%3Cpath d='M55 140 Q60 135 65 140 Q70 145 65 150 Q58 155 52 148 Q48 142 55 138'/%3E%3Cpath d='M195 145 Q202 138 210 145 Q218 152 210 160 Q200 168 192 158 Q186 150 195 143'/%3E%3Cpath d='M110 20 Q115 15 120 20 Q125 25 120 30 Q113 35 108 28 Q104 22 110 18'/%3E%3C!-- Wiggles --%3E%3Cpath d='M10 50 Q20 45 30 50 Q40 55 50 50 Q60 45 70 50'/%3E%3Cpath d='M150 5 Q160 0 170 5 Q180 10 190 5 Q200 0 210 5'/%3E%3Cpath d='M5 200 Q15 195 25 200 Q35 205 45 200'/%3E%3Cpath d='M240 130 Q245 125 250 130 Q245 135 240 130'/%3E%3C!-- Small hexagons --%3E%3Cpath d='M40 230 L48 224 L56 230 L56 242 L48 248 L40 242 Z'/%3E%3Cpath d='M145 85 L153 79 L161 85 L161 97 L153 103 L145 97 Z'/%3E%3C!-- Plus signs --%3E%3Cpath d='M235 25 L235 35 M230 30 L240 30'/%3E%3Cpath d='M75 175 L75 185 M70 180 L80 180'/%3E%3Cpath d='M15 165 L15 175 M10 170 L20 170'/%3E%3C/g%3E%3C/svg%3E");
    background-size: 250px 250px;
    background-attachment: local; /* Wallpaper scrolls with content */
    padding: var(--space-sm) var(--space-lg);
    padding-bottom: 330px; /* Space for composer panel (300px content + 30px handle/margins) */
}


/* Look Photo Display in Chat - Slideable Panel with Spring Animation */
.look-photo-display {
    position: absolute;
    top: 56px; /* Below header */
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    background: var(--bg-primary);
    z-index: 10;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    /* Spring animation timing */
    --photo-spring: linear(
        0, 0.006, 0.025 2.8%, 0.101 6.1%, 0.539 18.9%, 0.721 25.3%, 0.849 31.5%,
        0.937 38.1%, 0.968 41.8%, 0.991 45.7%, 1.006 50.1%, 1.015 55%, 1.017 63.9%,
        1.001
    );
}

/* Fallback for browsers without linear() */
@supports not (animation-timing-function: linear(0, 1)) {
    .look-photo-display {
        --photo-spring: cubic-bezier(0.32, 0.72, 0, 1);
    }
}

.look-photo-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: var(--space-md);
    width: 100%;
    transition:
        max-height 400ms var(--photo-spring),
        padding 400ms var(--photo-spring),
        opacity 250ms ease;
    max-height: 60vh; /* Max 60% of viewport height — matches P2P panel */
    will-change: max-height, padding;
}

.look-photo-display.collapsed .look-photo-content {
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    opacity: 0;
    pointer-events: none;
}

.look-photo-img {
    max-width: 100%;
    max-height: 100%;
    border-radius: 10px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
    object-fit: contain;
    border: 4px solid var(--selfie-accent, #4A8CB8); /* Default: blue for selfie */
}

/* Green border for interest Look photos */
.look-photo-display.interest .look-photo-img {
    border-color: var(--interest-accent, #5A9E60);
}

/* Slide handle for photo panel - iOS style */
.photo-slide-handle {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 8px 0;
    cursor: pointer;
    touch-action: none;
    width: 100%;
    background: var(--bg-primary);
    border-bottom: 1px solid var(--divider);
    -webkit-tap-highlight-color: transparent;
}

.photo-handle-bar {
    width: 36px;
    height: 5px;
    background: rgba(0, 0, 0, 0.12);
    border-radius: 2.5px;
    transition: transform 0.15s ease, background 0.15s ease;
}

.photo-slide-handle:active .photo-handle-bar {
    transform: scaleX(1.08);
    background: rgba(0, 0, 0, 0.2);
}

/* Dark mode handle */
[data-theme="dark"] .photo-handle-bar {
    background: rgba(255, 255, 255, 0.2);
}

[data-theme="dark"] .photo-slide-handle:active .photo-handle-bar {
    background: rgba(255, 255, 255, 0.35);
}

/* Chevron indicator */
.photo-slide-chevron {
    width: 12px;
    height: 12px;
    margin-left: 6px;
    color: var(--text-secondary);
    opacity: 0.4;
    transition: transform 400ms var(--photo-spring), opacity 0.15s ease;
}

.photo-slide-handle:active .photo-slide-chevron {
    opacity: 0.6;
}

.look-photo-display.collapsed .photo-slide-chevron {
    transform: rotate(180deg);
}

/* Pulsating animation for processing photos */
.look-photo-img.processing {
    animation: photo-pulse 1.5s ease-in-out infinite;
}

@keyframes photo-pulse {
    0%, 100% {
        transform: scale(1);
    }
    50% {
        transform: scale(1.02);
    }
}

/* P2P Profile Photo Display in Chat - Slideable Panel with Spring Animation */
.p2p-photo-display {
    position: fixed;
    top: 56px; /* Below header */
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    background: var(--bg-primary);
    z-index: 1000; /* Above composer panel (900) */
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    /* Uses same spring timing as Look photo */
    --photo-spring: linear(
        0, 0.006, 0.025 2.8%, 0.101 6.1%, 0.539 18.9%, 0.721 25.3%, 0.849 31.5%,
        0.937 38.1%, 0.968 41.8%, 0.991 45.7%, 1.006 50.1%, 1.015 55%, 1.017 63.9%,
        1.001
    );
}

@supports not (animation-timing-function: linear(0, 1)) {
    .p2p-photo-display {
        --photo-spring: cubic-bezier(0.32, 0.72, 0, 1);
    }
}

.p2p-photo-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: var(--space-md);
    width: 100%;
    transition:
        max-height 400ms var(--photo-spring),
        padding 400ms var(--photo-spring),
        opacity 250ms ease;
    max-height: 60vh; /* Max 60% of viewport height */
    will-change: max-height, padding;
}

.p2p-photo-display.collapsed .p2p-photo-content {
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    opacity: 0;
    pointer-events: none;
}

.p2p-photo-display.collapsed .photo-slide-chevron {
    transform: rotate(180deg);
}

.p2p-photo-img {
    max-width: 100%;
    max-height: 100%;
    border-radius: 10px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
    object-fit: contain;
}

/* Message wrapper for alignment - more spacing like WhatsApp */
.message-wrapper {
    display: flex;
    margin-bottom: 2px;
}

.message-wrapper.outgoing {
    justify-content: flex-end;
}

.message-wrapper.incoming {
    justify-content: flex-start;
}

/* Add more spacing between different senders */
.message-wrapper.outgoing + .message-wrapper.incoming,
.message-wrapper.incoming + .message-wrapper.outgoing {
    margin-top: var(--space-sm);
}

/* System messages (e.g., "Match created") */
.message-wrapper.system {
    display: flex;
    justify-content: center;
    margin: var(--space-sm) 0;
}

.message-system {
    text-align: center;
    color: var(--text-secondary);
    font-size: var(--font-sm);
    padding: 4px var(--space-lg);
    background: rgba(0, 0, 0, 0.04);
    border-radius: 12px;
}

[data-theme="dark"] .message-system {
    background: rgba(255, 255, 255, 0.06);
}


/* Message bubble - more rounded */
.message-bubble {
    max-width: var(--bubble-max-width);
    padding: 6px 8px;
    border-radius: var(--bubble-radius);
    position: relative;
    word-wrap: break-word;
    box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.08);
    font-size: 16px;
}

/* Outgoing bubble with tail */
.message-wrapper.outgoing .message-bubble {
    background-color: var(--bubble-outgoing);
    border-top-right-radius: 2px;
    margin-right: 12px;
}

.message-wrapper.outgoing .message-bubble::after {
    content: '';
    position: absolute;
    top: 0;
    right: -10px;
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 0 0 12px 10px;
    border-color: transparent transparent transparent var(--bubble-outgoing);
}

/* Incoming bubble with tail */
.message-wrapper.incoming .message-bubble {
    background-color: var(--bubble-incoming);
    border-top-left-radius: 2px;
    margin-left: 12px;
}

.message-wrapper.incoming .message-bubble::after {
    content: '';
    position: absolute;
    top: 0;
    left: -10px;
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 0 10px 12px 0;
    border-color: transparent var(--bubble-incoming) transparent transparent;
}

/* Message text with inline timestamp */
.message-text {
    font-size: 16px;
    font-weight: 400;
    color: var(--text-primary);
    line-height: 1.4;
    display: inline;
}

/* Message meta (timestamp + status) - inline with text */
.message-meta {
    display: inline-flex;
    align-items: center;
    margin-left: var(--space-sm);
    vertical-align: bottom;
    position: relative;
    top: 3px;
    float: right;
    padding-left: var(--space-sm);
}

.message-time {
    font-size: var(--font-xs);
    color: var(--text-timestamp);
}

/* Checkmarks tighter together */
.message-status {
    font-size: 14px;
    color: var(--check-sent);
    letter-spacing: -4px;
    margin-left: 1px;
    padding-right: 2px;
}

.message-status.read {
    color: var(--check-read);
}

/* ==========================================================================
   8. INPUT BAR
   ========================================================================== */

.input-bar {
    min-height: var(--input-bar-height);
    background: var(--bg-primary);
    display: flex;
    align-items: center;
    padding: var(--space-sm) var(--space-md);
    flex-shrink: 0;
}

/* Message field with the badge attachment control on its right. */
.input-field-container {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    background-color: var(--bubble-incoming);
    border: 1px solid var(--input-border);
    border-radius: 24px;
    padding: 0 var(--space-sm);
    margin-right: var(--space-sm);
    height: 46px;
}

.input-field {
    flex: 1;
    height: 46px;
    line-height: 46px;
    border: none;
    padding: 0 var(--space-sm);
    font-size: 16px;
    font-family: var(--font-family);
    color: var(--text-primary);
    background-color: transparent;
    outline: none;
}

.input-field::placeholder {
    color: var(--text-secondary);
}

.input-send {
    width: 46px;
    height: 46px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    border-radius: 50%;
    background-color: var(--accent);
    flex-shrink: 0;
    transition: background-color var(--transition-fast);
}

.input-send:active {
    opacity: 0.8;
}

.input-send svg {
    width: 20px;
    height: 20px;
    fill: #FFFFFF;
    margin-left: 2px;
}

/* ==========================================================================
   9. UTILITY CLASSES
   ========================================================================== */

.hidden {
    display: none !important;
}

/* Prevent text selection on interactive elements */
.no-select {
    -webkit-user-select: none;
    user-select: none;
}

/* ==========================================================================
   LOADING SCREEN (shown after auth, hidden when content is ready)
   ========================================================================== */

.loading-screen {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-primary);
    transition: opacity 0.3s ease-out;
}

.loading-screen.fade-out {
    opacity: 0;
    pointer-events: none;
}

.loading-logo {
    width: 80px;
    height: 80px;
    animation: pulse-logo 1.5s ease-in-out infinite;
}

@keyframes pulse-logo {
    0%, 100% { transform: scale(1); opacity: 0.7; }
    50% { transform: scale(1.08); opacity: 1; }
}

/* ==========================================================================
   10. SIGN-IN SCREEN
   ========================================================================== */

.signin-screen {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: linear-gradient(135deg, #1F5C94 0%, #153D6B 100%);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
}

.signin-content {
    text-align: center;
    padding: var(--space-2xl);
    max-width: 400px;
}

.signin-logo {
    width: 40px;
    height: 40px;
    margin: 0 auto var(--space-lg);
    display: flex;
    align-items: center;
    justify-content: center;
}

.signin-logo img {
    width: 40px;
    height: 40px;
    object-fit: contain;
}

.signin-title {
    font-size: 32px;
    font-weight: var(--font-weight-semibold);
    color: #FFFFFF;
    margin: 0 0 var(--space-md);
    font-family: var(--font-family);
}

.signin-subtitle {
    font-size: var(--font-md);
    color: rgba(255, 255, 255, 0.9);
    margin: 0 0 var(--space-2xl);
    font-family: var(--font-family);
}

.signin-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-md);
    background-color: #FFFFFF;
    color: #5f6368;
    border: none;
    border-radius: 24px;
    padding: 14px 28px;
    font-size: var(--font-md);
    font-weight: var(--font-weight-medium);
    font-family: var(--font-family);
    cursor: pointer;
    transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 150ms cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    user-select: none;
}

.signin-btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

.signin-btn:active {
    transform: translateY(0);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}

.signin-btn svg {
    width: 20px;
    height: 20px;
    color: #1F5C94;
}

.signin-footer {
    margin-top: var(--space-lg);
    font-size: 13px;
    color: rgba(255, 255, 255, 0.6);
    font-family: var(--font-family);
}

/* ==========================================================================
   11. FAB (Floating Action Button)
   ========================================================================== */

.fab {
    position: fixed;
    bottom: 24px;
    right: 24px;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background-color: var(--accent);
    border: none;
    box-shadow: 0 4px 12px rgba(31, 92, 148, 0.4);
    cursor: pointer;
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 200ms ease, box-shadow 200ms ease;
}

.fab:hover {
    transform: scale(1.05);
    box-shadow: 0 6px 16px rgba(31, 92, 148, 0.5);
}

.fab:active {
    transform: scale(0.95);
}

.fab svg {
    width: 28px;
    height: 28px;
    fill: #FFFFFF;
}

.fab.active {
    transform: rotate(45deg);
}

/* FAB positioning based on tab */
.fab.looks-tab {
    bottom: 38px; /* Slightly higher to sit above collapsed composer panel handle (~14px difference) */
}

.fab.promoted-tab {
    bottom: 24px; /* Standard position */
}

/* FAB Menu */
.fab-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.3);
    z-index: 89;
    opacity: 0;
    pointer-events: none;
    transition: opacity 200ms ease;
}

.fab-overlay.active {
    opacity: 1;
    pointer-events: auto;
}

.fab-menu {
    position: fixed;
    bottom: 96px;
    right: 24px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    z-index: 91;
    opacity: 0;
    transform: translateY(20px) scale(0.9);
    pointer-events: none;
    transition: opacity 200ms ease, transform 200ms ease;
}

.fab-menu.active {
    opacity: 1;
    transform: translateY(0) scale(1);
    pointer-events: auto;
}

.fab-menu-item {
    display: flex;
    align-items: center;
    gap: 10px;
    background-color: var(--bg-primary);
    border-radius: 20px;
    padding: 8px 14px 8px 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    cursor: pointer;
    transition: transform 150ms ease;
    white-space: nowrap;
}

.fab-menu-item:hover {
    transform: scale(1.02);
}

.fab-menu-item:active {
    transform: scale(0.98);
}

.fab-menu-icon {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
}

.fab-menu-icon.selfie {
    background-color: #E3F2FD;
}

.fab-menu-icon.interest {
    background-color: #FFF3E0;
}

[data-theme="dark"] .fab-menu-icon.selfie {
    background-color: #1E3A5F;
}

[data-theme="dark"] .fab-menu-icon.interest {
    background-color: #3E2723;
}

.fab-menu-icon svg {
    width: 24px;
    height: 24px;
    fill: var(--accent);
}

.fab-menu-icon.interest svg {
    fill: #5A9E60;
}

.fab-menu-item span {
    font-size: var(--font-md);
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
}

/* ==========================================================================
   11b. IN-APP CAMERA OVERLAY
   ========================================================================== */

/* When native camera is active, make WebView transparent so CameraX shows through */
body.native-camera-active,
body.native-camera-active .app-container {
    background: transparent !important;
}
body.native-camera-active .app-container > *:not(.camera-overlay) {
    visibility: hidden;
}

.location-permission-modal {
    position: fixed;
    inset: 0;
    z-index: 100000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}
.location-permission-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
}
.location-permission-card {
    position: relative;
    background: var(--color-bg, #fff);
    color: var(--color-text, #000);
    border-radius: 12px;
    padding: 24px;
    max-width: 340px;
    width: 100%;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
    text-align: center;
}
.location-permission-card h3 {
    margin: 0 0 12px 0;
    font-size: 18px;
}
.location-permission-card p {
    margin: 0 0 20px 0;
    font-size: 14px;
    line-height: 1.4;
    opacity: 0.8;
}
.location-permission-btn {
    display: block;
    width: 100%;
    padding: 12px;
    background: #6366f1;
    color: #fff;
    border: none;
    border-radius: 8px;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    margin-bottom: 8px;
}
.location-permission-cancel {
    display: block;
    width: 100%;
    padding: 10px;
    background: transparent;
    color: inherit;
    border: none;
    font-size: 14px;
    cursor: pointer;
    opacity: 0.6;
}

.camera-overlay {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: transparent;
    z-index: 99999;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    touch-action: none;
}

.camera-overlay #cameraVideo {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    object-fit: cover;
    background: #000;
}

.camera-bottom-bar {
    position: absolute;
    bottom: 0; left: 0; right: 0;
    display: flex;
    align-items: center;
    justify-content: space-evenly;
    padding: 24px 20px 40px;
    background: linear-gradient(transparent, rgba(0,0,0,0.6));
}

.camera-btn {
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    -webkit-tap-highlight-color: transparent;
}

.camera-btn-side {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: rgba(255,255,255,0.2);
}

.camera-btn-shutter {
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background: transparent;
    border: 4px solid #fff;
    padding: 4px;
}

.camera-shutter-inner {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    background: #fff;
}

.camera-btn-shutter:active .camera-shutter-inner {
    background: #ccc;
}

/* Interest photo history thumbnails */
.interest-history-strip {
    position: absolute;
    bottom: 140px; left: 0; right: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 0 20px;
    z-index: 1;
}
.interest-history-thumb {
    height: 80px;
    border-radius: 8px;
    object-fit: cover;
    border: 2px solid rgba(255,255,255,0.6);
    cursor: pointer;
    transition: transform 0.15s;
    flex-shrink: 1;
    min-width: 0;
}
.interest-history-thumb:active {
    transform: scale(0.92);
}
.interest-history-clear {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: rgba(184,90,90,0.85);
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

/* ==========================================================================
   12. COMPOSE SHEET
   ========================================================================== */

.compose-sheet {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: var(--bg-primary);
    z-index: 500;
    transform: translateY(100%);
    transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

.compose-sheet.active {
    transform: translateY(0);
}

.compose-sheet-content {
    height: 100%;
    display: flex;
    flex-direction: column;
}

.compose-header {
    height: var(--header-height);
    background-color: var(--bg-header);
    display: flex;
    align-items: center;
    padding: 0 var(--space-sm);
}

.compose-close {
    width: var(--min-touch-target);
    height: var(--min-touch-target);
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    cursor: pointer;
    border-radius: 50%;
}

.compose-close svg {
    width: 24px;
    height: 24px;
    fill: var(--text-header);
}

.compose-title {
    flex: 1;
    font-size: var(--font-lg);
    font-weight: var(--font-weight-bold);
    color: var(--text-header);
    margin-left: var(--space-sm);
}

.compose-preview {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--bg-chat);
    position: relative;
    overflow: hidden;
}

.compose-preview-img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    display: none;
}

.compose-preview-img.visible {
    display: block;
}

.compose-preview-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    width: 100%;
    height: 100%;
    padding: 60px var(--space-lg) var(--space-2xl);
}

.compose-preview-placeholder.hidden {
    display: none;
}

.compose-photo-options {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    width: 100%;
    max-width: 280px;
}

.compose-photo-option {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-sm);
    padding: 12px 12px;
    background: linear-gradient(135deg, #1F5C94 0%, #153D6B 100%);
    border: none;
    border-radius: 12px;
    color: white;
    cursor: pointer;
    transition: all 0.2s ease;
    box-shadow: 0 4px 12px rgba(31, 92, 148, 0.2);
    position: relative;
}

.compose-photo-option:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(31, 92, 148, 0.3);
}

.compose-photo-option:active {
    transform: translateY(0);
    box-shadow: 0 2px 8px rgba(31, 92, 148, 0.2);
}

.compose-photo-option-icon {
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.15);
    border-radius: 50%;
    margin-bottom: 2px;
}

.compose-photo-option-icon svg {
    width: 24px;
    height: 24px;
    fill: white;
}

.compose-photo-option-label {
    font-size: 14px;
    font-weight: var(--font-weight-semibold);
    color: white;
}

.compose-photo-option-dev {
    font-size: var(--font-xs);
    font-weight: var(--font-weight-medium);
    color: rgba(255, 255, 255, 0.7);
    background: rgba(255, 255, 255, 0.1);
    padding: 4px 12px;
    border-radius: 12px;
    margin-top: 4px;
}


/* ==========================================================================
   13. LOOK CHAT VIEW (Card-style messages)
   ========================================================================== */

/* Look chat header shows Look photo + timestamp */
.header-look-info {
    display: flex;
    flex-direction: column;
    align-items: center;
    flex: 1;
}

.header-look-timestamp {
    font-size: var(--font-xs);
    color: var(--text-header);
    opacity: 0.8;
}

/* Look chat messages container */
.look-messages {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    background-color: var(--bg-chat);
    padding: var(--space-md);
}

/* Look items with embedded chat heads */
.chatlist-item.look-chat {
    height: auto;
    min-height: var(--chatlist-item-height);
    align-items: center;
    padding: 0 5px 0 0;
    background: #EBEBEB;
    margin: 0 12px 6px 25px;
    border-radius: 8px 999px 999px 8px;
    overflow: hidden;
    width: fit-content;
    max-width: calc(100% - 37px);
    position: relative;
}

/* Vertical accent band at the start of Look items */
.chatlist-item.look-chat::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 12px;
    background: #4A8CB8;
    z-index: 1;
    border-radius: 8px 0 0 8px;
}
.chatlist-item.look-chat.interest::before {
    background: #5A9E60;
}
[data-theme="dark"] .chatlist-item.look-chat::before {
    background: #5C9AC7;
}
[data-theme="dark"] .chatlist-item.look-chat.interest::before {
    background: #72AD77;
}

[data-theme="dark"] .chatlist-item.look-chat {
    background: #163550;
}

/* Pin badge for latest selfie look — top-right corner */
.look-pin-badge {
    position: absolute;
    top: -4px;
    right: -6px;
    z-index: 2;
    color: var(--selfie-accent, #4A8CB8);
    line-height: 0;
    transform: rotate(25deg);
}

/* Left column: Look avatar */
.look-left-col {
    width: 62px;
    flex-shrink: 0;
    margin-left: 12px;
    margin-right: 24px;
    position: relative;
    align-self: stretch;
    border-right: 9px solid #4A8CB8;
}
.chatlist-item.look-chat.interest .look-left-col {
    border-right-color: #5A9E60;
}
[data-theme="dark"] .look-left-col {
    border-right-color: #5C9AC7;
}
[data-theme="dark"] .chatlist-item.look-chat.interest .look-left-col {
    border-right-color: #72AD77;
}

/* Look avatar — borderless, fills the left column */
.look-item-avatar {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border-radius: 0;
    object-fit: cover;
    display: block;
}

/* Timestamp above avatar */
.look-item-time {
    font-size: var(--font-xs);
    color: var(--text-secondary);
    white-space: nowrap;
    display: flex;
    align-items: center;
    gap: 3px;
}

/* Body */
.look-item-body {
    flex: 0 1 auto;
    display: flex;
    flex-direction: column;
    gap: 5px;
    min-width: 0;
    overflow: hidden;
}

.look-item-name {
    font-size: var(--font-sm);
    font-weight: var(--font-weight-medium);
    color: var(--selfie-accent);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chatlist-item.look-chat.interest .look-item-name {
    color: var(--interest-accent);
}

/* Chat heads row — fills from left; grows rightward */
.look-item-heads {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: visible;
    scrollbar-width: none;
    justify-content: flex-start;
    gap: 11px;
    padding-right: 0;
}

/* Fade right edge only when heads actually overflow */
.look-item-heads.has-overflow {
    mask-image: linear-gradient(to left, transparent 0, black 20px);
    -webkit-mask-image: linear-gradient(to left, transparent 0, black 20px);
}

.look-item-heads::-webkit-scrollbar {
    display: none;
}

.look-chat-head {
    position: relative;
    flex-shrink: 0;
    cursor: pointer;
}

.look-chat-head-avatar {
    width: 68px;
    height: 68px;
    border-radius: 50%;
    object-fit: cover;
    display: block;
}

/* Empty state: no match yet */
.look-no-matches-text {
    font-size: var(--font-xs);
    color: var(--text-secondary);
    opacity: 0.7;
    white-space: nowrap;
    align-self: center;
    margin-right: 8px;
}

/* Dark mode: light grey for both pill types */
[data-theme="dark"] .chatlist-item.look-chat .look-no-matches-text,
[data-theme="dark"] .chatlist-item.look-chat.interest .look-no-matches-text {
    color: #B0BAC4;
    opacity: 1;
}

/* New match notification dot */
.look-match-dot {
    display: none;
    position: absolute;
    top: 1px;
    right: 1px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--selfie-accent);
    border: 2px solid var(--bg-primary);
}

.look-chat-head.new-match .look-match-dot {
    display: block;
}

.chatlist-item.look-chat.interest .look-match-dot {
    background: var(--interest-accent);
}

/* Processing subtitle on Look items — animated dots */
.chatlist-look-processing {
    font-size: var(--font-sm);
    color: var(--text-secondary);
    font-weight: 500;
}
.chatlist-look-processing::after {
    display: inline-block;
    width: 1.2em;
    text-align: left;
    content: '';
    animation: processing-dots 1.5s steps(4, end) infinite;
}
@keyframes processing-dots {
    0%   { content: ''; }
    25%  { content: '.'; }
    50%  { content: '..'; }
    75%  { content: '...'; }
    100% { content: ''; }
}

/* Expired Look items — greyed out */
.chatlist-item.look-chat.expired {
    opacity: 0.5;
}
.chatlist-look-expired {
    font-size: var(--font-xs);
    color: var(--text-primary);
    font-style: italic;
    align-self: center;
    white-space: nowrap;
    flex-shrink: 0;
    margin-right: 8px;
}

.chatlist-look-failed {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: var(--font-xs);
    color: #e53935;
}

.look-retry-btn {
    padding: 4px 12px;
    border-radius: 12px;
    border: 1px solid #e53935;
    background: transparent;
    color: #e53935;
    font-size: var(--font-xs);
    cursor: pointer;
    font-weight: 600;
}

/* Chat avatar border removed — pill color differentiates selfie vs interest */

/* Look rows inside the composer history panel — tighter margins */
.look-row {
    width: 100%;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}
.look-row.selected {
    background-color: rgba(31, 92, 148, 0.12);
}
[data-theme="dark"] .look-row.selected {
    background-color: rgba(30, 58, 95, 0.35);
}
.history-panel .chatlist-item.look-chat {
    margin: 0 4px 6px 4px;
    max-width: calc(100% - 8px);
}

/* ==========================================================================


   14. SAFE AREA INSETS (for notched devices)
   ========================================================================== */

/* These are inert while the viewport is not viewport-fit=cover: without it
   the browser keeps the page inside the safe area itself and every env()
   below resolves to zero. They are kept because they are what the app would
   need the moment it draws edge to edge again — and the reason it does not is
   worth knowing. Drawing under the system bar put the page's own background
   there, and on the Looks page, which has no header to fill it, that was
   white while theme-color went on telling Android to draw the battery and
   the clock in white. The bar belongs to the phone; the app stays under it. */

@supports (padding-top: env(safe-area-inset-top)) {
    .header {
        padding-top: env(safe-area-inset-top);
        height: calc(var(--header-height) + env(safe-area-inset-top));
    }

    .input-bar {
        padding-bottom: env(safe-area-inset-bottom);
        height: calc(var(--input-bar-height) + env(safe-area-inset-bottom));
    }

    .fab {
        bottom: calc(24px + env(safe-area-inset-bottom));
    }

    .fab-menu {
        bottom: calc(96px + env(safe-area-inset-bottom));
    }

    .compose-header {
        padding-top: env(safe-area-inset-top);
        height: calc(var(--header-height) + env(safe-area-inset-top));
    }

    /* The Looks page has no header, so the page itself keeps clear of the
       status bar and the profile button sits below it. */
    .look-squares {
        padding-top: calc(10px + env(safe-area-inset-top));
        /* The buttons stand at the foot of the page now rather than inside
           the spots, so the bottom needs the same clearance — a gesture bar
           would otherwise sit on top of Listen and Call. */
        padding-bottom: calc(10px + env(safe-area-inset-bottom));
    }

    .looks-topbar {
        padding-top: calc(16px + env(safe-area-inset-top));
    }

    /* The messages button is placed rather than padded, so the safe area does
       not reach it on its own. Without this it keeps its 8px while the two
       opposite are pushed down the height of the status bar, and the line
       they are all supposed to stand on comes apart on exactly the phones
       that have a notch. */
    .looks-fab {
        top: calc(16px + env(safe-area-inset-top));
    }
}

/* =============================================
   PROFILE EDITOR PANEL
   ============================================= */

.profile-editor-panel {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--bg-primary);
    transform: translateX(100%);
    transition: transform 0.3s ease;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}

.profile-editor-panel.active {
    transform: translateX(0);
}

.profile-editor-header {
    height: var(--header-height);
    background: var(--header-bg);
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 0 16px;
    border-bottom: 1px solid var(--border-color);
}

.profile-editor-back {
    background: none;
    border: none;
    padding: 8px;
    cursor: pointer;
    color: var(--text-color);
    border-radius: 50%;
    transition: background 0.2s;
    display: flex;
    align-items: center;
    justify-content: center;
}

.profile-editor-back:hover {
    background: var(--hover-bg);
}

.profile-editor-back svg {
    width: 24px;
    height: 24px;
    fill: currentColor;
}

.profile-editor-title {
    font-size: 20px;
    font-weight: 600;
    color: var(--text-color);
    margin: 0;
}

.profile-editor-content {
    flex: 1;
    overflow-y: auto;
    padding: 24px 16px;
}

.profile-editor-photo-section {
    display: flex;
    justify-content: center;
    margin-bottom: 32px;
}

.profile-editor-photo-container {
    /* Only the placement. Everything that makes it a CARD — the white border,
       the square corner under the tab, the 1:1 shape, the container-type the
       cqw units inside it measure against — comes from .look-invite-card, so
       there is one description of the card in the stylesheet and not two.
       On the slide it is pinned to a stage; here it sits in the flow. */
    position: relative;
    left: auto;
    top: auto;
    transform: none;
    width: 199px;                  /* 150, then 15% twice */
    height: auto;
    /* The tab stands above the card's top edge and must not be clipped. The
       photograph is clipped by .look-invite-face, which is where it belongs. */
    overflow: visible;
    cursor: pointer;
    /* A shadow in pixels rather than cqw: this card is one fixed size, and the
       slide's is not. */
    filter: drop-shadow(0 3px 10px rgba(20, 28, 36, 0.22));
    transition: transform 0.2s;
}

.profile-editor-photo-container:hover {
    transform: scale(1.05);
}

.profile-editor-photo {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.profile-editor-photo-overlay {
    position: absolute;
    inset: 0;
    /* Darkest where the words are, and barely there at the edges.
       A flat 40% veil kept the label legible over any photograph by dimming
       the whole card, including all the parts nobody needs to read through.
       A radial one spends the shade where it is actually doing work: the
       corners come back to almost the real picture. The middle keeps the
       full 40% it always had, because that is what holds "Edit Photo" over
       a white shirt in daylight — the brightening is paid for out of the
       parts of the card nobody has to read through, not out of the label. */
    background: radial-gradient(ellipse at center,
        rgba(0, 0, 0, 0.40) 0%,
        rgba(0, 0, 0, 0.34) 40%,
        rgba(0, 0, 0, 0.10) 100%);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    color: white;
    /* The face's own corners, so the darkening stops where the picture does
       rather than squaring off over the white border. */
    border-radius: 0 4.97cqw 4.97cqw 4.97cqw;
    /* Above the name, which is what it is inviting a change to. */
    z-index: 2;
}

.profile-editor-photo-overlay svg {
    width: 32px;
    height: 32px;
    fill: currentColor;
    /* Its own contrast, so it does not depend on the veil being heavy. */
    filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.75));
}

.profile-editor-photo-overlay span {
    font-size: 14px;
    font-weight: 500;
    /* Same reasoning as the icon: the words carry their own dark edge, which
       is what lets the veil come down without them going soft on a light
       photograph. */
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85), 0 0 8px rgba(0, 0, 0, 0.45);
}

.profile-editor-name-section {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.profile-editor-label {
    font-size: 14px;
    font-weight: 500;
    color: var(--text-secondary);
    margin-bottom: 4px;
}

/* An input with something sitting inside it. */
.profile-editor-field {
    position: relative;
    display: block;
}

/* How many characters are left, once that has become worth knowing.
   Inside the box on the right, and light: it is a warning about a limit, not a
   thing to read while typing. The input keeps room for it at all times rather
   than only while it shows — text that reflowed the moment the count appeared
   would draw far more attention than the count itself. */
.profile-editor-field .profile-editor-input {
    padding-right: 46px;
}

.profile-editor-count {
    position: absolute;
    right: 14px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    color: var(--text-secondary);
    opacity: 0.5;
    pointer-events: none;
    transition: opacity 0.15s linear;
}

.profile-editor-count:empty { opacity: 0; }

/* The last few say so more plainly. */
.profile-editor-count[data-low] {
    color: var(--accent-red, #c0392b);
    opacity: 0.8;
}

/* Under a field, saying what it is for. Lighter and smaller than the label
   above it, so the pair reads as one thing rather than two. */
.profile-editor-hint {
    margin: 5px 0 0;
    font-size: 12px;
    color: var(--text-secondary);
    opacity: 0.75;
}

.profile-editor-input {
    width: 100%;
    padding: 11px 15px;
    border: 2px solid var(--text-secondary);
    border-color: color-mix(in srgb, var(--text-secondary) 58%, var(--bg-primary));
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--text-primary);
    font-size: 16px;
    box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.10);
    transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
}

.profile-editor-input:hover {
    border-color: var(--text-secondary);
}

.profile-editor-input:focus {
    outline: none;
    border-color: var(--accent);
    background: var(--input-bg);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent),
                inset 0 1px 2px rgba(0, 0, 0, 0.08);
}

.profile-editor-google-section {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 32px;
    padding-top: 24px;
    border-top: 1px solid var(--border-color);
}

.profile-editor-google-info {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px;
    background: var(--input-bg);
    border-radius: 8px;
    border: 1px solid var(--border-color);
}

.profile-editor-google-photo {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
}

.profile-editor-google-details {
    display: flex;
    flex-direction: column;
    gap: 4px;
    overflow: hidden;
    flex: 1;
}

.profile-editor-google-reset {
    background: none;
    border: 1px solid var(--border-color);
    color: var(--text-secondary);
    font-size: 13px;
    padding: 6px 12px;
    border-radius: 6px;
    cursor: pointer;
    flex-shrink: 0;
}

.profile-editor-google-name {
    font-size: 16px;
    font-weight: 500;
    color: var(--text-color);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.profile-editor-google-email {
    font-size: 14px;
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* =============================================
   NOTIFICATION SETTINGS TOGGLES
   ============================================= */

/* =============================================
   LANGUAGE SELECTOR
   ============================================= */

.profile-editor-language-section {
    margin-top: 32px;
    padding-top: 24px;
    border-top: 1px solid var(--border-color);
}

.language-selector { position: relative; margin-top: 8px; }

.language-selected {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 12px 14px;
    border-radius: 10px;
    border: 1px solid var(--divider);
    background: var(--bg-primary);
    cursor: pointer;
    transition: border-color 0.2s;
}
.language-selected:active { border-color: var(--accent); }
.language-selected .language-flag { width: 24px; height: 16px; border-radius: 2px; object-fit: cover; flex-shrink: 0; }
.language-selected .language-name { flex: 1; font-size: 15px; font-weight: 500; color: var(--text-primary); text-align: left; }
.language-chevron { width: 18px; height: 18px; color: var(--text-secondary); transition: transform 0.25s ease; }
.language-selector.open .language-chevron { transform: rotate(180deg); }

.language-dropdown {
    display: none;
    flex-direction: column;
    margin-top: 6px;
    border-radius: 10px;
    border: 1px solid var(--divider);
    background: var(--bg-primary);
    overflow: hidden;
    max-height: 280px;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}
.language-selector.open .language-dropdown { display: flex; }

.language-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 11px 14px;
    border: none;
    background: none;
    cursor: pointer;
    transition: background 0.15s;
    text-align: left;
    width: 100%;
}
.language-item + .language-item { border-top: 1px solid var(--divider); }
.language-item:active:not(.disabled) { background: var(--ripple); }
.language-item .language-flag { width: 24px; height: 16px; border-radius: 2px; object-fit: cover; flex-shrink: 0; }
.language-item .language-name { font-size: 15px; color: var(--text-primary); }
.language-item.selected { background: rgba(74, 140, 184, 0.12); }
.language-item.selected .language-name { color: var(--accent); font-weight: 600; }
.language-item.disabled { cursor: default; }
.language-item.disabled .language-flag { opacity: 0.35; filter: grayscale(0.6); }
.language-item.disabled .language-name { color: var(--text-secondary); opacity: 0.55; }

/* =============================================
   NOTIFICATION SETTINGS TOGGLES
   ============================================= */

.profile-editor-notifications-section {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 32px;
    padding-top: 24px;
    border-top: 1px solid var(--border-color);
}

/* Blocked Users Section in Settings */
.profile-editor-blocked-section {
    margin-top: 32px;
    padding-top: 24px;
    border-top: 1px solid var(--border-color);
}
.blocked-users-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    cursor: pointer;
    padding: 4px 0;
}
.blocked-users-header .profile-editor-label {
    margin: 0;
    pointer-events: none;
}
.blocked-chevron {
    width: 24px;
    height: 24px;
    fill: var(--text-secondary);
    transition: transform 0.2s ease;
}
.blocked-users-header.expanded .blocked-chevron {
    transform: rotate(180deg);
}
.blocked-users-list {
    margin-top: 8px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.blocked-user-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 12px;
    background: var(--input-bg);
    border-radius: 8px;
    border: 1px solid var(--border-color);
}
.blocked-user-name {
    font-size: 15px;
    color: var(--text-color);
}
.blocked-user-unblock {
    background: none;
    border: 1px solid var(--border-color);
    color: var(--accent-color);
    font-size: 13px;
    padding: 4px 12px;
    border-radius: 6px;
    cursor: pointer;
}
.blocked-user-unblock:active {
    background: rgba(0,0,0,0.05);
}
.blocked-users-empty {
    font-size: 14px;
    color: var(--text-secondary);
    padding: 12px;
    text-align: center;
}

/* Confirmation Dialog */
.confirm-dialog-overlay {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.5);
    z-index: 10000;
    display: flex;
    align-items: center;
    justify-content: center;
}
.confirm-dialog {
    background: var(--bg-primary);
    border-radius: 14px;
    padding: 24px;
    max-width: 320px;
    width: 85%;
    box-shadow: 0 4px 24px rgba(0,0,0,0.3);
}
.confirm-dialog-message {
    font-size: 15px;
    color: var(--text-color);
    line-height: 1.5;
    margin: 0 0 20px 0;
}
.confirm-dialog-actions {
    display: flex;
    gap: 10px;
    justify-content: flex-end;
}
.confirm-dialog-btn {
    padding: 8px 20px;
    border-radius: 8px;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    border: none;
}
.confirm-dialog-cancel {
    background: var(--input-bg);
    color: var(--text-color);
    border: 1px solid var(--border-color);
}
.confirm-dialog-confirm {
    background: #d32f2f;
    color: white;
}
.confirm-dialog-btn:active {
    opacity: 0.8;
}

.notification-toggle-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px;
    background: var(--input-bg);
    border-radius: 8px;
    border: 1px solid var(--border-color);
}

.notification-toggle-label {
    font-size: 15px;
    color: var(--text-color);
}

.notification-toggle {
    position: relative;
    width: 48px;
    height: 28px;
    flex-shrink: 0;
}

.notification-toggle input {
    opacity: 0;
    width: 0;
    height: 0;
}

.notification-slider {
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
    background: #ccc;
    border-radius: 28px;
    cursor: pointer;
    transition: background 0.3s;
}

.notification-slider::before {
    content: '';
    position: absolute;
    width: 22px;
    height: 22px;
    left: 3px;
    bottom: 3px;
    background: white;
    border-radius: 50%;
    transition: transform 0.3s;
}

.notification-toggle input:checked + .notification-slider {
    background: #5A9E60;
}

.notification-toggle input:checked + .notification-slider::before {
    transform: translateX(20px);
}

/* =============================================
   PROFILE CROP MODAL
   ============================================= */

.profile-crop-modal {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0, 0, 0, 0.95);
    z-index: 10001;
    display: none;
    flex-direction: column;
}

.profile-crop-modal.active {
    display: flex;
}

.profile-crop-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px;
    background: rgba(255, 255, 255, 0.05);
}

.profile-crop-title {
    font-size: 18px;
    font-weight: 600;
    color: white;
    margin: 0;
}

.profile-crop-close {
    background: none;
    border: none;
    padding: 8px;
    cursor: pointer;
    color: white;
    border-radius: 50%;
    transition: background 0.2s;
    display: flex;
    align-items: center;
    justify-content: center;
}

.profile-crop-close:hover {
    background: rgba(255, 255, 255, 0.1);
}

.profile-crop-close svg {
    width: 24px;
    height: 24px;
    fill: currentColor;
}

.profile-crop-container {
    flex: 1;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    overflow: hidden;
}

#profileCropCanvas {
    max-width: 100%;
    max-height: 100%;
    touch-action: none;
    cursor: move;
}

.profile-crop-overlay {
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
    pointer-events: none;
    display: none;
}

.profile-crop-controls {
    display: flex;
    gap: 12px;
    padding: 20px;
    background: rgba(255, 255, 255, 0.05);
}

.profile-crop-btn {
    flex: 1;
    padding: 14px 24px;
    border: none;
    border-radius: 8px;
    font-size: 16px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s;
}

.profile-crop-choose {
    background: rgba(255, 255, 255, 0.1);
    color: white;
    text-align: center;
}

.profile-crop-choose:hover {
    background: rgba(255, 255, 255, 0.15);
}

.profile-crop-save {
    background: var(--accent);
    color: white;
}

.profile-crop-save:hover {
    background: #153D6B;
}

.profile-crop-save:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* ==========================================================================
   Interest Photo Crop Modal
   ========================================================================== */

.interest-crop-modal {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.95);
    z-index: 10000;
    display: flex;
    align-items: center;
    justify-content: center;
    animation: fadeIn 0.2s ease;
}

.interest-crop-content {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    background: #000;
    overflow: hidden;
}

.interest-crop-header {
    padding: 20px 24px;
    background: linear-gradient(135deg, #1F5C94 0%, #153D6B 100%);
    color: white;
}

.interest-crop-title {
    font-size: 20px;
    font-weight: 600;
    margin: 0 0 4px 0;
    color: white;
}

.interest-crop-subtitle {
    font-size: 13px;
    margin: 0;
    opacity: 0.9;
    color: rgba(255, 255, 255, 0.9);
}

.interest-crop-container {
    position: relative;
    flex: 1;
    overflow: hidden;
    background: #000;
    display: flex;
    align-items: center;
    justify-content: center;
    touch-action: none;
    user-select: none;
    padding: 0;
    min-height: 0;
}

#interestCropCanvas {
    display: block;
    max-width: 100%;
    max-height: 100%;
    cursor: move;
}

#interestCropCanvas.dragging {
    cursor: grabbing;
}

.interest-crop-controls {
    padding: 16px 24px;
    display: flex;
    gap: 12px;
    background: #1a1a1a;
    border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.interest-crop-btn {
    flex: 1;
    padding: 14px 24px;
    border: none;
    border-radius: 8px;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.2s ease;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.interest-crop-cancel {
    background: rgba(255, 255, 255, 0.1);
    color: white;
}

.interest-crop-cancel:hover {
    background: rgba(255, 255, 255, 0.15);
}

.interest-crop-cancel:active {
    transform: scale(0.98);
}

.interest-crop-use {
    background: linear-gradient(135deg, #1F5C94 0%, #153D6B 100%);
    color: white;
    box-shadow: 0 2px 8px rgba(31, 92, 148, 0.3);
}

.interest-crop-use:hover {
    box-shadow: 0 4px 12px rgba(31, 92, 148, 0.4);
    transform: translateY(-1px);
}

.interest-crop-use:active {
    transform: translateY(0) scale(0.98);
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* Mobile optimizations */
@media (max-width: 600px) {
    .interest-crop-content {
        max-width: 100vw;
        max-height: 100vh;
        border-radius: 0;
    }
    
    .interest-crop-header {
        padding: 16px 20px;
    }
    
    .interest-crop-title {
        font-size: 18px;
    }
    
    .interest-crop-subtitle {
        font-size: 12px;
    }
    
    .interest-crop-controls {
        padding: 12px 16px;
    }
    
    .interest-crop-btn {
        padding: 12px 20px;
        font-size: 14px;
    }
}

/* ==========================================================================
   SELECTION MODE
   ========================================================================== */

.selection-toolbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--header-height);
    background: var(--bg-header);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 8px;
    z-index: 200;
}

.selection-toolbar-left {
    display: flex;
    align-items: center;
    gap: 12px;
}

.selection-toolbar-btn {
    background: none;
    border: none;
    padding: 8px;
    cursor: pointer;
    display: flex;
    align-items: center;
    border-radius: 50%;
}

.selection-toolbar-btn svg {
    width: 24px;
    height: 24px;
    fill: var(--text-header);
}

.selection-toolbar-btn:active {
    background: rgba(255, 255, 255, 0.15);
}

.selection-count {
    font-size: var(--font-lg);
    font-weight: var(--font-weight-medium);
    color: var(--text-header);
}

.selection-toolbar-actions {
    display: flex;
    align-items: center;
    gap: 2px;
}

/* Selected chat list item highlight */
.chatlist-item.selected {
    background-color: rgba(31, 92, 148, 0.12);
}

[data-theme="dark"] .chatlist-item.selected {
    background-color: rgba(30, 58, 95, 0.35);
}


/* Mute icon in chatlist */
.chatlist-mute-icon {
    display: flex;
    align-items: center;
    color: var(--text-secondary);
    opacity: 0.55;
}

.chatlist-mute-icon svg {
    width: 16px;
    height: 16px;
    fill: currentColor;
}

/* Archive row at top of chat list */
.archive-row {
    height: 46px;
    display: flex;
    align-items: center;
    padding: 0 var(--space-lg);
    gap: 16px;
    cursor: pointer;
    border-bottom: 1px solid var(--divider);
}

.archive-row:active {
    background-color: var(--ripple);
}

.archive-row svg {
    width: 22px;
    height: 22px;
    fill: var(--text-secondary);
}

.archive-row-label {
    font-size: var(--font-sm);
    color: var(--text-secondary);
    font-weight: var(--font-weight-medium);
    flex: 1;
}

.archive-row-count {
    font-size: var(--font-xs);
    color: var(--badge-text);
    background: var(--badge-unread);
    border-radius: 10px;
    min-width: 20px;
    height: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 5px;
    font-weight: var(--font-weight-bold);
}

/* ==========================================================================
   AVATAR POPUP SYSTEM
   ========================================================================== */

/* Make avatars clickable for popup */
.avatar-clickable {
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.avatar-clickable:active {
    opacity: 0.7;
    transition: opacity 0.1s ease;
}

/* Transparent overlay to catch outside clicks */
.avatar-popup-overlay {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1999;
    background: transparent;
}

.avatar-popup-overlay.active {
    display: block;
}

/* Small popup near avatar */
.avatar-popup {
    position: fixed;
    z-index: 2000;
    background: var(--bg-primary);
    border-radius: 12px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
    padding: 8px;
    opacity: 0;
    transform: scale(0.3);
    transition: transform 350ms var(--photo-spring), opacity 150ms ease;
    will-change: transform, opacity;
    pointer-events: none;
}

.avatar-popup.visible {
    opacity: 1;
    transform: scale(1);
    pointer-events: auto;
}

.avatar-popup-img {
    max-width: min(60vw, 280px);
    max-height: 50vh;
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: 8px;
    display: block;
    cursor: pointer;
}

.avatar-popup-img.selfie-border {
    border: 3px solid var(--selfie-accent, #4A8CB8);
}

.avatar-popup-img.interest-border {
    border: 3px solid var(--interest-accent, #5A9E60);
}

.avatar-popup-name {
    text-align: center;
    padding: 8px 4px 4px;
    font-size: var(--font-md);
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 60vw;
}

/* Full screen photo viewer */
.fullscreen-photo {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 3000;
    background: rgba(0, 0, 0, 0.92);
    display: flex;
    flex-direction: column;
    opacity: 0;
    transform: scale(0.85);
    transition: transform 400ms var(--photo-spring), opacity 200ms ease;
    will-change: transform, opacity;
}

.fullscreen-photo.visible {
    opacity: 1;
    transform: scale(1);
}

.fullscreen-photo-header {
    display: flex;
    align-items: center;
    padding: 12px 16px;
    padding-top: max(12px, env(safe-area-inset-top, 12px));
}

.fullscreen-photo-back {
    background: none;
    border: none;
    color: white;
    padding: 8px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
}

.fullscreen-photo-back svg {
    width: 24px;
    height: 24px;
}

.fullscreen-photo-name {
    color: white;
    font-size: var(--font-lg);
    font-weight: var(--font-weight-medium);
    margin-left: 8px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.fullscreen-photo-body {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    overflow: hidden;
}

.fullscreen-photo-img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    border-radius: 4px;
}

/* Toast — small bottom-centred notification used for limit/disabled messages */
.toast {
    position: fixed;
    left: 50%;
    bottom: calc(env(safe-area-inset-bottom, 0) + 32px);
    transform: translate(-50%, 12px);
    background: rgba(20, 20, 20, 0.92);
    color: #fff;
    padding: 14px 22px;
    border-radius: 10px;
    font-size: 15px;
    line-height: 1.4;
    max-width: 88vw;
    text-align: center;
    z-index: 99999;
    box-shadow: 0 8px 24px rgba(0,0,0,0.35);
    opacity: 0;
    transition: opacity 220ms ease, transform 220ms ease;
    pointer-events: none;
}
.toast.visible {
    opacity: 1;
    transform: translate(-50%, 0);
}

/* The toast itself lets presses through to the page underneath, but a toast
   that offers somewhere to go has to be reachable, or the offer is a lie. */
.toast a { pointer-events: auto; }

/* ==========================================================================
   THE CREDITS PAGE
   Reached from the menu, slides in over everything like Settings and borrows
   that page's header. One thing to buy for now, laid out as a shop row so a
   paid price can take the place of the testing price without rebuilding it.
   ========================================================================== */

.credits-panel {
    position: fixed;
    inset: 0;
    background: var(--bg-primary);
    transform: translateX(100%);
    transition: transform 0.3s ease;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}

.credits-panel.active { transform: translateX(0); }

.credits-content {
    padding: 28px 20px 40px;
    display: flex;
    flex-direction: column;
    align-items: center;
}

/* The balance, said once and said large. It takes the same three colours as
   the pill, so the number in the corner and the number here agree. */
.credits-balance {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    margin-bottom: 6px;
}

.credits-balance-number {
    font-size: 64px;
    font-weight: var(--font-weight-bold);
    font-variant-numeric: tabular-nums;
    line-height: 1.05;
    color: var(--credit-green);
    transition: color 260ms ease;
}

.credits-balance-number.orange { color: var(--credit-orange); }
.credits-balance-number.red    { color: var(--credit-red); }

.credits-balance-label {
    font-size: var(--font-sm);
    color: var(--text-secondary);
}

.credits-explainer {
    margin: 18px 0 26px;
    max-width: 300px;
    text-align: center;
    font-size: var(--font-md);
    line-height: 1.45;
    color: var(--text-secondary);
}

.credits-shop {
    width: 100%;
    max-width: 420px;
}

.credits-product {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 16px 18px;
    border: 1px solid var(--divider);
    border-radius: 14px;
    background: var(--bg-primary);
}

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

.credits-product-name {
    font-size: var(--font-lg);
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
}

.credits-product-note {
    font-size: var(--font-sm);
    color: var(--text-secondary);
}

.credits-buy-btn {
    flex: none;
    min-width: 92px;
    height: 40px;
    padding: 0 20px;
    border: none;
    border-radius: 999px;
    background: var(--accent);
    color: #fff;
    font-family: var(--font-family);
    font-size: var(--font-md);
    font-weight: var(--font-weight-medium);
    cursor: pointer;
    transition: filter var(--transition-fast), opacity var(--transition-fast);
}

.credits-buy-btn:active { filter: brightness(0.9); }

/* The dark accent is a pale blue, and white on it reads at about 2.6:1.
   Dark ink on the same blue reads at 7.5:1, so the label switches rather
   than the button. */
[data-theme="dark"] .credits-buy-btn { color: #0B141A; }

/* Waiting on the server, or nothing left to add: the same flat look either
   way, because in both cases pressing it again does nothing. */
.credits-buy-btn:disabled {
    opacity: 0.45;
    cursor: default;
}

.credits-fineprint {
    margin: 20px 0 0;
    font-size: var(--font-sm);
    color: var(--text-secondary);
    text-align: center;
}

/* ==========================================================================
   ABOUT
   The logo at a size worth looking at, the name under it, and the two lines
   that say whose protocol this is. Borrows the credits page's frame — one
   slide-in shape for every page reached from the menu.
   ========================================================================== */

.about-panel {
    position: fixed;
    inset: 0;
    background: var(--bg-primary);
    transform: translateX(100%);
    transition: transform 0.3s ease;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}

.about-panel.active { transform: translateX(0); }

.about-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: 2px;
    padding: clamp(10px, 3vh, 24px) 24px calc(16px + env(safe-area-inset-bottom, 0px));
    text-align: center;
}

.about-logo {
    width: 132px;
    height: 132px;
    max-width: 42vw;
    max-height: 42vw;
    object-fit: contain;
    display: block;
}

.about-name {
    margin: 14px 0 0;
    font-size: 34px;
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.01em;
    color: var(--text-primary);
}

.about-protocol {
    margin: 18px 0 0;
    max-width: 300px;
    font-size: var(--font-md);
    line-height: 1.45;
    color: var(--text-secondary);
}

/* Small, as asked: it is a note about the protocol, not a claim being made. */
.about-patent {
    margin: 4px 0 0;
    font-size: var(--font-xs);
    color: var(--text-secondary);
    opacity: 0.85;
}

.about-site {
    margin-top: 14px;
    font-size: var(--font-md);
    font-weight: var(--font-weight-medium);
    color: var(--accent);
    text-decoration: none;
}

.about-site:active { opacity: 0.7; }

.about-qr {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* Down at the foot of the page rather than tucked under the link.
       auto rather than a margin in pixels: .about-content is a flex column
       filling the panel, so this takes whatever room is left over and the code
       ends up near the bottom on a tall screen without being pushed off a short
       one — where the auto margin simply collapses and it follows the text as
       it used to. The padding is the gap it keeps either way. */
    margin-top: auto;
    padding-top: 28px;
    color: var(--text-secondary);
}

.about-qr img {
    display: block;
    /* A quarter bigger, both terms: 144 to 180, 38vw to 47.5vw, so it grows by
       the same amount whichever of the two is doing the limiting. */
    width: min(180px, 47.5vw);
    aspect-ratio: 1;
    padding: 2px;
    border: 1px solid var(--divider);
    border-radius: 4px;
    background: #fff;
}

.about-qr span {
    display: block;
    width: 100%;
    margin-top: 6px;
    font-size: 13px;
    line-height: 1.25;
    text-align: center;
}


/* ==========================================================================
   THE HAND
   The other way of saying which spot a slide means, and the one in use — see
   SPOT_POINTER in LookSquares.js, which still builds the arrows instead if it
   is set back to them.

   A hand points at the mark on the spot a slide is about: from the left for
   listening, from the right for calling. It does not move. It used to sweep
   in from the edge of the screen and tap, which is a lot of animation for
   something that then stays in view the whole time a slide is read; all that
   moves now is the tap itself, the rings spreading from under the fingertip.

   It is the whole arm, and it is hung long enough that the end of the arm is
   off the side of the screen — an arm never seen to stop reads as one reaching
   in, where a cut end reads as a picture of an arm. That is why the size is a
   share of the page across rather than a number of pixels: the reach has to
   keep up with the width it is reaching over, or on a wide enough screen the
   end of it comes back into view.

   Only the drawing is mirrored for the right-hand side, never the box it sits
   in — the box carries the placing, and a mirrored box would put everything
   written on it the wrong way round.

   It is placed by its fingertip, and turned about it. The drawing stands
   upright with the tip hard against its top edge, --tip-across of the way
   over; turning it about that point swings the arm up towards the corner
   without moving the place it is pointing at, so the angle can be changed
   without re-measuring anything. That fraction, and the drawing's proportions,
   come from scripts/build-hand.js, which prints them when it cuts the hand
   out — change the crop there and these are what has to be brought across.
   ========================================================================== */

.look-hand {
    --hand-w: 25%;               /* of the page across; see above */
    --tip-across: 0.6915;        /* where the fingertip is, across the drawing */
    /* Upright would be 0. This points it down at the spot, and lays the arm
       out towards the corner shallowly enough to leave the picture above it
       alone on its way off the side. */
    --lean: 120deg;

    position: absolute;
    /* Into the top of the spot, wherever the spot has ended up. This used to
       be 60% of the page, which was only ever right because the two halves
       were equal — and they are not any more: the spots take the height their
       shape asks for and the explainer takes the rest. So the measurement
       comes from the spot itself, and the percentage is only what is used
       before anything has been measured. */
    top: var(--hand-top, 60%);
    width: var(--hand-w);
    aspect-ratio: 363 / 700;     /* the drawing's own, printed by build-hand.js */
    pointer-events: none;        /* it lies over a spot; presses belong to the spot */
    opacity: 0;
    transition: opacity 260ms ease;
    z-index: 6;
}

/* Both are hung the same way round — the mirroring is on the drawing, and it
   pivots about the fingertip, so the tip lands at the same fraction of the
   box either way. */
.look-hand.left  { left: calc(var(--hand-x-left, 25%) - var(--hand-w) * var(--tip-across)); }
.look-hand.right { left: calc(var(--hand-x-right, 75%) - var(--hand-w) * var(--tip-across)); }

.look-hand-img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
    filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.28));
    /* Turned about the fingertip, so the point it names does not move when
       the angle does. */
    transform-origin: calc(var(--tip-across) * 100%) 0;
    transform: rotate(var(--lean));
}

/* Mirrored on the drawing alone, never on the box: the box carries the
   placing, and a mirrored box would put all of it the other way round. The
   mirror is about the same fingertip, so the tip stays put. */
.look-hand.right .look-hand-img { transform: scaleX(-1) rotate(var(--lean)); }

/* ---- The tap ----
   Two rings from under the fingertip, the second trailing the first. They are
   centred on the same point the drawing is hung and turned by, so they spread
   from the tip itself rather than from somewhere near it. Under the hand,
   because that is where a ripple would be. */
.look-hand-ring {
    position: absolute;
    top: 0;
    left: calc(var(--tip-across) * 100%);
    /* Small enough that the widest it gets still sits inside the spot:
       72 at 1.55 is 112 across, against a spot 180 wide. */
    width: 72px;
    height: 72px;
    margin: -36px 0 0 -36px;
    border: 3px solid rgba(46, 183, 117, 0.9);
    border-radius: 50%;
    opacity: 0;
    transform: scale(0.2);
    z-index: -1;
}

/* Up only for the slide that raised it, and the hand only fades in and out:
   the tapping is the rings' alone. */
.look-hand.showing { opacity: 1; }
.look-hand.showing .look-hand-ring     { animation: lookHandRing 2.4s ease-out infinite; }
.look-hand.showing .look-hand-ring.two { animation-delay: 0.3s; }

/* One ripple out and gone, then a wait long enough that the next one reads as
   a fresh tap rather than a pulse that never stops. */
@keyframes lookHandRing {
    0%           { transform: scale(0.2); opacity: 0; }
    6%           { opacity: 0.55; }
    46%          { transform: scale(1.55); opacity: 0; }
    100%         { transform: scale(1.55); opacity: 0; }
}

/* Asked for less movement: the rings rest where they are. */
@media (prefers-reduced-motion: reduce) {
    .look-hand.showing .look-hand-ring { animation: none; opacity: 0.4; transform: scale(1); }
}

/* ==========================================================================
   The Call spot without a viewfinder

   A camera view held up at somebody is what tells them they are being
   photographed. Behind the masked button the spot shows a corner of what the
   lens sees and nothing else — whoever stands in the spot is who the picture
   will be of — which is what lets the phone stay pointed at the pavement.
   ========================================================================== */

/* The preview is the video itself, laid out at the frame's own size and
   moved by one transform the spot then clips. No canvas, no per-frame work:
   the compositor already has the picture and is better at showing it than
   anything that redraws it thirty times a second. */
.look-live-video {
    position: absolute;
    top: 0;
    left: 0;
    display: none;
    z-index: 3;
    pointer-events: none;
    background: #2A3138;
}
.look-square.live .look-live-video { display: block; }

/* The warped preview. Sits over the video, which stays in the page because
   it is the texture the shader reads — it is simply never the thing looked
   at once the GPU is drawing. */
.look-live-gl {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: none;
    z-index: 4;
    border-radius: inherit;
    pointer-events: none;
    background: #2A3138;
}
.look-square.live .look-live-gl { display: block; }
/* Until it has been given a size and a place it is not shown at all. A video
   with no frame yet draws itself as a small dark box, and a small dark box in
   the corner of a spot reads as half the spot having gone black. */
.look-live-video:not(.ready) { visibility: hidden; }

/* The still, while it is being framed. */
.look-live {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: none;
    z-index: 3;
    background: #2A3138;
    border-radius: inherit;
    touch-action: none;
}
.look-square.adjusting .look-live { display: block; }

/* The logo and the masked button step aside for the picture they asked for. */
.look-square.live .look-square-shutter,
.look-square.adjusting .look-square-shutter,
.look-square.live .look-square-shutter,
.look-square.adjusting .look-square-shutter { opacity: 0; pointer-events: none; }

/* The way out stays reachable the whole way through — while the lens is
   open, while the shot is being framed, and over the top of both. */
.look-square.live .look-square-tools,
.look-square.adjusting .look-square-tools { z-index: 8; }

/* ---- The sliding hint ----
   A moment of it when the lens opens: a finger up and down the spot is the
   zoom, which nobody would guess at and which is not worth a sentence. Light
   enough to read as a hint over a picture rather than as something covering
   it, and gone before it can be in the way. */
.look-slidehint {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%) scale(.94);
    width: 44%;
    max-width: 116px;
    height: auto;
    color: #fff;
    opacity: 0;
    pointer-events: none;
    z-index: 7;
    filter: drop-shadow(0 1px 5px rgba(0, 0, 0, .8));
    transition: opacity 520ms ease, transform 520ms ease;
}
.look-slidehint.showing {
    opacity: .78;
    transform: translate(-50%, -50%) scale(1);
    transition-duration: 200ms;
}
@media (prefers-reduced-motion: reduce) {
    .look-slidehint, .look-slidehint.showing { transition: opacity 200ms ease; transform: translate(-50%, -50%); }
}

/* ---- The zoom frame ----
   Only present while a finger is changing the zoom. Four open corner brackets
   ask for the person to fill most of the Look without boxing them in. Each
   horizontal leg occupies one third of the frame, leaving the middle third of
   the top and bottom open; aspect-ratio keeps its vertical leg equally long. */
.look-zoom-frame {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 70%;
    height: 85%;
    transform: translate(-50%, -50%);
    z-index: 7;
    pointer-events: none;
    opacity: 0;
    transition: opacity 80ms linear;
}
.look-zoom-corner {
    position: absolute;
    box-sizing: border-box;
    width: 33.333%;
    aspect-ratio: 1;
    border-color: rgba(255, 255, 255, .98);
    border-style: solid;
    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 1))
            drop-shadow(0 2px 3px rgba(0, 0, 0, .8));
}
.look-zoom-corner.top-left {
    top: 0;
    left: 0;
    border-width: 3px 0 0 3px;
}
.look-zoom-corner.top-right {
    top: 0;
    right: 0;
    border-width: 3px 3px 0 0;
}
.look-zoom-corner.bottom-left {
    bottom: 0;
    left: 0;
    border-width: 0 0 3px 3px;
}
.look-zoom-corner.bottom-right {
    right: 0;
    bottom: 0;
    border-width: 0 3px 3px 0;
}
.look-square.live.sliding .look-zoom-frame { opacity: 1; }

/* While the lens is open or a shot is being framed, the spot owns every
   touch on it. Without this the browser claims a vertical drag as a scroll
   and the zoom arrives in the fragments that are left over. */
.look-square.live,
.look-square.adjusting { touch-action: none; }

/* ---- The way out to the phone's own camera ----
   Opposite the cross and matching it exactly: two ways out of a preview, one
   back to an empty spot and one on to the camera with all the features. Only
   while the preview is up — before it the spot is the way in, and after it
   there is a picture worth keeping. */
.look-fullscreen {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 39px;
    height: 39px;
    padding: 0;
    border: 0;
    border-radius: 11px;
    display: none;
    align-items: center;
    justify-content: center;
    /* Nothing of its own behind the drawing. There used to be a box here with
       its own colour and its own rounded corners, and the drawing — which is
       already a rounded plate — sat inside it at half the size. Two squares,
       and the arrows squeezed into the smaller one. */
    background: none;
    cursor: pointer;
    z-index: 9;
    -webkit-tap-highlight-color: transparent;
}

/* The plate is the button, so it fills it. */
.look-fullscreen svg { width: 100%; height: 100%; display: block; }
/* The same grey as the cross, which is the point: they sit in opposite
   corners of the same picture. That grey is not a colour but a veil —
   rgba(20,26,32,.45) over whatever the lens is showing — so an opaque fill
   of any shade reads as a black tile beside it however carefully it is
   picked. #141A20 IS rgb(20,26,32); the 0.45 is what makes it grey. */
.look-fullscreen-plate { fill: #141A20; fill-opacity: 0.45; }
.look-fullscreen-arrows { fill: #C9D2DA; }

.look-fullscreen:active { opacity: 0.6; }
/* The spots no longer carry one — the switch in the top left chooses the
   camera before the preview opens, so there is nothing to escape to mid-aim.
   The styles above stay for app-camera.html, which loads this stylesheet and
   still draws the same button. */

/* ---- The zoom reading ----
   Pinned over the top edge of the framing rectangle, and only visible while
   the finger is moving. 1.0x is the opening view; 2.0x shows half as much of
   the scene across the same glass. */
.look-zoom {
    position: absolute;
    left: 50%;
    top: 7.5%;
    transform: translate(-50%, -50%);
    z-index: 8;
    pointer-events: none;
    font-size: 15px;
    font-weight: 600;
    letter-spacing: .02em;
    font-variant-numeric: tabular-nums;
    padding: 3px 7px;
    border-radius: 999px;
    color: #fff;
    background: rgba(10, 14, 18, .72);
    text-shadow: 0 1px 2px #000;
    opacity: 0;
    transition: opacity 80ms linear;
}
.look-square.sliding .look-zoom.showing {
    opacity: 1;
}


/* The camera's own numbers, over the preview.
   Only a developer account ever has `.look-dev`, so this never appears for
   anyone else — and it sits on the phone, where the numbers are true, rather
   than in a console on a desktop where they are not. */
.look-live-numbers { display: none; }
.look-dev .look-live-numbers {
  display: block;
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 6;
  padding: 2px 4px;
  font: 500 9px/1.25 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: #d8e2ea;
  background: rgba(0, 0, 0, .55);
  text-align: center;
  pointer-events: none;
  word-break: break-word;
}

/* The shutter, on the spot.
   Only while the lens is live, and taken away while the spot is being slid —
   a finger on the glass is zooming, and the button would be under it. */
.look-shutter {
    position: absolute;
    left: 50%;
    bottom: 10px;
    transform: translateX(-50%) scale(.82);
    width: 52px;
    height: 52px;
    padding: 0;
    z-index: 7;
    border: 0;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 0 0 3px rgba(255, 255, 255, .45),
                0 2px 10px rgba(0, 0, 0, .45);
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    /* No transition on the way in: the button has to be pressable the instant
       it appears, and a button fading in is a button that gets pressed while
       it is still arriving. */
    transition: opacity .12s linear, transform .12s ease-out;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
}
.look-square.live .look-shutter {
    opacity: 1;
    transform: translateX(-50%) scale(1);
    pointer-events: auto;
}
.look-square.live.sliding .look-shutter {
    opacity: 0;
    transform: translateX(-50%) scale(.82);
    pointer-events: none;
}

/* During the zoom gesture the picture, its rectangle and its factor are the
   whole interface. Everything actionable returns as soon as the finger lifts. */
.look-square.live.sliding .look-spot-flip,
.look-square.live.sliding .look-square-tools {
    display: none;
}
.look-shutter:active { transform: translateX(-50%) scale(.9); }

/* ==========================================================================
   The wide spots — one band across the page instead of two cards on it.

   Hangs entirely off `.look-wide`, which LookSquares puts on the container
   when WIDE_SPOTS is true. Set that constant to false and none of this
   applies: the card layout above is what the stylesheet says on its own, so
   the two can be swapped without anything being unpicked.
   ========================================================================== */

/* Out to the side edges. The vertical padding stays — it separates the band
   from the explainer above it, which the side padding was never doing. */
/* Same element, not a descendant: the container LookSquares is given is the
   .look-squares div itself, so this has to be written as one compound
   selector. As `.look-wide .look-squares` it matched nothing and the side
   padding quietly stayed. */
.look-squares.look-wide {
    padding-left: 0;
    padding-right: 0;
}

/* The thin line between them is the page showing through. A drawn border
   would have to belong to one spot or the other, and would then be inside
   the box whose proportion has to stay exactly 2:3. */
.look-wide .looks-bottom {
    gap: 1px;
}

/* No frame, no rounding, no shadow — a card's chrome is what stops it
   reading as part of the page, and here it is meant to be part of it. The
   border going means the script's ratio maths gets a border width of zero,
   which it already handles. */
.look-wide .look-square {
    /* A real border, not a line drawn over the top.

       The picture then sits INSIDE it rather than under it, which is the
       whole point — and it costs nothing, because the script that sizes the
       spots already takes the border off both sides before applying the
       ratio, so the inside stays exactly 2:3 and the crop still matches the
       upload. Insetting the picture instead would have shrunk the preview
       canvas away from the box its transform was built for. */
    border: 1px solid var(--spot-edge);
    /* Corners kept. A thin border with square corners against a rounded one
       next to it read as two different components; the rounding is what makes
       a spot a spot, and it costs nothing at the sides of the page. */
    box-shadow: none;
    margin-inline: 0;
}

/* The drawn-on line is switched OFF in the wide layout, not merely left
   alone.

   Off is only its default. .demoing turns it back on whenever a slide puts
   a drawing in the spot, which showed as two edges round the Call spot —
   the real border, and this one two pixels inside it with its own shadow.
   A spot holding a photograph and its neighbour holding a drawing have to
   look like the same component.

   Written through .look-squares.look-wide so it is one class more specific
   than .look-square.demoing::after, rather than merely later in the file.
   Those two are otherwise evenly matched and whichever came last would win,
   which is the trap that rule already carries a warning about. */
.look-squares.look-wide .look-square::after {
    display: none;
}

/* The buttons keep their own margins so they do not run into each other or
   off the sides, now that the spots above them no longer leave a gap. */
.look-wide .look-square-action {
    margin-inline: 8px;
}

/* The live canvas and the still both take the square's rounding by
   inheriting it, so they need nothing said here. The held photograph does
   not — it carries its own, worked out for the 3px border it used to sit
   inside. One pixel of border means one pixel less radius. */
.look-wide .look-square-img {
    border-radius: 15px;
}

/* ==========================================================================
   Room to actually hit the two corner controls.

   The cross and the fullscreen button are 39px squares sitting 8px in from
   the top and from their own side of the spot. Thumbs are wider than that,
   and both of them sit over a live preview where a miss is not nothing — it
   starts a zoom.

   So each grows an invisible pad of exactly that 8px on every side: out to
   the corner of the spot on the two sides it is already near, and the same
   again on the other two. 39 becomes 55, which is comfortably past the 44
   both platforms ask for, and not one pixel of the drawing moves.

   A pseudo-element rather than padding or a transform, because both buttons
   are sized boxes with their artwork stretched to fill them — padding would
   shrink the drawing inside the same square, and a scale would blur it. */
.look-square-tools button {
    /* The pad is positioned against the button, so the button has to be what
       it is positioned against. The cross sits in a flex row and had no
       position of its own, so its pad would have hung off the row instead and
       covered the wrong place.

       Only the cross. The fullscreen button is ALREADY absolute, at top 8
       right 8, and is its own containing block as it stands — giving it
       position: relative here would have taken it out of the corner and
       dropped it into the flow. */
    position: relative;
}

.look-square-tools button::before,
.look-fullscreen::before {
    content: "";
    position: absolute;
    inset: -8px;
    /* Nothing to see. It is here to be touched, and it must not sit over the
       artwork it belongs to — an invisible box painted over an SVG still
       takes the tap, which is fine, but it would also take it from the
       button's own :active feedback if it were a layer above. */
    background: transparent;
    border-radius: 19px;          /* 11 on the button, plus the 8 it grew by */
}

/* ---- Waiting on the fix ----
   The button is the indicator, so it says the one thing that is holding the
   Look up. Red with a pin while the phone does not know where it is well
   enough to place a Look, ordinary the moment it does — at which point the
   countdown starts by itself and the colour goes with it.

   Nothing here has to be pressed. Pressing it only explains itself. */
.look-square-action.waiting-fix {
    background: var(--fix-waiting, #B3402F);
}

/* The pin, drawn rather than fetched: one glyph, both themes, no request. */
.look-square-action.waiting-fix::after {
    content: "";
    width: 15px;
    height: 15px;
    margin-left: 7px;
    flex: none;
    background: currentColor;
    /* A map pin: circle over a tapering point. */
    -webkit-mask: no-repeat center / contain
        url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2a7 7 0 0 0-7 7c0 5 7 13 7 13s7-8 7-13a7 7 0 0 0-7-7zm0 9.5A2.5 2.5 0 1 1 12 6.5a2.5 2.5 0 0 1 0 5z"/></svg>');
    mask: no-repeat center / contain
        url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2a7 7 0 0 0-7 7c0 5 7 13 7 13s7-8 7-13a7 7 0 0 0-7-7zm0 9.5A2.5 2.5 0 1 1 12 6.5a2.5 2.5 0 0 1 0 5z"/></svg>');
    /* Slow, because it is a state and not an alarm. */
    animation: lookFixPulse 1.6s ease-in-out infinite;
}

@keyframes lookFixPulse {
    0%, 100% { opacity: 0.55; }
    50%      { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .look-square-action.waiting-fix::after { animation: none; opacity: 0.9; }
}

/* ---- The mark in the bracket ----
   The private camera aims with a thin cross in the middle of the spot; the
   fullscreen flow crops with a bracket and had nothing in the middle of it.
   Same mark, so the two read as the same act.

   13% of the bracket's width, which is what 26px is of a 204px spot — sized
   as a share rather than in pixels because the bracket is sized in script and
   changes with the screen. */
.look-adjust-cross {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 13%;
    aspect-ratio: 1;
    transform: translate(-50%, -50%);
    opacity: 0.5;
    pointer-events: none;
}

.look-adjust-cross::before,
.look-adjust-cross::after {
    content: "";
    position: absolute;
    background: rgba(255, 255, 255, 0.9);
    box-shadow: 0 0 2px rgba(0, 0, 0, 0.55);
}
.look-adjust-cross::before { left: 50%; top: 0; width: 1.5px; height: 100%; transform: translateX(-50%); }
.look-adjust-cross::after  { top: 50%; left: 0; height: 1.5px; width: 100%; transform: translateY(-50%); }

/* ---- Sending a badge ----
   The attach button sits inside the field as something that can be put in a
   message, not as a separate mode. */
.input-badge {
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    flex-shrink: 0;
    color: var(--text-secondary);
    padding: 0;
}

.input-badge svg {
    width: 21px;
    height: 21px;
    fill: var(--text-secondary);
}

.input-badge:active svg { opacity: 0.6; }
.input-badge.open svg { fill: var(--accent); }

/* The picker: one row of the badges that have a copy to spare, sitting on
   the input bar. Nothing about what anybody else holds appears here — what
   somebody wants is something they say, in words, in the conversation. */
.badge-picker {
    flex-shrink: 0;
    width: 100%;
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    box-sizing: border-box;
    background: var(--bg-primary);
    border-top: 1px solid var(--input-border);
    padding: 10px var(--space-md) 4px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

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

.badge-picker-head {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-secondary);
}

.badge-picker-row {
    display: flex;
    width: 100%;
    min-width: 0;
    max-width: 100%;
    gap: 8px;
    overflow-x: auto;
    padding-bottom: 6px;
    /* A long row of spares scrolls sideways rather than wrapping the bar
       into two rows and shoving the conversation up the screen. */
    -webkit-overflow-scrolling: touch;
}

/* Nothing to send. Said rather than shown empty, because an empty row looks
   like something failed to load. */
.badge-picker-none {
    font-size: 13px;
    color: var(--text-secondary);
    padding-bottom: 8px;
}

.badge-pick {
    position: relative;
    flex: 0 0 auto;
    /* Half again on 46. Big enough to know a face at a glance, which is the
       whole point of them carrying faces. */
    width: 69px;
    height: 69px;
    padding: 0;
    overflow: hidden;
    border: 1px solid color-mix(in srgb, var(--text-secondary) 30%, transparent);
    border-radius: 6px;
    background: var(--look-spot-bg);
    cursor: pointer;
}

.badge-pick-face {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.badge-pick:active { opacity: 0.75; }
.badge-pick[disabled] { opacity: 0.5; cursor: default; }

/* How many are spare, which is one fewer than held. */
/* The spare count, same corner and same weight as the grid's, so the row
   and the album read as one thing. */
.badge-pick i {
    position: absolute;
    top: 0;
    right: 0;
    min-width: 20px;
    padding: 1px 5px;
    border-radius: 0 0 0 6px;
    background: var(--accent);
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    font-style: normal;
    line-height: 18px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* ---- A badge in the conversation ----
   Drawn as the badge itself rather than as text, so it reads as a thing that
   moved rather than a message about one. */
.message-badge {
    display: flex;
    align-items: center;
    gap: 10px;
}

.message-badge .chip {
    width: 52px;
    height: 52px;
    flex-shrink: 0;
    border-radius: 6px;
    object-fit: cover;
    display: block;
    border: 1px solid rgba(255, 255, 255, 0.35);
}

.message-badge .said {
    font-size: 14px;
    line-height: 1.3;
}
