/* Vapor 2.0 — the look. Built to match samples/. Mobile-first, pure-black theme.
   Kept sectioned so it stays readable as screens are added (login first). */

:root{
  --bg:#000;
  --card:#111;
  --field:#000;
  --line:#2b2b2b;
  --line-soft:#1e1e1e;
  --text:#fff;
  --muted:#8b8b8b;
  --muted-dim:#5a5a5a;
  --radius:22px;
  --radius-sm:14px;
  --font:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
}

/* Softer pink/white theme for female + trans woman — applied by setting
   data-theme="feminine" on <html> (see core/ui.js applyGenderTheme). Nearly
   every screen inherits this from the variables alone, the same way it
   inherits the default black theme — the two small overrides below exist
   only because those two spots hard-baked an assumption ("the button is
   white") that doesn't hold once --text/--bg swap to a dark-on-light pair. */
:root[data-theme="feminine"]{
  --bg:#fdf0f5;
  --card:#fff;
  --field:#fff8fa;
  --line:#f5c2d9;
  --line-soft:#fbe0ec;
  --text:#3d1526;
  --muted:#b8657f;
  --muted-dim:#d9a3b8;
}
/* The vape-record button's icon is a plain white SVG, inverted to black
   specifically because the button used to always be white (white-on-white
   needs the flip). Now that --text is dark rose, the un-inverted white icon
   already has plenty of contrast — inverting it would turn it black-on-dark. */
:root[data-theme="feminine"] .vapeBtn .vapeMask{filter:none}
/* Same reasoning as #main.on::before's dark pattern: swap to the light-stroke
   wallpaper art + a pink base so the pattern reads on a light background
   instead of vanishing (or worse, showing dark strokes meant for a dark base). */
:root[data-theme="feminine"] #main.on::before{background:#fdf0f5 url('/wallpaper.svg') repeat;background-size:260px 180px}
/* .loginPanel hard-codes a near-black shade (deliberately distinct from --bg,
   for a subtle two-tone panel-within-panel look) instead of using a variable
   — a feminine-specific override keeps that same intent without touching the
   default theme's exact tone at all. */
:root[data-theme="feminine"] .loginPanel{background:#fdf0f5;border-color:#f5c2d9;box-shadow:0 24px 70px rgba(216,120,160,.25)}
/* Same story as .loginPanel — the no-photo carousel card's radial gradient is
   two hardcoded near-black shades, not a variable. */
:root[data-theme="feminine"] .profileCard{background:radial-gradient(120% 80% at 50% 30%,#fff 0%,#fce8f0 70%)}
:root[data-theme="feminine"] .roomCard{background:radial-gradient(120% 80% at 50% 25%,#fff 0%,#f7dcea 72%)}
/* .msg.them (received bubbles) hardcodes a near-black fill with no explicit
   text color of its own — it just inherits body's var(--text). That's fine
   in the default theme (near-black bg, white inherited text), but it means
   once --text flips to dark rose, the UNCHANGED hardcoded #1b1b1b background
   would leave dark text on a near-black bubble — the single most-seen
   element in the whole app, and by far the worst contrast bug of the bunch. */
:root[data-theme="feminine"] .msg.them,
:root[data-theme="feminine"] .msg.uploading{background:var(--card)}
:root[data-theme="feminine"] .eventsCard{background:radial-gradient(120% 80% at 50% 25%,#fff 0%,#fbe4d8 72%)}
:root[data-theme="feminine"] .eventsCard .meta{color:var(--muted)}
:root[data-theme="feminine"] .introCard{background:#fdf0f5;border-color:#f5c2d9;box-shadow:0 24px 70px rgba(216,120,160,.25)}
:root[data-theme="feminine"] .inboxRowMain{background:var(--bg)}
:root[data-theme="feminine"] .skelCard{background:linear-gradient(100deg,#fbe0ec 30%,#fff 50%,#fbe0ec 70%);background-size:220% 100%}

*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
/* Black base on html; body stays transparent so the wallpaper layer shows. */
html{background:var(--bg)}
html,body{margin:0;padding:0;color:var(--text);font-family:var(--font)}
/* Lock the app to exactly the device viewport — no whole-page scroll "leak".
   Each screen fits 100dvh; only inner regions (message list, carousel) scroll. */
html,body{height:100dvh;overflow:hidden;overscroll-behavior:none}

/* Per-screen sketch-art wallpaper (fixed, full-viewport, behind the content).
   Login is the light "sketch-pad" the dark app panel floats on; main is a faint
   dark version of the same artwork. */
#login.on::before{
  content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  background:#e9e7ec url('/wallpaper.svg') repeat;background-size:260px 180px;
}
#main.on::before{
  content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  background:#000 url('/wallpaper-dark.svg') repeat;background-size:260px 180px;
}
button{font-family:inherit;cursor:pointer}
input,select,button{font-family:inherit}

/* Screens: only one visible at a time. Only LOGIN is constrained to a centered
   panel; once inside, main/chat use the full screen width. */
.screen{display:none;min-height:100vh;min-height:100dvh}
.screen.on{display:block}

/* ---------- Login ---------- */
/* Display gated on .on so the id selector can't override .screen visibility.
   A flex container with margin:auto on the panel centers it when it fits and
   scrolls WITHOUT clipping the top when the panel is taller than the screen
   (the mobile "leaking / swipe to see the top" bug). */
/* Center the panel and keep an EVEN wallpaper border on all sides. The panel never
   exceeds the padded viewport (scrolls inside itself if a small screen is too short),
   so it can't bleed off the bottom. */
#login.on{
  display:flex;align-items:center;justify-content:center;height:100dvh;overflow:hidden;
  padding:14px;
  padding-top:calc(14px + env(safe-area-inset-top));
  padding-bottom:calc(14px + env(safe-area-inset-bottom));
}

/* The dark app panel that floats on the light wallpaper (see samples). */
.loginPanel{
  width:100%;max-width:440px;margin:0;max-height:100%;overflow-y:auto;
  background:#0a0a0a;border:1px solid #1c1c1c;border-radius:26px;
  padding:18px;display:flex;flex-direction:column;gap:12px;
  box-shadow:0 24px 70px rgba(0,0,0,.5);
}
.loginPanel::-webkit-scrollbar{display:none}

.loginCard{
  background:var(--card);
  border:1px solid var(--line);
  border-radius:var(--radius);
  padding:16px;
  display:flex;flex-direction:column;gap:13px;
}

.brand{display:flex;align-items:center;justify-content:center;gap:12px;padding:2px 0 6px}
.brand .mask{width:52px;height:30px;display:inline-flex}
.brand .mask svg{width:100%;height:100%}
.brand .word{font-size:38px;font-weight:800;letter-spacing:-.02em;line-height:1}

.fieldRow{display:grid;grid-template-columns:1fr 96px;gap:12px}
.field label,.fieldLabel{display:block;color:var(--muted);font-size:12px;font-weight:700;letter-spacing:.14em;margin:2px 0 8px}

.inp{
  width:100%;background:var(--field);border:1px solid var(--line);
  border-radius:var(--radius-sm);color:var(--text);font-size:17px;font-weight:600;
  padding:16px;outline:none;
}
.inp::placeholder{color:var(--muted-dim);font-weight:600}
.inp:focus{border-color:var(--text)}
#age{text-align:center;font-weight:800}

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

.selectWrap{position:relative}
.selectWrap select{
  -webkit-appearance:none;appearance:none;
  width:100%;background:var(--field);border:1px solid var(--line);
  border-radius:var(--radius-sm);color:var(--text);font-size:18px;font-weight:800;
  padding:16px 40px 16px 16px;outline:none;
}
.selectWrap::after{
  content:"";position:absolute;right:16px;top:50%;width:0;height:0;
  border-left:5px solid transparent;border-right:5px solid transparent;
  border-top:6px solid var(--muted);transform:translateY(-40%);pointer-events:none;
}

.seg{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.segBtn{
  display:flex;align-items:center;justify-content:center;gap:8px;
  background:var(--field);border:1px solid var(--line);color:var(--text);
  border-radius:var(--radius-sm);padding:16px;font-size:16px;font-weight:700;
}
.segBtn svg{width:20px;height:20px;fill:currentColor}
.segBtn.on{background:var(--text);color:var(--bg);border-color:var(--text)}

/* Privacy bullets + start button */
.loginFoot{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:0 4px}
.privacy{display:flex;flex-direction:column;gap:12px;padding-top:2px}
.privacy .row{display:flex;align-items:center;gap:13px;color:var(--text);font-size:14px;font-weight:700;letter-spacing:.03em}
.privacy .row svg{width:20px;height:20px;stroke:var(--text);fill:none;flex:0 0 auto}

.startBtn{
  flex:0 0 auto;width:128px;height:128px;background:var(--bg);color:var(--text);
  border:1.5px solid var(--line);border-radius:24px;
  display:flex;flex-direction:column;align-items:center;justify-content:center;line-height:1.15;
}
.startBtn .sm{font-size:15px;font-weight:700}
.startBtn .lg{font-size:30px;font-weight:800;letter-spacing:-.01em;margin:1px 0}
.startBtn:active{transform:scale(.98)}

.legal{color:var(--muted-dim);font-size:11.5px;line-height:1.45;padding:2px 4px 4px}

/* Live username availability hint */
.nameHint{font-size:12px;font-weight:700;min-height:14px;padding-left:4px;margin-top:-8px}
.nameHint.ok{color:#33d17a}
.nameHint.bad{color:#ff5c7a}

/* One-time explainer popup */
.introOverlay{position:fixed;inset:0;z-index:90;background:rgba(0,0,0,.75);display:none;align-items:center;justify-content:center;padding:20px}
.introOverlay.on{display:flex}
.introCard{background:#0a0a0a;border:1px solid #1c1c1c;border-radius:24px;max-width:400px;width:100%;padding:26px;display:flex;flex-direction:column;gap:14px;box-shadow:0 24px 70px rgba(0,0,0,.6)}
.introCard .brand{justify-content:flex-start;gap:8px;padding:0}
.introCard .brand .mask{width:36px;height:22px}
.introCard .brand .word{font-size:26px}
.introCard h3{margin:2px 0 0;font-size:22px;font-weight:800}
.introCard p{margin:0;color:var(--muted);font-size:15px;line-height:1.5}
.introCard b{color:var(--text);font-weight:800}
.introClose{margin-top:6px;background:var(--text);color:var(--bg);border:0;border-radius:14px;padding:15px;font-weight:800;font-size:16px;cursor:pointer}

/* ---------- Main (matched world) ---------- */
/* Fills exactly one screen; profiles + inbox share the space, each scrolls.
   overflow-y:auto (not hidden) is the fallback for the opposite of the
   dead-gap problem below: on a viewport SHORTER than header + carousel row +
   inbox combined (common on a wide-but-short desktop browser window), hidden
   silently clipped the inbox off entirely with no way to reach it at all —
   the actual "can't scroll the inbox" bug this was tracked down from. When
   everything already fits (the common case), this changes nothing visually. */
#main{display:none;flex-direction:column;height:100vh;height:100dvh;overflow-y:auto;overflow-x:hidden}
#main.on{display:flex}
/* flex:0 0 auto (not 1 1 auto) matters: the carousel is a single horizontal
   row, fixed-height regardless of window size — letting this section flex-grow
   to fill the viewport just left a dead, ever-growing gap below the tiles on
   any screen taller than one tile row (worst on desktop: 447px of empty black
   space at 1920×1080). Content-sized instead, so the inbox sits right below
   the actual tiles; any true leftover space lands harmlessly at the page's
   bottom edge instead of splitting the layout apart. */
.profilesSection{flex:0 0 auto;min-height:0;display:flex;flex-direction:column}

/* Tighter top padding — with the duplicate username + online count gone, and the
   Vaporize button now inline-height with the logo, everything sits higher. */
.appHead{padding:calc(8px + env(safe-area-inset-top)) 16px 10px;border-bottom:1px solid var(--line-soft)}
.appHead .top{display:flex;align-items:center;justify-content:space-between;gap:12px}
.appHead .brandRow{display:flex;align-items:center;gap:10px;min-width:0}
.appHead .mask{width:40px;height:24px;display:inline-flex}
.appHead .word{font-size:28px;font-weight:800;letter-spacing:-.02em}

/* Compact rectangle, top-right — icon+label on one line, a small subtitle below.
   Still much shorter than the original tall stacked version. */
.vaporizeBtn{
  flex:0 0 auto;background:var(--bg);border:1.5px solid var(--text);border-radius:10px;color:var(--text);
  padding:7px 14px;display:flex;flex-direction:column;align-items:center;gap:2px;
}
.vaporizeBtn .vTop{display:flex;align-items:center;gap:6px}
/* Local Events button — mirrors Vaporize's shape in a calm teal, not alarm-white. */
.eventsBtn{
  flex:0 0 auto;background:var(--bg);border:1.5px solid #33d17a;border-radius:18px;color:#eafff3;
  padding:12px 14px;display:flex;flex-direction:column;align-items:center;gap:2px;min-width:96px;cursor:pointer;
}
.eventsBtn .ic{width:22px;height:22px;fill:none;stroke:#33d17a;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.eventsBtn .lbl{font-size:13px;font-weight:800;letter-spacing:.02em}
.eventsBtn .sub{font-size:9px;font-weight:700;color:var(--muted);letter-spacing:.06em}

/* Events diary overlay */
.eventsOverlay{position:fixed;inset:0;z-index:95;background:rgba(0,0,0,.78);display:none;align-items:flex-end;justify-content:center}
.eventsOverlay.on{display:flex}
.eventsCard{width:100%;max-width:560px;max-height:88vh;background:#0c0c0c;border:1px solid var(--line);border-radius:20px 20px 0 0;display:flex;flex-direction:column;overflow:hidden}
.eventsHead{display:flex;align-items:flex-start;justify-content:space-between;padding:18px 18px 10px}
.eventsHead h3{font-size:22px;font-weight:800}
.eventsSub{color:var(--muted);font-size:13px;font-weight:600;margin-top:2px}
/* The post-event sheet opens ON TOP of the events board overlay (z-index 95). */
#postEventSheet{z-index:100}
.eventsHeadBtns{display:flex;align-items:center;gap:10px}
.eventsPost{background:#33d17a;color:#04220f;border:0;border-radius:999px;padding:8px 14px;font-weight:800;font-size:13px;cursor:pointer;white-space:nowrap}
.eventsClose{background:none;border:0;color:var(--muted);font-size:28px;line-height:1;cursor:pointer;padding:0 4px}
.eventsList{overflow-y:auto;padding:4px 14px calc(18px + env(safe-area-inset-bottom))}
.eventsEmpty{color:var(--muted-dim);text-align:center;padding:40px 16px;font-size:14px;line-height:1.5}
.evRow{display:flex;gap:12px;align-items:flex-start;padding:13px 4px;border-top:1px solid var(--line-soft)}
.evWhen{flex:0 0 auto;width:58px;display:flex;flex-direction:column;gap:2px}
.evWhen .evDate{font-size:11px;font-weight:700;color:#33d17a;letter-spacing:.02em}
.evWhen .evTime{font-size:16px;font-weight:800;color:var(--text);line-height:1}
.evMain{flex:1;min-width:0}
.evTitle{font-weight:800;font-size:15px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.evTag{font-size:9px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;padding:2px 7px;border-radius:999px;border:1px solid var(--line);color:var(--muted)}
.evTag.lgbtq{color:#ff7ab6;border-color:#7a2b4d}
.evTag.all{color:#6fc3ff;border-color:#2b5a7a}
.evMeta{color:var(--muted);font-size:12.5px;margin-top:3px;overflow-wrap:anywhere}
.evBlurb{color:var(--muted-dim);font-size:12.5px;margin-top:4px;line-height:1.35}
.evAvail{margin-top:6px;font-size:12.5px;color:var(--muted);font-weight:600}
.evAvail .evCost{color:#33d17a;font-weight:800}
.evBy{color:var(--muted-dim);font-weight:600}
.evBook{flex:0 0 auto;align-self:center;background:#33d17a;color:#04220f;border:0;border-radius:999px;padding:8px 16px;font-weight:800;font-size:13px;cursor:pointer;white-space:nowrap;text-decoration:none;display:inline-flex;align-items:center}
.evActions{flex:0 0 auto;align-self:center;display:flex;flex-direction:column;gap:8px;align-items:stretch}
.evActions .evBook{justify-content:center}
.evBook.rsvp.on{background:transparent;color:#33d17a;border:1px solid #33d17a}
.evBook.rsvp:disabled{background:var(--line);color:var(--muted);cursor:default}
.evGoingLine{margin-top:6px;font-size:12.5px;font-weight:700;color:#33d17a}
.evGoing{color:var(--muted-dim);font-size:12px;margin-top:2px;overflow-wrap:anywhere}
.inpRow{display:flex;gap:8px}
.inpRow .inp{flex:1;min-width:0}
#pevGeo{flex:0 0 auto;width:44px;border-radius:12px;border:1px solid var(--line);background:transparent;font-size:18px;cursor:pointer}
#pevGeo:disabled{opacity:.5;cursor:default}
.vaporizeBtn .x{font-size:16px;font-weight:800;line-height:1}
.vaporizeBtn .lbl{font-size:13px;font-weight:800;letter-spacing:.02em;white-space:nowrap}
.vaporizeBtn .sub{font-size:9px;font-weight:700;color:var(--muted);letter-spacing:.02em;white-space:nowrap}

/* Compact controls row (replaces the 'Profiles' header) — frees vertical space */
.controlsBar{display:flex;gap:10px;padding:12px 14px 10px;overflow-x:auto}
.controlsBar[hidden]{display:none}
.controlsBar::-webkit-scrollbar{display:none}
.controlsBar .pillBtn{flex:0 0 auto}
.pillBtn{background:var(--bg);border:1px solid var(--line);color:var(--text);border-radius:16px;padding:11px 16px;font-weight:700;font-size:15px;display:inline-flex;align-items:center;gap:8px}
.pillBtn.on{border-color:var(--text)}
/* Filter is filtering — green to signal an active filter. */
.pillBtn.filtering{border-color:#33d17a;color:#33d17a;background:rgba(51,209,122,.08)}
.pillBtn svg{width:15px;height:15px;fill:currentColor}

/* Small, fixed single-line row: your profile (mini replica), Update Profile,
   Filter, New Group, Local Events. Deliberately smaller than the browsing
   carousel below — everything here just scales the same profile-card look down,
   except the action buttons, which are deliberately plain buttons, not tiles. */
.topActionsRow{flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:0 8px 10px;overflow-x:auto;-webkit-overflow-scrolling:touch}
.topActionsRow::-webkit-scrollbar{display:none}
/* Square, not the wider 4:3 this used to be — 4:3 forced every photo (almost
   always shot portrait) into a short, wide crop that read as a stretched/
   distorted thumbnail. Square is the standard avatar shape and crops far less
   aggressively out of a portrait source. */
.topActionsRow .profileCard{flex:0 0 161px;aspect-ratio:1/1}
.topActionsRow .profileCard .avatar{font-size:25px}
.topActionsRow .profileCard .tag{font-size:8.5px}
/* Shrink the photo a touch (78%->70%) to give the info strip more of the
   tile's own height, so the name sits noticeably higher up rather than
   pinned to the very bottom edge. cardInfo's own height grows to match
   (22%->30%) — leaving it at the old height while only shrinking the photo
   just left a dead gap between the two AND kept the name cramped in the
   same tight strip it was already getting cut off in. */
.topActionsRow .profileCard .cardPhoto{height:70%}
/* justify-content:flex-start (the base .cardInfo rule centers vertically) so
   the name sits a little higher in its strip instead of dead-center — square
   made the strip taller in absolute px than the old 4:3 shape had, and
   centered here just left it looking adrift in the extra space. Extra
   bottom padding (7px->9px) keeps real device font rendering (descenders,
   line-height rounding) clear of the tile's bottom edge instead of flush
   against it. */
.topActionsRow .profileCard .cardInfo{height:30%;padding:6px 9px 9px;justify-content:flex-start}
.topActionsRow .profileCard .cardInfo .nm{font-size:11px}
.topActionsRow .profileCard .cardInfo .mindPreview{font-size:11px;line-height:1.2}

/* A small pill — thin-outline pencil + soft "Edit profile" label — sits
   top-right (the self card moves its .tag to the bottom-right, see
   .selfCard .tag below, leaving this corner free). Third pass at this
   overall (first a diagonal corner ribbon, then a bold full-width top bar,
   then icon-only); the label is back, but muted/lighter-weight rather than
   the old bold white banner text, so it reads as a caption, not a shout. */
.updateBadge{position:absolute;top:6px;right:6px;z-index:2;
  display:flex;align-items:center;gap:5px;padding:5px 10px 5px 7px;border-radius:999px;
  background:rgba(0,0,0,.78);border:1px solid rgba(255,255,255,.4);color:#fff}
.updateBadge svg{flex:0 0 auto}
.updateBadge span{font-size:10.5px;font-weight:500;color:rgba(255,255,255,.72);white-space:nowrap}

/* Filter / New Group — proper black-and-white buttons (solid fill, centered
   icon+label), not profile-card-styled tiles. Filter stays black until a filter
   is actually applied (then green, matching the old pill button's behaviour);
   New Group is white — keeps the two visually distinct within the same CI.
   "New Group" wraps onto two lines so each word can use a bigger font. */
.actionBtn{flex:0 0 80px;aspect-ratio:1/1;border-radius:14px;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:6px;font-weight:800;font-size:13px;line-height:1.2;text-align:center}
.actionBtn .calIco{width:22px;height:22px}
.filterBtnTile{background:#000;border:1.5px solid var(--text);color:var(--text)}
.filterBtnTile .calIco{stroke:var(--text)}
.filterBtnTile.filtering{border-color:#33d17a;color:#33d17a}
.filterBtnTile.filtering .calIco{stroke:#33d17a}
/* New Group now matches Filter — black by default, same CI, distinguished by
   icon (solid filled plus) rather than by colour. */
.newGroupBtnTile{background:#000;border:1.5px solid var(--text);color:var(--text)}

/* Large browsing carousel: back to a SINGLE scrollable line (as it always was),
   not a wrapping grid — swipe sideways to cycle, ~2 tiles visible at once on
   phone width so the photo AND the "what's on my mind" text both read clearly. */
/* align-items:flex-start matters, not just style: flexbox's default (stretch)
   was overriding aspect-ratio entirely, stretching every tile to the carousel's
   full height regardless of aspect-ratio — that's what was actually causing the
   "too elongated" look, not the aspect-ratio value itself. */
.carousel{flex:0 0 auto;min-height:0;display:flex;align-items:flex-start;gap:10px;overflow-x:auto;overflow-y:hidden;padding:0 8px 10px;scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch}
.carousel::-webkit-scrollbar{display:none}
/* Big, immersive tiles — one carousel now doing the job two used to, so each
   tile gets to be as large as that combined space allows (same 3/4 shape that
   already proved not to look elongated, just scaled up). */
.profileCard{
  position:relative;flex:0 0 64%;aspect-ratio:5/8;border-radius:14px;overflow:hidden;cursor:pointer;
  background:radial-gradient(120% 80% at 50% 30%,#1a1a1a 0%,#050505 70%);
  border:1px solid var(--line);scroll-snap-align:start;
}
/* Desktop / big screens: same single-line carousel, just more tiles fit at once
   because each one is a fixed width instead of a percentage of a narrow phone.
   Text was staying at its mobile pixel size while the tile itself grew to
   300px+ — same words, suddenly tiny against a much bigger tile. Scaled up
   to match. */
@media (min-width:700px){
  .profileCard{flex:0 0 300px}
  .cardInfo .nm{font-size:19px}
  .cardInfo .mindPreview{font-size:16px}
  .cardInfo{padding:12px 16px}
}
/* Card with a profile photo: cover image + a bottom gradient so the name stays legible */
.profileCard.hasPhoto{background-color:var(--bg);background-size:cover;background-position:center;background-repeat:no-repeat}
.profileCard::after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,transparent 58%,rgba(0,0,0,.82) 100%)}
.profileCard .avatar{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:800;color:var(--text);background:var(--card)}
/* Gentle crossfade between rotating photos (no hard flash, no swipe gesture needed). */
.cardFade{position:absolute;inset:0;z-index:0;background-size:cover;background-position:center;opacity:0;transition:opacity .9s ease-in-out}
.cardFade.show{opacity:1}

/* Person/group cards: TILE SIZE IS LOCKED (aspect-ratio 5/8) — do not change.
   Only the internal photo/info split moves: photo now takes the large majority
   of the tile (78%) so you see as much of the actual image as possible without
   stretching it (background-size:cover crops, never stretches), and the info
   strip is a compact 22% band at the bottom for name/age + "what's on my mind"
   (or a group's about text). */
.cardPhoto{position:absolute;top:0;left:0;right:0;height:78%;background-color:var(--card);background-size:cover;background-position:center;background-repeat:no-repeat;touch-action:pan-x}
.cardPhoto::after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,transparent 72%,#000 100%)}
.cardPhoto .avatar{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:800;color:var(--text);background:var(--card)}
/* This card type has its own gradient (above) + solid black info strip, so the
   generic full-card ::after (built for text-over-photo) would just double up. */
.splitCard::after{display:none}
.cardInfo{position:absolute;left:0;right:0;bottom:0;height:22%;z-index:1;background:#000;padding:6px 12px;display:flex;flex-direction:column;justify-content:center;gap:2px;overflow:hidden}
.cardInfo .who{display:contents}
.cardInfo .dot{width:8px;height:8px;border-radius:50%;background:#33d17a;flex:0 0 auto;display:inline-block;margin-right:5px;vertical-align:middle}
.cardInfo .dot.off{background:#ff4d4d}
.cardInfo .nm{font-size:15px;font-weight:800;color:#fff;overflow-wrap:break-word;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* A group's "what's this about" text stands in for both name + bio — needs to
   wrap over a couple of lines, not truncate to one like a short "Name, 29". */
.cardInfo .groupNm{white-space:normal;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;line-height:1.25;font-size:13.5px}
.cardInfo .mindPreview{font-size:12.5px;font-weight:600;color:#d3d3d3;overflow-wrap:break-word;margin-top:1px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.3}

/* Photo indicator dots — VERTICAL (swipe is up/down to cycle photos; a
   horizontal row of dots was signalling the wrong axis). Sits down the right
   edge of the PHOTO area only, tap one to jump straight to that photo. */
.photoDots{position:absolute;top:0;bottom:0;right:8px;z-index:2;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:6px;pointer-events:auto}
.pdot{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.4);cursor:pointer}
.pdot.on{background:#fff}
/* Top-right corner badge (AI / GROUP / LOCKED / YOU) */
.profileCard .tag{position:absolute;top:5px;right:5px;z-index:2;background:rgba(0,0,0,.55);color:#fff;font-size:8.5px;font-weight:800;letter-spacing:.03em;text-transform:uppercase;padding:3px 6px;border-radius:6px;display:inline-flex;align-items:center;gap:3px}
/* Bottom-right (not top:5px like every other tile's tag) — mirrors the online
   dot sitting bottom-left, and keeps the avatar's own centered space clear of
   both the tag and the "Update your profile" bar above it. */
/* Top-left, level with the Edit-profile badge (top-right) — a symmetric
   header row instead of "YOU" sitting alone at the bottom, easy to miss
   next to the name it's labelling. */
.profileCard.selfCard .tag{color:#33d17a;top:6px;left:6px;bottom:auto;right:auto}
/* AI disclosure badge — bigger and bolder than the other tags on purpose: this
   is the "disclosed AI" label, it should never be easy to miss. */
.profileCard.aiCard .tag{font-size:12px;padding:5px 9px;border-radius:8px;color:#c9a8ff;background:rgba(0,0,0,.7);border:1.5px solid #c9a8ff}
/* Locked group badge: bigger block, padlock stacked ABOVE the label, gold so it
   stands out against every photo instead of blending into the usual dark pill. */
.profileCard.locked .tag{flex-direction:column;gap:3px;padding:7px 9px;font-size:9.5px;color:#ffc94d;background:rgba(0,0,0,.75);border:1.5px solid #ffc94d;border-radius:10px}
.profileCard.locked .tag .lockIco{width:20px;height:20px}
/* Vape-to-enter badge: same stacked-block treatment as Locked, distinct green
   so the two gate types read as different things at a glance. */
.profileCard.vapeGated .tag{flex-direction:column;gap:3px;padding:7px 9px;font-size:9.5px;color:#33d17a;background:rgba(0,0,0,.75);border:1.5px solid #33d17a;border-radius:10px;text-align:center;max-width:110px;line-height:1.3}
.profileCard.vapeGated .tag .vapeGateIco{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* Top-left "Not for me" — small pill: X icon + a short label so it reads at a glance */
.profileCard .hideBtn{position:absolute;top:5px;left:5px;z-index:2;height:20px;border-radius:10px;background:rgba(0,0,0,.55);border:0;color:#fff;font-size:9px;font-weight:700;letter-spacing:.01em;line-height:1;cursor:pointer;display:flex;align-items:center;gap:3px;padding:0 7px 0 6px}
.profileCard .hideBtn:hover,.profileCard .hideBtn:active{background:rgba(0,0,0,.8)}
.profileCard .hideBtn .hideX{display:block;font-size:10px;opacity:.85}
.profileCard .hideBtn .hideLbl{white-space:nowrap}
/* Bottom overlay: online dot + name, age, and (self only) a "what's on my mind"
   preview — the whole point of going back to 2-up is room to read this. */
.profileCard .cardFoot{position:absolute;left:9px;right:9px;bottom:8px;z-index:1;display:flex;flex-wrap:wrap;align-items:center;gap:5px;min-width:0}
.profileCard .who{display:contents} /* the dot + name are laid out directly by .cardFoot now */
.profileCard .who .dot{width:9px;height:9px;border-radius:50%;background:#33d17a;flex:0 0 auto;box-shadow:0 0 0 2px rgba(0,0,0,.45)}
.profileCard .who .dot.off{background:#ff4d4d;box-shadow:0 0 0 2px rgba(0,0,0,.45)}
/* Name and "what's on my mind"/group-name text WRAP instead of clipping — the
   point is to actually read it over the photo, not lose it to an ellipsis. */
.profileCard .cardFoot .nm{font-size:15px;font-weight:800;color:#fff;overflow-wrap:break-word;white-space:normal;text-shadow:0 1px 3px rgba(0,0,0,.6)}
.profileCard .cardFoot .mindPreview{flex:1 0 100%;font-size:12px;font-weight:600;color:#d8d8d8;text-shadow:0 1px 3px rgba(0,0,0,.7);overflow-wrap:break-word;white-space:normal;line-height:1.3}
.profileCard.dismissing{transform:scale(.7);opacity:0;pointer-events:none;transition:transform .3s ease,opacity .3s ease}
.carouselEmpty{grid-column:1/-1;padding:40px 20px;color:var(--muted);text-align:center;font-weight:600}
/* Skeleton cards shown until the first feed arrives — the carousel never looks empty. */
.skelCard{flex:0 0 min(88%,360px);height:100%;border-radius:22px;border:1px solid var(--line);
  background:linear-gradient(100deg,#141414 30%,#1e1e1e 50%,#141414 70%);background-size:220% 100%;
  animation:skelShimmer 1.3s ease-in-out infinite}
@keyframes skelShimmer{0%{background-position:180% 0}100%{background-position:-80% 0}}

/* Inbox — shares the screen with profiles; shows ~4 rows then scrolls */
.inbox{flex:0 0 auto;display:flex;flex-direction:column;border-top:1px solid var(--line-soft)}
/* Capped to ~3 conversation rows tall, then scrolls for the rest. */
/* Exactly 3 full rows (measured: 71px each), not 2-and-a-sliver. */
.inboxList{padding:2px 16px 14px;overflow-y:auto;max-height:213px;-webkit-overflow-scrolling:touch}
.inboxRow{padding:14px 4px}
.inboxEmpty{color:var(--muted-dim);font-size:14px;padding:8px 4px 12px}
.inboxRow{display:flex;align-items:center;gap:12px;padding:12px 4px;border-bottom:1px solid var(--line-soft);cursor:pointer}
/* Round thumbnail of who the chat is with: first photo, else room emoji, else initial. */
.inboxAvatar{flex:0 0 auto;width:42px;height:42px;border-radius:50%;overflow:hidden;
  background:var(--card) center/cover no-repeat;
  display:flex;align-items:center;justify-content:center;font-weight:800;font-size:17px;color:var(--muted)}
.inboxAvatar.room{font-size:20px}
.inboxRow.unread .inboxAvatar{box-shadow:0 0 0 2px #33d17a}
.inboxMeta{flex:1;min-width:0}
.inboxName{font-weight:800;font-size:14.5px;color:var(--muted)}
.inboxRow.unread .inboxName{color:var(--text)}
.inboxPreview{color:var(--muted-dim);font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.inboxRow.unread .inboxPreview{color:var(--muted)}
.inboxOpen{flex:0 0 auto;background:var(--text);color:var(--bg);border:0;border-radius:13px;padding:8px 16px;font-size:13.5px;font-weight:800;cursor:pointer}

/* Filter panel */
.sheet{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;align-items:flex-end;z-index:60}
.sheet.on{display:flex}
.sheetCard{background:var(--card);border:1px solid var(--line);border-radius:22px 22px 0 0;width:100%;padding:20px;display:flex;flex-direction:column;gap:16px}
.sheetCard h3{margin:0;font-size:20px;font-weight:800}
.rangeRow{display:flex;align-items:center;gap:12px}
.rangeRow input[type=range]{flex:1}
.rangeRow .val{min-width:96px;text-align:right;font-weight:800}
.sheetHint{margin:2px 0 14px;font-size:12px;color:var(--muted);line-height:1.4}
.sheetActions{display:flex;gap:12px}
.sheetActions button{flex:1;border-radius:14px;padding:14px;font-weight:800;border:1px solid var(--line);background:var(--bg);color:var(--text)}
.sheetActions .apply{background:var(--text);color:var(--bg);border-color:var(--text)}

/* ---------- Chat ---------- */
#chat{display:none;flex-direction:column;height:100vh;height:100dvh;overflow:hidden;background:var(--bg)}
#chat.on{display:flex}
.chatHead{display:flex;align-items:center;gap:10px;padding:calc(12px + env(safe-area-inset-top)) 12px 12px;border-bottom:1px solid var(--line-soft);flex:0 0 auto}
/* Fixed-size, always-tappable back target that sits below the notch/status bar. */
.backBtn{flex:0 0 auto;width:42px;height:42px;display:flex;align-items:center;justify-content:center;background:none;border:0;color:var(--text);font-size:34px;line-height:1;padding:0 0 4px;cursor:pointer}
.chatWho{margin-right:auto}
.msgTimer{font-size:11px;font-weight:700;opacity:.6;margin-left:8px;white-space:nowrap;color:var(--muted)}
.chatWho .chatName{font-size:20px;font-weight:800;line-height:1.1}
.chatWho .chatSub{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:13px;font-weight:700;margin-top:2px}
.chatWho .chatSub .dot{width:8px;height:8px;border-radius:50%;background:#33d17a}
/* touch-action:pan-y — vertical scrolling stays fully native/smooth, but the
   browser no longer claims a HORIZONTAL touch drag as its own pan gesture
   before JS gets to see it (see wireSwipeToPeek in chat.js): without this,
   Chromium fires pointercancel after the very first pointermove of a
   sideways drag, so the swipe-to-peek gesture never gets enough movement
   events to recognize itself. */
.msgs{flex:1 1 auto;min-height:0;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px;-webkit-overflow-scrolling:touch;
  touch-action:pan-y;transition:transform .35s ease,opacity .35s ease}
/* Swipe-to-peek (see wireSwipeToPeek in chat.js): slides the whole thread out
   of the way so the wallpaper behind it is fully visible, then slides itself
   back on its own — transform/opacity only, so scroll position underneath is
   completely untouched by the peek. */
.msgs.peeking{transform:translateX(-92%);opacity:.12;pointer-events:none}
/* Sticks a short conversation to the bottom of the box (instead of
   justify-content:flex-end on the scroll container itself, which is a known
   Chromium/Android bug: once content overflows, the browser miscalculates
   the scrollable range and the top of the conversation becomes unreachable —
   this is the "can't scroll up on Android" report. margin-top:auto on the
   first bubble gets the same "hug the bottom when there's slack" look
   without putting the scroll container itself in a reversed/end state. */
.msgs>.msg:first-child{margin-top:auto}
/* Everything hugs the LEFT — the wallpaper vape is the star of a chat, so the
   right side of the screen stays clear to watch it. Bubbles are deliberately
   a single black/gunmetal palette — no per-sender shades, no white "me"
   bubble — so the wallpaper stays the only color in the room. */
.msg{position:relative;max-width:min(68%,420px);padding:11px 14px;border-radius:18px;font-size:16px;line-height:1.35;overflow-wrap:anywhere;align-self:flex-start}
.msg.them{background:#141414;border:1px solid var(--line);border-bottom-left-radius:6px}
.msg.me{background:#2c3539;color:#fff;border-bottom-left-radius:6px}
.msg a{color:#6fc3ff;text-decoration:underline;word-break:break-all}
/* @mentions: only ever wraps a real current member's name (see mentionify in
   chat.js) — highlighted always, and made unmissable when it's you. */
.mention{color:#8ab4ff;font-weight:800}
.mentionMe{background:rgba(224,36,94,.28);border-radius:4px;padding:0 3px;color:#ff8fa8}
/* Location share: an icon chip, not raw coordinates — tap opens Maps directly. */
.locChip{display:inline-flex;align-items:center;gap:7px;color:inherit;text-decoration:none;font-weight:700}
.locChip svg{fill:#33d17a}
/* One rule for every screen size, not a mobile guess + a separate desktop
   guess: the 375px/450px caps that read as "correct" on a laptop only look
   right there because the SAME percentages (72.5%/87%) also happen to be
   irrelevant at that width — 87% of a 1440px laptop is 1253px, so the fixed
   450px cap is doing 100% of the work and the percentage never engages. On a
   390px phone, that percentage suddenly IS the binding constraint (87% of
   390px = 339px), which is why the two ends of one "responsive" rule ended up
   looking like completely different sizes rather than the same proportion.
   vw is measured against the actual viewport (not a parent's percentage, so
   nested padding/layout can't throw off the ratio), computed directly from
   the confirmed-correct laptop numbers at a 1440px reference width — the
   SAME real-world proportion at every width, not a re-guessed one per
   breakpoint. clamp()'s max reproduces the exact laptop size and stops it
   growing further on an ultra-wide monitor; its min is only a hard floor for
   an unrealistically narrow viewport, kept BELOW what a real phone computes
   (390px x 26.04vw = ~102px) so it never actually overrides the ratio on an
   actual phone — that was the bug in the first version of this rule: the
   floor was set high enough to win at ordinary phone widths, silently
   breaking the "same ratio everywhere" goal exactly where it mattered most. */
.msg.media{padding:3px;background:none;border:0;max-width:clamp(80px,26.04vw,375px)}
.msg.media img,.msg.media video{display:block;width:100%;max-height:clamp(90px,31.25vw,450px);object-fit:cover;border-radius:16px}
/* Video bubbles specifically (not photos) stay a bit bigger than photos —
   the looping clip is the thing worth seeing clearly in the feed. */
.msg.media.video{max-width:clamp(90px,31.25vw,450px)}
.msg.media.video video{max-height:clamp(100px,37.5vw,540px)}
/* Touch devices (phones/tablets) get their OWN size, not a shrunk-down
   version of the desktop ratio. A prior pass here just halved the desktop
   numbers and it came out illegibly tiny in real use. The right reference
   point is what every mainstream chat app already does: WhatsApp/iMessage/
   Telegram size media previews at roughly 60-65% of screen width on mobile —
   noticeably LARGER than desktop's ratio (31%), not smaller, because a
   narrow phone has far less room to spare, so the preview needs a bigger
   share of it to stay legible. `pointer: coarse` targets actual touch input,
   not just a narrow window, so a small desktop browser isn't caught by this.
   Text bubbles are unaffected — only media (photo/video) preview size. */
@media (pointer:coarse){
  .msg.media{max-width:min(58%,260px)}
  .msg.media img,.msg.media video{max-height:260px}
  .msg.media.video{max-width:min(65%,300px)}
  .msg.media.video video{max-height:320px}
}
/* Fade photos in as they load (no hard pop). Local optimistic previews get .loaded up front. */
.msg.media img{opacity:0;transition:opacity .35s ease}
.msg.media img.loaded{opacity:1}
/* Optimistic upload: instant local preview with a small corner spinner. */
.msg.media.uploading{position:relative}
.msg.media.uploading img,.msg.media.uploading video{opacity:.9}
.upSpin{position:absolute;top:8px;right:8px;width:18px;height:18px;border-radius:50%;border:2px solid rgba(255,255,255,.35);border-top-color:#fff;animation:upspin .8s linear infinite}
@keyframes upspin{to{transform:rotate(360deg)}}
.msg.media .mediaCap{padding:6px 8px 2px;font-size:15px}
/* Chat video: autoplays and loops on its own, like a GIF (see chat.js). */
.vidWrap{position:relative;line-height:0;cursor:pointer}
.vidWrap video{background:#000}
/* Same wrapper shape as .vidWrap, for the same reason: a positioning anchor
   for the drag-hint icon (see image-holder.js — the custom pop-out for
   photos, since native PiP is video-only). */
.imgWrap{position:relative;line-height:0}
/* "You can drag this out" — AND a real tap target (see the click handler in
   chat.js). Touch-drag gesture recognition on <video> elements is inconsistent
   across mobile browsers (iOS Safari in particular often swallows it before
   it ever reaches our pointer-event tracking), so a plain tap here is the
   reliable fallback — desktop can still just drag the whole video instead.
   A gentle side-to-side nudge on the hand, not a full swipe, so it reads as a
   hint rather than an actual instruction to follow. */
.dragHint{position:absolute;top:8px;left:50%;transform:translateX(-50%);z-index:2;
  cursor:pointer;display:flex;align-items:center;gap:4px;
  background:rgba(0,0,0,.55);color:#fff;font-size:11px;font-weight:700;
  padding:6px 11px;border-radius:999px;line-height:1;animation:dragHintPulse 2.2s ease-in-out infinite}
.dragHintHand{font-size:14px;display:inline-block;animation:dragHintNudge 2.2s ease-in-out infinite}
.dragHintArrows{opacity:.85}
@keyframes dragHintNudge{0%,100%{transform:translateX(-3px)}50%{transform:translateX(3px)}}
@keyframes dragHintPulse{0%,100%{opacity:.6}50%{opacity:1}}

/* Custom image pop-out — freely draggable (by its header, see
   image-holder.js), holds several photos at once, fixed so it stays on top
   regardless of which screen is showing (same z-index tier as the vape
   overlay/menu). Default position: upper-left, clear of the vape preview
   (bottom-right) and the menu tab/wallpaper-react button (right edge). */
.imageHolder{position:fixed;top:90px;left:16px;z-index:70;width:210px;max-height:min(60vh,420px);
  background:#0d0d0d;border:1.5px solid rgba(255,255,255,.18);border-radius:16px;
  box-shadow:0 12px 34px rgba(0,0,0,.55);display:flex;flex-direction:column;overflow:hidden}
.imageHolder[hidden]{display:none}
.imageHolderHead{display:flex;align-items:center;justify-content:space-between;
  padding:8px 10px;background:rgba(255,255,255,.06);cursor:grab;user-select:none;touch-action:none;flex:0 0 auto}
.imageHolderHead:active{cursor:grabbing}
.imageHolderTitle{font-size:11px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
.imageHolderClose{width:22px;height:22px;border-radius:50%;background:rgba(255,255,255,.1);
  border:0;color:#fff;font-size:15px;line-height:1;cursor:pointer;flex:0 0 auto}
.imageHolderTiles{display:flex;flex-wrap:wrap;gap:8px;padding:10px;overflow-y:auto}
.imageHolderTile{position:relative;width:84px;height:84px;border-radius:10px;overflow:hidden;background:#000;flex:0 0 auto}
.imageHolderTile img{width:100%;height:100%;object-fit:cover;display:block}
.imageHolderTileRemove{position:absolute;top:3px;right:3px;width:19px;height:19px;border-radius:50%;
  background:rgba(0,0,0,.65);border:0;color:#fff;font-size:13px;line-height:1;cursor:pointer}

/* Sender name over every incoming bubble, so you always know who sent it. */
.msgFrom{display:block;font-size:11px;font-weight:800;color:#33d17a;margin-bottom:3px;line-height:1.1}
.msg.media .msgFrom{padding:4px 8px 2px}
.msg.uploading{background:#1b1b1b;color:var(--muted);border:1px solid var(--line);font-style:italic}

.msgsEmpty{margin:auto;color:var(--muted-dim);font-size:14px;text-align:center}

/* @mention autocomplete — position (left/right/bottom) is set in JS from the
   composer's real on-screen position, so it holds up across the mobile/laptop
   layouts and the safe-area padding rather than a guessed fixed offset. */
.mentionDropdown{position:fixed;z-index:6;background:#141414;border:1px solid #2a2a2a;border-radius:14px;padding:6px;box-shadow:0 8px 24px rgba(0,0,0,.45);max-height:200px;overflow-y:auto}
.mentionItem{display:flex;align-items:center;padding:9px 10px;border-radius:9px;font-size:14px;font-weight:700;color:var(--text);cursor:pointer}
.mentionItem.active,.mentionItem:hover{background:rgba(255,255,255,.1)}

.composer{display:flex;align-items:center;gap:10px;padding:12px 14px calc(12px + env(safe-area-inset-bottom));border-top:1px solid var(--line-soft);flex:0 0 auto}
.attachBtn{flex:0 0 auto;width:44px;height:44px;border-radius:50%;background:var(--card);border:1px solid var(--line);color:var(--text);display:flex;align-items:center;justify-content:center;cursor:pointer}
.attachBtn svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:2.6;stroke-linecap:round}
/* min-width:0 lets the input shrink so the send/VAPE buttons never push off-screen. */
.msgInput{flex:1;min-width:0;background:var(--card);border:1px solid var(--line);border-radius:22px;color:var(--text);font-size:16px;padding:13px 16px;outline:none}
.msgInput:focus{border-color:var(--text)}
.sendBtn{flex:0 0 auto;width:48px;height:48px;border-radius:50%;background:var(--text);border:0;display:flex;align-items:center;justify-content:center;cursor:pointer}
.sendBtn svg{width:22px;height:22px;fill:var(--bg)}
/* Clean brand-mark record button — a white disc with the Vapor bandit, matching the
   composer's send button. Recording: fills red with a soft pulse (like the mic). */
.vapeBtn{flex:0 0 auto;width:48px;height:48px;border-radius:50%;background:var(--text);border:0;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.35);transition:background .2s ease}
.vapeBtn .vapeMask{width:26px;height:16px;filter:invert(1)}          /* white bandit → black on the white disc */
.vapeBtn.recording{background:#e0245e;animation:micPulse 1.2s infinite}
.vapeBtn.recording .vapeMask{filter:none}                            /* white bandit on the red disc */
.vapeBtn:active{transform:scale(.94)}

/* Vape capture: small live preview floating in the top-right (not full-screen). */
/* Bottom-right, just above the composer, so the record dot is thumb-reachable. */
.vapeOverlay{position:fixed;bottom:calc(84px + env(safe-area-inset-bottom));right:12px;z-index:80;display:none}
.vapeOverlay.on{display:block}
.vapeCard{position:relative;width:150px;height:200px;border-radius:16px;overflow:hidden;background:#000;
  box-shadow:0 12px 34px rgba(0,0,0,.55);border:2px solid rgba(255,255,255,.18);transition:border-color .2s ease}
/* Recording state on the card itself — the composer's vapeBtn.recording is out of
   sight during a "vape to enter" capture (no chat open yet), so the card needs its
   OWN red tell that recording is live, not just the button behind it. */
.vapeCard.recording{border-color:#e0245e;animation:vapeCardPulse 1.2s infinite}
@keyframes vapeCardPulse{0%,100%{box-shadow:0 12px 34px rgba(0,0,0,.55),0 0 0 0 rgba(224,36,94,.5)}50%{box-shadow:0 12px 34px rgba(0,0,0,.55),0 0 0 6px rgba(224,36,94,0)}}
#vapePreview{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
#vapePreview.mirror{transform:scaleX(-1)}          /* selfie mirror for front camera only */
.vapeTimer{position:absolute;top:6px;left:9px;z-index:2;color:#fff;font-weight:800;font-size:13px;text-shadow:0 1px 3px rgba(0,0,0,.7)}
.vapeCancel{position:absolute;top:4px;right:6px;z-index:2;width:26px;height:26px;border-radius:50%;
  background:rgba(0,0,0,.5);border:0;color:#fff;font-size:18px;line-height:1;cursor:pointer}
/* Flip front ↔ back camera — bottom-right, out of the way of the countdown/timer.
   In chat, recording is started/stopped from the composer's vape button itself
   (it flashes red while recording — see .vapeBtn.recording). A "vape to enter"
   recording has no composer on screen at all, so it gets its own stop control —
   see .vapeStopEntry below. */
.vapeFlip{position:absolute;bottom:8px;right:8px;z-index:2;width:30px;height:30px;border-radius:50%;
  background:rgba(0,0,0,.5);border:0;display:flex;align-items:center;justify-content:center;cursor:pointer}
.vapeFlip svg{width:17px;height:17px;fill:none;stroke:#fff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* 3·2·1 countdown over the preview */
.vapeCountdown{position:absolute;inset:0;z-index:3;display:flex;align-items:center;justify-content:center;color:#fff;font-size:72px;font-weight:800;text-shadow:0 2px 12px rgba(0,0,0,.7);background:rgba(0,0,0,.35)}
.vapeCountdown[hidden]{display:none}
/* "Vape to enter" only: a persistent minimum-length label (not just a toast
   that fades) so the requirement stays visible the whole time you're
   recording — bottom-left, clear of the flip button's bottom-right corner.
   Swapped out for .vapeStopEntry (below) the instant the minimum is met. */
.vapeMinHint{position:absolute;bottom:8px;left:9px;z-index:2;
  color:#fff;font-size:10.5px;font-weight:800;letter-spacing:.01em;
  background:rgba(0,0,0,.55);padding:3px 7px;border-radius:999px;white-space:nowrap}
.vapeMinHint[hidden]{display:none}
/* The actual STOP control for a "vape to enter" recording — appears the moment
   the 7s minimum is reached. Deliberately the SAME red-disc-with-icon look as
   the composer's vapeBtn.recording (same micPulse animation too), so it reads
   as "recording, tap to finish" at a glance, same as it would in a chat.
   Centered at the bottom rather than the vapeMinHint's left slot — this card
   is only 150px wide, and a wider control there would collide with vapeFlip's
   bottom-right corner. */
.vapeStopEntry{position:absolute;bottom:8px;left:50%;transform:translateX(-50%);z-index:2;
  width:44px;height:44px;border-radius:50%;background:#e0245e;border:0;cursor:pointer;
  display:flex;align-items:center;justify-content:center;animation:micPulse 1.2s infinite;
  box-shadow:0 1px 6px rgba(0,0,0,.4)}
.vapeStopEntry .vapeMask{width:24px;height:15px;filter:none}
.vapeStopEntry[hidden]{display:none}

/* Chat MENU tab (right edge, thumb-height) + slide-out panel */
/* The MENU tab stays STATIC on top (z-index above the panel), so tapping it always
   toggles. The panel slides out from behind it and slides back off when tapped again. */
.menuTab{position:fixed;right:0;top:50%;transform:translateY(-50%);z-index:43;background:var(--card);border:1px solid var(--line);border-right:0;border-radius:14px 0 0 14px;color:var(--text);font-weight:800;font-size:11px;letter-spacing:.14em;padding:16px 7px;writing-mode:vertical-rl;cursor:pointer}
.menuTab.open{background:#e0245e;border-color:#e0245e}
/* React-to-wallpaper button — sits directly above the menu tab, same right
   edge. Position is measured in JS for the picker (see wallpaper.js) so it
   holds up regardless of exact button size/layout. */
.wallReactBtn{position:fixed;right:10px;top:50%;transform:translateY(-50%) translateY(-92px);z-index:43;width:38px;height:38px;border-radius:50%;background:var(--card);border:1px solid var(--line);color:var(--text);font-size:17px;line-height:1;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.35)}
.wallReactBtn:active{background:#e0245e;border-color:#e0245e}
.wallReactPicker{position:fixed;z-index:44;display:none;flex-direction:column;gap:2px;background:#0d0d0d;border:1px solid var(--line);border-radius:14px;padding:6px;box-shadow:0 8px 24px rgba(0,0,0,.45)}
.wallReactPicker.on{display:flex}
.wallReactPicker button{background:none;border:0;font-size:21px;line-height:1;padding:6px;cursor:pointer;border-radius:9px}
.wallReactPicker button:hover,.wallReactPicker button:active{background:rgba(255,255,255,.12)}

/* Full-screen wallpaper-reaction burst — decorative only, never intercepts a
   tap, and respects reduced-motion. */
.burstLayer{position:fixed;inset:0;z-index:70;overflow:hidden;pointer-events:none}
.burstEmoji{position:absolute;bottom:-10vh;line-height:1;will-change:transform,opacity;animation-name:burstFloat;animation-timing-function:cubic-bezier(.15,.7,.4,1);animation-fill-mode:forwards}
@keyframes burstFloat{
  0%{transform:translate(0,0) rotate(0deg) scale(.5);opacity:0}
  12%{opacity:1}
  100%{transform:translate(var(--drift),-115vh) rotate(var(--rotate)) scale(1);opacity:0}
}
@media (prefers-reduced-motion:reduce){
  .burstEmoji{animation:none;opacity:0}
}
/* Panel sits to the LEFT of the tab (right:30px = tab width) so the tab is never covered. */
.chatMenu{position:fixed;right:30px;top:50%;transform:translateY(-50%) translateX(calc(100% + 30px));z-index:42;background:#0d0d0d;border:1px solid var(--line);border-radius:16px 0 0 16px;padding:12px;display:flex;flex-direction:column;gap:8px;min-width:220px;transition:transform .22s ease;box-shadow:-8px 0 30px rgba(0,0,0,.5)}
.chatMenu.on{transform:translateY(-50%) translateX(0)}
.menuItem{display:flex;align-items:center;gap:11px;background:var(--bg);border:1px solid var(--line);color:var(--text);border-radius:12px;padding:13px 15px;font-weight:700;font-size:15px;text-align:left;cursor:pointer}
.menuItem:active{background:#151515}
/* Line-drawn (sketch) icons, matching the app's SVG branding — no photo-like emoji. */
.menuItem .ic{flex:0 0 auto;width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
/* Vaporising toggle row */
.menuToggle .menuLabel{flex:1}
.switch{flex:0 0 auto;width:44px;height:26px;border-radius:999px;background:#3a3a3a;position:relative;transition:background .2s}
.switch .knob{position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;transition:transform .2s}
.switch.on{background:#33d17a}
.switch.on .knob{transform:translateX(18px)}

/* Update Profile photo picker */
.photoRow{display:flex;gap:10px;flex-wrap:wrap}
.photoSlot{position:relative;width:84px;height:84px;border-radius:14px;background-size:cover;background-position:center;border:1px solid var(--line)}
.photoDel{position:absolute;top:-7px;right:-7px;width:24px;height:24px;border-radius:50%;background:#e0245e;color:#fff;border:2px solid #0a0a0a;font-weight:800;line-height:1;cursor:pointer}
.photoAdd{width:84px;height:84px;border-radius:14px;background:var(--bg);border:1.5px dashed var(--line);color:var(--muted);font-weight:400;font-size:34px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}
.photoRow{gap:10px}
#bioInput{min-height:72px;resize:none}

/* Toast */
#toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%) translateY(20px);
  background:var(--text);color:var(--bg);font-weight:700;font-size:14px;padding:12px 18px;border-radius:12px;
  opacity:0;pointer-events:none;transition:.2s;z-index:100;max-width:88vw;text-align:center}
#toast.on{opacity:1;transform:translateX(-50%) translateY(0)}

/* One-time "you can drag videos out" nudge — sits just under chatHead,
   same fade pattern as #toast but its own element so it can't collide
   with an unrelated toast firing at the same time. */
.dragTipBanner{position:absolute;top:calc(12px + env(safe-area-inset-top) + 54px);left:50%;
  transform:translateX(-50%) translateY(-8px);z-index:5;background:var(--text);color:var(--bg);
  font-weight:700;font-size:13.5px;padding:10px 16px;border-radius:12px;opacity:0;
  pointer-events:none;transition:opacity .3s ease,transform .3s ease;max-width:min(88vw,360px);text-align:center}
.dragTipBanner.show{opacity:1;transform:translateX(-50%) translateY(0)}

/* Vaporize confirm — two buttons, matching the intro/login panel */
.vconfirmRow{display:flex;gap:12px;margin-top:8px}
.vconfirmRow .introClose{margin-top:0;flex:1}
.vconfirmCancel{flex:1;background:var(--bg);color:var(--text);border:1px solid var(--line);border-radius:14px;padding:15px;font-weight:800;font-size:16px;cursor:pointer}

/* "Vaporizing…" busy state — replaces the confirm prompt the instant Vaporize
   is tapped, so there's real feedback during the round-trip instead of a
   static screen that looks like nothing happened. */
/* :not([hidden]) — plain #vaporizeBusy{display:flex} would override the
   browser's own [hidden]{display:none} (author CSS always wins over the UA
   stylesheet, whatever the specificity), so setting .hidden=true in JS did
   nothing and the spinner showed the instant the confirm dialog opened. */
#vaporizeBusy:not([hidden]){text-align:center;display:flex;flex-direction:column;align-items:center;gap:2px}
.vaporizeSpinner{width:44px;height:44px;margin-bottom:6px}
.vaporizeSpinner svg{width:100%;height:100%;animation:vapSpin 1s linear infinite}
.vaporizeSpinner circle{stroke:#e0245e;stroke-dasharray:42 100;stroke-linecap:round}
@keyframes vapSpin{to{transform:rotate(360deg)}}

/* Themed chat-room tiles in the grid */
.roomCard{background:radial-gradient(120% 80% at 50% 25%,#171a2b 0%,#050505 72%)}
.roomCard .avatar.roomAvatar{background:rgba(255,255,255,.04)}
.roomCard .tag{color:#8ab4ff}
.roomCard.locked .tag{color:#ffcf6f;display:inline-flex;align-items:center;gap:3px}
.lockIco{width:11px;height:11px;fill:none;stroke:currentColor;stroke-width:2;vertical-align:-1px}
.roomIc{fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
.roomAvatar .roomIc{width:34px;height:34px}
/* Local Events tile in the grid (teal accent) */
.eventsCard{background:radial-gradient(120% 80% at 50% 25%,#0e2a20 0%,#050505 72%)}
.eventsCard .tag{color:#33d17a}
.calIco{fill:none;stroke:#33d17a;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.eventsAvatar{background:rgba(51,209,122,.08)}
.eventsAvatar .eventsIco{width:76px;height:76px;stroke-width:3.4}
/* Rotating "what's coming up" preview (see loadEventsPreview in profiles.js)
   — swapped in over the plain icon once there's real event data. Stacked and
   cross-faded exactly like .cardFade's photo rotation, just text instead of
   a background image. Falls back to the plain icon when the board is empty,
   so an empty area never looks broken — just quieter. */
.evPreviewItem{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:5px;padding:0 22px;text-align:center;opacity:0;
  transition:opacity .6s ease-in-out}
.evPreviewItem.show{opacity:1}
.evPreviewWhen{font-size:11.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:#33d17a}
.evPreviewTitle{font-size:17px;font-weight:800;color:var(--text);line-height:1.28}
.evPreviewVenue{font-size:13px;font-weight:600;color:var(--muted)}
.evPreviewDots{position:absolute;bottom:38px;left:0;right:0;z-index:1;display:flex;justify-content:center;gap:6px}
.evPreviewDots .pdot{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.35)}
.evPreviewDots .pdot.on{background:#33d17a}

/* Vapor bandit-mask on the vape button */
.vapeMask{width:22px;height:13px;display:inline-flex;pointer-events:none;margin:1px 0}
.vapeMask svg{width:100%;height:100%}

/* "Set the tone" — background video behind an independent chat */
#chat{position:relative}
/* Chat content floats above the video (menuTab/chatMenu keep their own fixed pos). */
.chatHead,.msgs,.composer{position:relative;z-index:2}
.toneLayer{position:absolute;inset:0;z-index:0;overflow:hidden;background:#000}
/* Two stacked video layers cross-dissolve (same pattern as the photo fallback) —
   the incoming clip fades IN while the outgoing one fades OUT, at the same time,
   so there's never a black gap between them. */
.toneVideoLayer{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;
  opacity:0;transition:opacity 1s ease-in-out}
.toneVideoLayer.show{opacity:1}
.toneScrim{position:absolute;inset:0;background:rgba(0,0,0,.5)}
/* Photo-wallpaper fallback: two layers cross-dissolve gently (no harsh cuts). */
.photoBg{position:absolute;inset:0;z-index:0;overflow:hidden;background:#000}
.photoBg[hidden]{display:none}
.photoBgLayer{position:absolute;inset:0;background-size:cover;background-position:center;opacity:0;transition:opacity 2.2s ease-in-out;transform:scale(1.04)}
.photoBgLayer.show{opacity:1}
/* the message list must be see-through so the tone shows behind the bubbles */
.toneLayer:not([hidden])~.chatHead ~ .msgs{background:transparent}
/* When a wallpaper (vape video or member photos) is behind the chat, fade the message
   list toward the top — fully INVISIBLE across the top stretch, ghosted through the
   middle, solid only near the composer — so the wallpaper plays uncovered. Scroll up
   and older messages slide down into clarity. */
/* Tuned from REAL measured bubble positions — a realistic mixed-length 12-message
   thread, not uniform placeholders (scripted, not eyeballed): on an iPhone-size
   screen, the 5th-from-bottom bubble's top lands at 49.2% of the container.
   Solid zone starts there, so the last 5 bubbles stay fully readable (up from
   an earlier pass that only kept 3 solid — reported as fading too soon) and
   everything older fades into the wallpaper above it. */
.toneLayer:not([hidden])~.msgs,.photoBg:not([hidden])~.msgs{
  -webkit-mask-image:linear-gradient(to bottom, transparent 0, transparent 34%, rgba(0,0,0,.4) 42%, #000 49%);
  mask-image:linear-gradient(to bottom, transparent 0, transparent 34%, rgba(0,0,0,.4) 42%, #000 49%);
}
/* Bigger screens (same 700px cutoff as the carousel's desktop layout above):
   .msgs is taller in real pixels, but bubble height stays the same (font
   doesn't scale with viewport) — so the mobile percentages alone would just
   silently reveal a little more without ever being deliberately tuned for it.
   Re-measured the same way at 1440×900 (.msgs 760px tall): the 7th-from-bottom
   bubble's top lands at 51.4%. Solid zone starts there, so a laptop shows the
   last 7 bubbles instead of mobile's 3 — there's simply more vertical room to
   give back to legible history on a bigger screen. */
@media (min-width:700px){
  .toneLayer:not([hidden])~.msgs,.photoBg:not([hidden])~.msgs{
    -webkit-mask-image:linear-gradient(to bottom, transparent 0, transparent 36%, rgba(0,0,0,.4) 44%, #000 51%);
    mask-image:linear-gradient(to bottom, transparent 0, transparent 36%, rgba(0,0,0,.4) 44%, #000 51%);
  }
}

/* Report & block */
.menuItem.danger{color:#ff5c8a}
.menuItem.danger .ic{stroke:#ff5c8a}
.sheetSub{margin:2px 0 12px;color:var(--muted);font-size:13.5px;line-height:1.4}
.reportReasons{display:flex;flex-direction:column;gap:8px}
.reportReason{width:100%;text-align:left;background:var(--card);border:1px solid var(--line);color:var(--text);border-radius:12px;padding:13px 14px;font-size:15px;font-weight:700;cursor:pointer}
.reportReason:active{background:#241318;border-color:#ff5c8a}

/* Inbox rows: swipe left to reveal Delete / Block (and, for groups, Leave) */
.inboxRow{display:block;position:relative;overflow:hidden;padding:0;border-bottom:1px solid var(--line-soft)}
/* Main row fully covers the actions until you swipe it left. */
.inboxRowMain{position:relative;z-index:1;width:100%;display:flex;align-items:center;gap:12px;padding:14px 4px;background:#050505;transition:transform .2s;touch-action:pan-y}
.inboxActions{position:absolute;right:0;top:0;bottom:0;display:flex;z-index:0}
.inboxAct{width:76px;border:0;color:#fff;font-weight:700;font-size:12px;letter-spacing:.01em;cursor:pointer}
.inboxAct.del{background:#3a3a3a}
.inboxAct.block{background:#e0245e}
.inboxAct.leave{background:#3a6ea5}
.inboxAct:disabled{opacity:.6;cursor:default}

/* ---------- Privacy hardening ---------- */
/* Media: block the long-press "Save Image" callout + text-selection + drag.
   (A deterrent against casual saving — screenshots can't be blocked on the web.) */
.msgs img,.msgs video,.profileCard img,.avatar img,.roomCard img{
  -webkit-touch-callout:none;-webkit-user-select:none;user-select:none;
  -webkit-user-drag:none;user-drag:none;
}


/* Backgrounding veil: frosts the whole app when it loses focus, so the phone's
   app-switcher snapshot shows nothing readable. */
#privacyVeil{
  position:fixed;inset:0;z-index:9999;display:none;
  align-items:center;justify-content:center;flex-direction:column;gap:14px;
  background:#000;
  -webkit-backdrop-filter:blur(26px);backdrop-filter:blur(26px);
}
body.veiled #privacyVeil{display:flex}
#privacyVeil .mask{width:64px;height:38px;opacity:.9}
#privacyVeil .veilWord{font-size:30px;font-weight:800;letter-spacing:.02em;color:#fff;opacity:.85}

/* ---------- Inbox row enhancements ---------- */
.inboxTotal{min-width:19px;height:19px;padding:0 6px;border-radius:10px;background:#33d17a;color:#04140b;
  font-size:11.5px;font-weight:800;display:inline-flex;align-items:center;justify-content:center}

/* Thumbnail wrapper carries the unread count badge in its corner. */
.inboxThumb{position:relative;flex:0 0 auto}
.inboxBadge{position:absolute;top:-3px;right:-4px;min-width:18px;height:18px;padding:0 5px;border-radius:9px;
  background:#e0245e;color:#fff;font-size:11px;font-weight:800;display:flex;align-items:center;justify-content:center;
  border:2px solid #050505}

/* Name + relative time on one line. */
.inboxTopline{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
.inboxTime{flex:0 0 auto;font-size:11px;font-weight:700;color:var(--muted-dim)}
.inboxRow.unread .inboxTime{color:#33d17a}

/* First-visit hint: gently reveal the swipe actions, then settle back. */
@keyframes inboxNudge{0%{transform:translateX(0)}30%{transform:translateX(-64px)}60%{transform:translateX(-64px)}100%{transform:translateX(0)}}
.inboxRowMain.hintNudge{animation:inboxNudge 1.4s ease}

/* ---------- Typing indicator (live presence) ---------- */
.typingRow{position:relative;z-index:2;display:flex;align-items:center;gap:8px;padding:2px 16px 6px;min-height:20px}
.typingRow[hidden]{display:none}
.typingName{color:var(--muted);font-size:12.5px;font-weight:700}
.typingDots{display:inline-flex;gap:3px}
.typingDots i{width:5px;height:5px;border-radius:50%;background:#33d17a;opacity:.4;animation:typingBlink 1.2s infinite ease-in-out}
.typingDots i:nth-child(2){animation-delay:.18s}
.typingDots i:nth-child(3){animation-delay:.36s}
@keyframes typingBlink{0%,80%,100%{opacity:.35;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}

/* Vaporising toggle lives in the chat menu; green when on. */
#menuDisappear.on{color:#33d17a}

/* Line-drawn room icons (match the app's sketch branding) — base rule +
   grid-tile sizing live earlier alongside the rest of the card CSS; this is
   just the inbox-row size, which is unrelated to the carousel. */
.inboxAvatar.room .roomIc{width:24px;height:24px}

/* Voice-to-text mic */
.micBtn{flex:0 0 auto;width:44px;height:44px;border-radius:50%;background:var(--card);border:1px solid var(--line);color:var(--text);display:flex;align-items:center;justify-content:center;cursor:pointer}
.micBtn svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
.micBtn.listening{background:#e0245e;border-color:#e0245e;animation:micPulse 1.2s infinite}
@keyframes micPulse{0%,100%{box-shadow:0 0 0 0 rgba(224,36,94,.55)}50%{box-shadow:0 0 0 9px rgba(224,36,94,0)}}

/* Add-user live search results */
.userResults{display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto;margin-top:4px}
.userResults .ur{display:flex;align-items:center;gap:10px;padding:11px 12px;background:var(--bg);border:1px solid var(--line);border-radius:12px;color:var(--text);font-weight:700;font-size:15px;cursor:pointer;text-align:left}
.userResults .ur:active{background:#151515}
.userResults .ur .dot{width:9px;height:9px;border-radius:50%;background:#33d17a;flex:0 0 auto}
.userResults .ur .dot.off{background:#ff4d4d}
.userResults .ur .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.userResults .empty{color:var(--muted-dim);font-size:14px;padding:10px 4px}
