* { box-sizing: border-box; }
/* height:100% is load-bearing for the standalone shell — see html.standalone
   .app below. The canvas is not painted here: on /chats it belongs to <html>
   in home.css (the top wash), and body has to stay transparent so the grain
   layer at z-index -1 shows through. The three pages that load this file
   without home.css paint it on body instead, one rule down. */
html, body { margin: 0; height: 100%; }
body {
  background: var(--page-bg);
  font-family: var(--h3-font, 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif);
}
body.chats { background: transparent; }

/* Chat tokens. The palette is the home page's: this file loads after /home.css
   on /chats and takes --h3-* as its source, keeping its own names as semantic
   aliases (fewer edits across 2600 lines, and --card-radius is pinned by name
   in minify_test.go).

   Every alias carries a literal fallback because two pages — /terms and
   /billing — load this file WITHOUT home.css. There var(--h3-bg) would resolve
   to nothing and the declaration would be dropped at computed-value time, so the
   fallback is what those pages actually get. Keep it in step with home.css when
   the palette moves; it is the one place values are duplicated, and they sit
   next to the var() so the drift is visible.

   Those two take only the LEGAL PAGE HEADER section from this file. Everything
   else here is the chat, which does load home.css and never sees a fallback. */
:root {
  /* The whole UI is dark, and saying so has one effect the palette below cannot:
     it darkens the canvas the browser paints *before* any of our CSS applies —
     the surface iOS shows during the interactive back-swipe, which was flashing
     white. The <meta name="color-scheme"> in every template does the same job
     even earlier, before this stylesheet has loaded at all. */
  color-scheme: dark;
  /* Accent: the home page's two pinks, split by contrast rather than taste.
     --accent (4.3:1) fills and borders, --accent-text (5.8:1) is the only one
     allowed to carry text. Until this release the trio arrived inline on .app
     from the server; it is CSS now, and the chat is no longer a page with its
     own palette. */
  --accent: var(--h3-primary, #de0d79);
  --accent-text: var(--h3-primary-text, #f43e9c);
  /* Derived from --accent rather than aliased to --h3-primary-soft/-weak so the
     three translucent washes move together when the accent does. Alphas are the
     ones this file already used, so every surface that reads them keeps its
     weight. --accent-glow is a COLOUR: slots.css and a dozen rules here feed it
     to box-shadow's colour slot and to backgrounds, so it must not become a
     shadow list. The home page's inset lamp is --accent-lamp, below. */
  --accent-soft: color-mix(in srgb, var(--accent) 22%, transparent);
  --accent-glow: color-mix(in srgb, var(--accent) 18%, transparent);
  --accent-weak: color-mix(in srgb, var(--accent) 10%, transparent);
  /* A flat accent fill lit from inside, the home page's .h3-btn-primary. It
     replaced a pink→violet gradient plus an outer drop shadow: on the near-black
     canvas the gradient read as a second colour rather than as one lit surface. */
  --accent-lamp: var(--h3-glow-accent, inset 0 0 16px 1px rgba(222, 13, 121, .5), inset 0 0 4px rgba(222, 13, 121, .65));
  --accent-lamp-hover: var(--h3-glow-accent-hover, inset 0 0 16px 1px rgba(222, 13, 121, .5), inset 0 0 4px rgba(222, 13, 121, .65), 0 0 16px 1px rgba(222, 13, 121, .55));
  --on-accent: var(--h3-on-accent, #fff);
  /* The "premium glow" object, worn by everything that sells: a dark two-stop
     fill under a six-stop gradient hairline. Applied as
     `background: var(--grad-fill) padding-box, var(--grad-border) border-box`
     with a transparent border — the same construction as .h3-btn-glow. */
  --grad-fill: var(--h3-glow-fill, linear-gradient(90deg, #172841, #41122c));
  --grad-border: var(--h3-glow-border, linear-gradient(195deg, rgba(255, 44, 247, 1) -10%, rgba(251, 4, 123, .8) 12%, rgba(255, 126, 188, .81) 31%, rgba(255, 255, 255, .15) 54%, rgba(73, 255, 233, .65) 70%, rgba(19, 14, 255, 1) 100%));
  --grad-shadow: var(--h3-glow-shadow, -28px 12px 50px rgba(0, 56, 255, .15), 10px -6px 30px rgba(248, 0, 59, .2));
  --grad-shadow-hover: var(--h3-glow-shadow-hover, -28px 12px 50px rgba(0, 56, 255, .25), 10px -6px 30px rgba(248, 0, 59, .3));
  /* Gold stays the chat's own. It means "coins here" — price and payout alike —
     and the home page has no such role to borrow from. Same for presence green
     and the destructive red: colour the next paid button gold, not accent, or
     it reads as a gift rather than a price. */
  --gold: #ffcf6a;
  --gold-soft: rgba(255, 199, 96, .13);
  --gold-border: rgba(255, 199, 96, .3);
  --online: #5fd29a;
  --danger: #ff5f6d;
  --danger-soft: rgba(255, 95, 109, .14);
  --danger-glow: rgba(255, 95, 109, .32);
  --text: var(--h3-text, #fff);
  --text-dim: var(--h3-text-dim, rgba(255, 255, 255, .72));
  --text-dim2: var(--h3-text-dim2, rgba(255, 255, 255, .62));
  --surface-1: var(--h3-panel, rgba(255, 255, 255, .04));
  --surface-2: var(--h3-panel-2, rgba(255, 255, 255, .08));
  --hairline: var(--h3-line, #26202e);
  --page-bg: var(--h3-bg, #070708);
  --chrome-bg: var(--h3-bg-header, #0e0e10);
  /* The canvas as a bare triple, for scrims and pills that need it at an alpha:
     rgba(var(--dark-rgb), .72). Same trick, and same value, as --h3-dark-rgb. */
  --dark-rgb: var(--h3-dark-rgb, 7, 7, 8);
  --card-bg: var(--h3-panel, rgba(255, 255, 255, .04));
  --card-border: var(--h3-line, #26202e);
  --card-radius: var(--h3-r-card, 24px);
  --pill: var(--h3-r-pill, 999px);
  /* Painted behind an assistant's photo until the image arrives, and behind the
     initial letter in the rare box that has no photo at all. Every assistant has
     media, so this is a placeholder rather than a surface anyone sees for long —
     it used to be a CSS gradient authored per assistant and stored in MongoDB,
     which is a column's worth of ceremony for a colour no visitor notices. */
  --media-bg: var(--h3-panel, rgba(255, 255, 255, .04));
  --chip-bg: var(--h3-panel, rgba(255, 255, 255, .04));
  --chip-border: var(--h3-line, #26202e);
  --chip-text: var(--h3-text-dim, rgba(255, 255, 255, .72));
}

::-webkit-scrollbar { width: 7px; height: 7px; }
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .12); border-radius: 8px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, .2); }
::-webkit-scrollbar-track { background: transparent; }

/* Акцентный скроллбар для скроллящихся колонок страницы чата: лента сообщений,
   список чатов слева, панель ассистента справа и сетка подарков в модалке.
   Переменные --accent* инжектятся инлайном на .app, поэтому доступны всем как
   потомкам. */
.messages,
.chat-list,
.assistant-panel,
.gift-scroll,
.sub-modal {
  scrollbar-width: thin;
  scrollbar-color: var(--accent-soft) transparent;
}
.messages::-webkit-scrollbar,
.chat-list::-webkit-scrollbar,
.assistant-panel::-webkit-scrollbar,
.gift-scroll::-webkit-scrollbar,
.sub-modal::-webkit-scrollbar { width: 8px; }
.messages::-webkit-scrollbar-track,
.chat-list::-webkit-scrollbar-track,
.assistant-panel::-webkit-scrollbar-track,
.gift-scroll::-webkit-scrollbar-track,
.sub-modal::-webkit-scrollbar-track { background: transparent; }
.messages::-webkit-scrollbar-thumb,
.chat-list::-webkit-scrollbar-thumb,
.assistant-panel::-webkit-scrollbar-thumb,
.gift-scroll::-webkit-scrollbar-thumb,
.sub-modal::-webkit-scrollbar-thumb {
  background: var(--accent-soft);
  border-radius: 8px;
}
.messages::-webkit-scrollbar-thumb:hover,
.chat-list::-webkit-scrollbar-thumb:hover,
.assistant-panel::-webkit-scrollbar-thumb:hover,
.gift-scroll::-webkit-scrollbar-thumb:hover,
.sub-modal::-webkit-scrollbar-thumb:hover { background: var(--accent); }

input { font-family: inherit; }
input::placeholder { color: var(--text-dim2); }

@keyframes blink {
  0%, 80%, 100% { opacity: .25; transform: translateY(0); }
  40% { opacity: 1; transform: translateY(-2px); }
}
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* ================= LAYOUT ================= */
.app {
  height: 100vh;
  height: 100dvh;
  width: 100%;
  display: flex;
  gap: 16px;
  /* Landscape on a notched phone is wider than the 720px phone breakpoint, so
     this base padding is what stands between the cards and the side cutout.
     max() rather than a sum: 16px is already breathing room, and on every
     device without a side inset (all desktops) it stays exactly 16px. */
  padding: 16px max(16px, env(safe-area-inset-right)) 16px max(16px, env(safe-area-inset-left));
  overflow: hidden;
  color: var(--text);
  /* Transparent so the wash on <html> and the grain layer beneath body show
     through the gutters between the three columns. */
  background: transparent;
  position: relative;
}

/* One focus ring for the whole page, keyed to :focus-visible so a mouse press
   stays quiet. Declared once rather than per component, the way home.css does
   it, so no future control can ship without one. Scoped to .app rather than
   body: /terms and /billing load this file too and have their own. */
.app :where(a, button, input, summary):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Installed to the home screen there is no retracting browser chrome — but on
   the first frame iOS hands the page a dynamic viewport one bottom inset short
   of the screen, so 100dvh comes out ~34px small and .app stops above the bottom
   edge. The phone tab bar is a plain flex row of .sidebar, which has no height
   of its own, so its bottom edge *is* .app's: it floated over a strip of page
   background until the first scroll made WebKit recompute (the composer did the
   same, one view over). The initial containing block is right from the first
   frame under viewport-fit=cover, and html/body already track it with
   height:100%, so take that. In a browser tab 100dvh has to stay: there the ICB
   is the *large* viewport and 100% would tuck the composer under the URL bar.
   max() rather than a bare 100%: a standalone window retracts nothing, so both
   sources are supposed to agree, and whichever one is right wins. */
html.standalone .app { height: max(100%, 100dvh); }

/* The backdrop is the home page's, and it arrives from home.css: a violet-lifted
   wash pinned to the top 1100px of <html>, with a fixed film-grain layer over it.
   Four drifting colour blobs used to live here instead, and the reason they went
   is the same one home.css gives: the three columns are opaque, so all that is
   ever visible of the backdrop are the 16px gutters between them, and a soft
   shape wide enough to read as a shape only ever appears there as an arbitrary
   slice of itself. Grain has no shape to slice. */

/* ================= LEFT PANEL ================= */
/* Opaque, like every piece of chrome on the home page. It used to be frosted
   glass — a translucent white film over a 30px backdrop blur — which was there
   to let the aurora glow through it. With the aurora gone the blur had nothing
   left to sample but flat canvas, so it cost a compositing layer per column to
   produce a colour a solid fill states outright. */
.sidebar {
  width: 312px;
  flex: none;
  display: flex;
  flex-direction: column;
  background: var(--chrome-bg);
  border: 1px solid var(--card-border);
  border-radius: var(--card-radius);
  overflow: hidden;
  position: relative;
  z-index: 1;
}
/* The row's padding lives here rather than on .brand so align-items can centre
   the phone balance pill against the wordmark: .brand's own top padding is
   larger than its bottom one, and centring inside that box would ride the pill
   high. .brand is an <a>, so the pill cannot be nested inside it. */
.sidebar-top { flex: none; display: flex; align-items: center; gap: 10px; padding: 20px 20px 14px; }
.brand { display: flex; align-items: center; gap: 11px; padding: 0; color: inherit; text-decoration: none; }
.brand-wordmark { height: 26px; width: auto; display: block; }
/* Desktop: the wordmark heads the sidebar the way it heads the menu on the home
   page, and at exactly the size it has there — 231px, which is that page's 280px
   menu column minus its two 24px paddings and the 1px border (see .h3-brand-word
   in home.css). The number is copied rather than derived because this sidebar is
   312px: spanning *its* column would draw a logo visibly bigger than the home
   page's, and the two pages have to read as one product. height:auto keeps the
   594:120 file's ratio (~47px). Below the breakpoint the phone balance pill
   shares this row, so the base 26px rule stays. */
@media (min-width: 721px) {
  .sidebar-top .brand-wordmark { width: 231px; height: auto; }
}
/* Phone-only surfaces; on desktop the sidebar keeps .balance-card and its
   .sidebar-foot rows, which have the room a 312px column affords. */
.mobile-balance { display: none; }
.chat-bottomnav { display: none; }
.brand-add {
  margin-left: auto;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim2);
}


/* The home page's sidebar rows: pills in a column, tinting to a faint accent on
   hover. They were full-bleed strips divided by hairlines, which turned the foot
   of the column into a ledger of rules; the home menu carries the same five
   destinations with no dividers at all, and the pill is what marks the row
   instead. Letter-spacing and size are that page's .h3-nav-item. */
.sidebar-foot {
  flex: none;
  /* Two of these rows are <button> (daily, games) and three are <a>. Without a
     reset the buttons keep the UA border and ButtonFace fill, which is what the
     daily row looked like: a grey plate with a white outset frame. */
  border: none;
  background: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 11px;
  margin: 0 12px 2px;
  padding: 9px 12px;
  border-radius: var(--pill);
  color: var(--text-dim);
  text-decoration: none;
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: .025em;
  transition: background-color .15s ease, color .15s ease;
}
/* The last row is flush with the column's bottom edge otherwise. */
.sidebar-foot:last-child { margin-bottom: 12px; }
.sidebar-foot svg { flex: none; }
.sidebar-foot:hover { background: var(--accent-weak); color: var(--text); }
/* The wrapper around those rows. Column, so the desktop foot keeps its shape —
   the margins that space the rows still live on the rows themselves. One thing
   does change: :last-child above now matches "log out", where before it matched
   nothing (the hidden .chat-bottomnav is the last child of .sidebar), so the
   column finally gets the 12px bottom clearance that rule always meant. Its
   reason to exist is the short-viewport query at the foot of this file. */
.sidebar-nav { flex: none; display: flex; flex-direction: column; }
.sub-badge {
  margin-left: auto;
  padding: 2px 9px;
  border-radius: var(--pill);
  background: var(--accent-soft);
  color: var(--accent-text);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
}

/* Coins balance — sidebar footer row and chat header badge. */
.coins-foot {
  flex: none;
  display: flex;
  align-items: center;
  gap: 11px;
  margin: 0 12px 2px;
  padding: 9px 12px;
  border-radius: var(--pill);
  color: var(--text-dim);
  font-size: 13.5px;
  font-weight: 500;
}
.coin-ico { flex: none; object-fit: contain; vertical-align: middle; }
.coins-foot .coin-ico { width: 22px; height: 22px; }
.coins-count { margin-left: auto; font-weight: 600; color: var(--text); }
.header-coins {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--gold-border);
  background: var(--gold-soft);
  color: var(--gold);
  font-size: 13px;
  font-weight: 600;
}
.header-coins .coin-ico { width: 18px; height: 18px; }

/* The coin balance (sidebar footer + header badge) is a button that opens the
   coins purchase modal; strip native button chrome and keep the prior look. The
   two-class selectors reaffirm size/color so source order can't clobber them. */
.coins-open { appearance: none; -webkit-appearance: none; cursor: pointer; }
.coins-foot.coins-open {
  width: calc(100% - 24px);
  border: none;
  background: none;
  color: var(--text-dim);
  font-size: 13.5px;
  font-weight: 500;
  font-family: inherit;
  text-align: left;
}
.coins-foot.coins-open:hover { background: var(--accent-weak); }
.header-coins.coins-open { font-family: inherit; }
.header-coins.coins-open:hover { background: rgba(255, 199, 96, .2); border-color: var(--gold); }

/* Subscription pill in the chat header: shows the current tier and expiry (or a
   "no subscription" prompt for free users) and opens the checkout modal on click.
   Whole pill is the button. */
.header-sub {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 11px;
  border: 1px solid var(--accent-soft);
  border-radius: var(--pill);
  background: var(--accent-soft);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.15;
  cursor: pointer;
  transition: background .15s, border-color .15s;
}
.header-sub:hover { background: var(--accent-glow); border-color: var(--accent); }
.header-sub-ico { fill: currentColor; color: var(--accent-text); flex: none; }
.header-sub-tier { white-space: nowrap; }
.header-sub-until {
  color: var(--text-dim2);
  font-weight: 500;
  white-space: nowrap;
}
/* No subscription: not a status badge but the page's call to action, wearing the
   premium glow the home page gives everything that sells — a dark two-stop fill
   under a gradient hairline. It is the only such object in the header; the coins
   pill beside it keeps gold, so the two asks stay told apart — gold is what
   conversation is spent from, the star is what unlocks photos. */
.header-sub--free {
  border-color: transparent;
  background: var(--grad-fill) padding-box, var(--grad-border) border-box;
  color: #fff;
  box-shadow: var(--grad-shadow);
  transition: filter .18s, transform .12s, box-shadow .18s;
}
.header-sub--free:hover {
  background: var(--grad-fill) padding-box, var(--grad-border) border-box;
  border-color: transparent;
  box-shadow: var(--grad-shadow-hover);
  transform: translateY(-1px);
}
/* The subscribed rule leaves the star hollow and grey; on the gradient it has to
   be a filled white star instead. */
.header-sub--free .header-sub-ico { fill: currentColor; color: #fff; }
.header-sub-cta { white-space: nowrap; }
/* Both labels ship; the viewport picks one (see the 900px block below). */
.header-sub-cta-short { display: none; }

/* Screen-reader-only text: visually removed but announced by assistive tech. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.lang-switch {
  flex: none;
  display: flex;
  gap: 4px;
  margin: 12px 18px 0;
  padding: 3px;
  background: var(--surface-1);
  border: 1px solid var(--hairline);
  border-radius: 10px;
}
.lang-opt {
  flex: 1;
  border: none;
  background: none;
  cursor: pointer;
  padding: 6px 0;
  border-radius: 8px;
  color: var(--text-dim2);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .04em;
  transition: background .2s, color .2s;
}
.lang-opt:hover { color: var(--text); }
.lang-opt.active {
  background: var(--accent-soft);
  color: var(--accent);
}
/* The same switcher is reused for the assistant's reply language in the panel,
   where it sits flush under its section title instead of in the sidebar. */
.assistant-panel .lang-switch { margin: 8px 0 0; }

.search-wrap {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0 16px 12px;
  padding: 0 13px;
  height: 42px;
  border-radius: 13px;
  background: var(--surface-1);
  border: 1px solid var(--hairline);
}
.search-wrap:focus-within { border-color: var(--accent); }
.search-input {
  flex: 1;
  border: none;
  outline: none;
  background: transparent;
  color: var(--text);
  font-size: 13.5px;
}
.search-input:focus-visible { outline: none; }

.section-label {
  padding: 2px 22px 8px;
  font-size: 11px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--text-dim2);
  font-weight: 500;
}
.section-recent {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 22px 8px;
}
.section-count {
  min-width: 18px;
  height: 18px;
  padding: 0 6px;
  border-radius: 9px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.chat-list {
  flex: 1;
  overflow-y: auto;
  padding: 0 10px 14px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* The home page's .h3-nav-item, one size up: a pill that tints on hover and, when
   it is the current one, takes an accent border, an accent wash and a short drop
   shadow of the accent. A 46px avatar with two lines beside it sits inside a pill
   the way a 24px icon with one line does — the round ends follow the avatar.
   The transparent border is what keeps the row from moving when it becomes
   active: the border is always there, only its colour changes. */
.row {
  position: relative;
  /* The glow below is z-index:-1, which without a stacking context here would
     put it behind the sidebar's own background instead of behind the row. */
  isolation: isolate;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px;
  border: 1px solid transparent;
  border-radius: var(--pill);
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  transition: background-color .15s ease, color .15s ease;
}
/* Hover is a blurred shape rather than a fill, so the row has no edge of its own
   until it is the selected one. A flat tint inside the pill drew a second hard
   outline a pixel inside the first, and at 68px tall — much bigger than the home
   page's nav rows this pill came from — that outline read as a control the
   pointer had switched on. Blurring the same shape keeps the pill's silhouette
   and takes away the boundary: what follows the cursor is a soft patch of accent,
   and the only crisp edge in the column stays the one that means "you are here".

   The fill is 40% accent, well above the 10% a flat tint used: blur spreads the
   shape over ~12px at every edge and dilutes it, so a wash mixed at the flat
   tint's strength came out almost invisible. 40% was picked against the row
   above it — at 60% the hover glowed brighter than the selected row and the two
   swapped places in the hierarchy. */
.row::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  border-radius: inherit;
  background: color-mix(in srgb, var(--accent) 40%, transparent);
  opacity: 0;
  filter: blur(12px);
  transition: opacity .18s ease;
}
.row:hover::before { opacity: 1; }
.row.active {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: 0 6px 20px -8px var(--accent);
}

.new-chat-row {
  width: 100%;
  text-align: left;
  font: inherit;
  border-color: var(--accent-soft);
  background: var(--accent-weak);
  margin-bottom: 6px;
}
.new-chat-row:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.new-chat-title { font-weight: 600; font-size: 14px; color: var(--text); }
.new-chat-sub { font-size: 12px; color: var(--text-dim2); margin-top: 2px; }
.avatar {
  position: relative;
  width: 46px;
  height: 46px;
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 15px;
  color: #fff;
  flex: none;
  background: var(--media-bg);
}
.new-chat-avatar {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--accent-lamp);
  transition: filter .18s ease, transform .18s ease, box-shadow .18s ease;
}
.new-chat-avatar svg { transition: transform .18s ease; }
.new-chat-row:hover .new-chat-avatar {
  transform: translateY(-1px);
  box-shadow: var(--accent-lamp-hover);
}
.new-chat-row:hover .new-chat-avatar svg { transform: rotate(90deg); }

.row-online {
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--online);
  border: 2.5px solid var(--chrome-bg);
  box-shadow: 0 0 6px var(--online);
}
.row-body { flex: 1; min-width: 0; }
.row-top { display: flex; align-items: baseline; gap: 8px; }
.row-name {
  font-weight: 500;
  font-size: 14px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.row-time { margin-left: auto; font-size: 11px; color: var(--text-dim2); flex: none; }
.row-bottom { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
.row-snippet {
  flex: 1;
  min-width: 0;
  font-size: 12.5px;
  color: var(--text-dim2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.row-typing {
  color: var(--accent-text);
  animation: row-typing-pulse 1.6s ease-in-out infinite;
}
@keyframes row-typing-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .5; }
}
/* While a conversation is open, drop the last-message preview from its own row
   in the chat list — it duplicates the dialog you're already reading. Other
   (inactive) chats keep their snippet. On desktop both panes are visible at
   once; on phones the conversation replaces the list entirely, so this only
   applies above that breakpoint (where the list stays beside the open dialog). */
@media (min-width: 721px) {
  .app.chat-open .row.active .row-snippet { display: none; }
}
.badge {
  min-width: 19px;
  height: 19px;
  padding: 0 6px;
  border-radius: 10px;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 11px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}

/* Balance card — sidebar footer summary of the coin balance with a quick "+"
   top-up button (the button reuses .coins-open to open the coins modal). */
.balance-card {
  flex: none;
  display: flex;
  align-items: center;
  gap: 11px;
  margin: 8px 12px 4px;
  padding: 12px 14px;
  border-radius: 16px;
  background: var(--gold-soft);
  border: 1px solid var(--gold-border);
}
.balance-ico { width: 24px; height: 24px; }
.balance-info { flex: 1; min-width: 0; line-height: 1.2; }
.balance-label {
  font-size: 11px;
  letter-spacing: .3px;
  color: var(--text-dim2);
}
.balance-value { font-size: 16px; font-weight: 600; color: var(--gold); }
.balance-value .coins-count { margin: 0; color: inherit; }
.balance-add {
  flex: none;
  width: 28px;
  height: 28px;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  cursor: pointer;
  color: var(--page-bg);
  background: var(--gold);
}
.balance-add:hover { filter: brightness(1.06); }

/* Phone stand-in for .balance-card: the same balance and the same "+" top-up, as
   a pill in the brand row instead of a card that costs the list ~76px. Shown by
   the 720px query; the base rule above keeps it out of the desktop sidebar. */
.mobile-balance {
  flex: none;
  margin-left: auto;
  align-items: center;
  gap: 6px;
  padding: 5px 6px 5px 10px;
  border-radius: 999px;
  border: 1px solid var(--gold-border);
  background: var(--gold-soft);
  color: var(--gold);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
}
.mobile-balance .coin-ico { width: 18px; height: 18px; }
.mobile-balance-plus {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  color: var(--page-bg);
  background: var(--gold);
}

/* Phone tab bar for the chat-list screen — see the comment on the markup in
   chats.gotmpl for why it lives inside .sidebar instead of being fixed. */
.chat-bottomnav {
  flex: none;
  align-items: stretch;
  border-top: 1px solid var(--hairline);
  background: var(--chrome-bg);
  /* .app is 100dvh under viewport-fit=cover and drops its padding on phones, so
     this is the only thing between the labels and the home indicator. max() and
     not a sum, same as .composer: the indicator strip is already clearance, and
     adding to it leaves a visibly empty band. Note the row this bar replaces
     (the "log out" .sidebar-foot) had no inset at all and sat under it. */
  padding-bottom: max(6px, env(safe-area-inset-bottom));
}
.chat-bottomnav > a,
.chat-bottomnav > button {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 10px 4px;
  border: none;
  background: none;
  font-family: inherit;
  /* .72rem and 500, the home page's tab bar. The old 10px/600 was a size below
     anything else in the interface and read as a caption rather than a label. */
  font-size: .72rem;
  font-weight: 500;
  line-height: 1.1;
  text-align: center;
  text-decoration: none;
  color: var(--text-dim);
  cursor: pointer;
}
.chat-bottomnav > a > svg,
.chat-bottomnav > button > svg { flex: none; }
.chat-bottomnav > .on { color: var(--accent-text); }

/* ================= MIDDLE PANEL ================= */
.main {
  flex: 1;
  display: flex;
  /* Query source for the composer: what that row has to fit into is this column,
     not the screen. Safe to contain — .main is flex:1 with a zero basis, so its
     width never came from its contents, and it was already position:relative +
     z-index, so the implied layout/style containment changes nothing. */
  container-type: inline-size;
  flex-direction: column;
  min-width: 0;
  background: var(--chrome-bg);
  border: 1px solid var(--card-border);
  border-radius: var(--card-radius);
  overflow: hidden;
  position: relative;
  z-index: 1;
}
/* The home page's header row: 64px tall, hairline underneath, nothing else. */
.chat-header {
  display: flex;
  align-items: center;
  gap: 13px;
  min-height: 64px;
  padding: 12px 24px;
  border-bottom: 1px solid var(--hairline);
}
.header-avatar {
  width: 42px;
  height: 42px;
  flex: none;
  border-radius: 13px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 15px;
  color: #fff;
  background: var(--media-bg);
}
/* The avatar + name area is a button so it can open the assistant panel as an
   overlay on mobile; on desktop the panel is always visible so it stays inert. */
.header-info {
  display: flex;
  align-items: center;
  gap: 13px;
  min-width: 0;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: default;
}
/* Back arrow returning to the chat list, shown only in the single-column mobile
   layout. */
.mobile-back {
  display: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin-left: -8px;
  border-radius: 12px;
  color: var(--text-dim);
}
.mobile-back:hover { background: var(--surface-2); color: var(--text); }
/* Closes the assistant panel overlay; only meaningful on mobile. */
.panel-close {
  display: none;
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 2;
  width: 36px;
  height: 36px;
  border: none;
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-dim);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}
.panel-close:hover { background: rgba(255, 255, 255, .12); color: var(--text); }
/* The CTA pill beside it is wider than the status badge it replaced, so the name
   is what gives: it truncates instead of shoving the header actions out of the
   bar. .header-info already carries min-width:0; this inner column needs the same
   before an ellipsis can bite. */
.header-text { min-width: 0; }
.header-name {
  font-weight: 600;
  font-size: 15.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.status { display: flex; align-items: center; gap: 6px; margin-top: 1px; min-width: 0; }
.status-dot {
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: #5fd29a;
  box-shadow: 0 0 8px #5fd29a;
}
.status-text {
  font-size: 12px;
  color: var(--text-dim2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* flex:none makes the coins pill, the CTA and the menu inviolable, so the name is
   what shrinks. Pairs with the ellipsis above — without it the name would spill
   over the pill instead of truncating. The avatar and the status dot get the same
   protection: they have fixed dimensions but were squashed into ovals under
   pressure (42×42 became 27×42 on a phone). */
.header-actions { margin-left: auto; flex: none; display: flex; gap: 6px; }

/* The home page's .h3-icon-btn: a 40px circle with a hairline, dim until it is
   hovered. The hairline is the one home uses over photos rather than --hairline,
   because these buttons sit in the header where nothing backs them. */
.icon-btn { cursor: pointer; color: var(--text-dim); }
.icon-btn:hover { background: var(--surface-1); color: var(--text); }
.action-btn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--h3-line-media, rgba(255, 255, 255, .24));
  display: flex;
  align-items: center;
  justify-content: center;
}

.menu-wrap { position: relative; }
.menu-btn.open { background: var(--surface-1); color: var(--text); }
.menu-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 20;
  min-width: 184px;
  padding: 6px;
  border-radius: 14px;
  background: var(--chrome-bg);
  border: 1px solid var(--hairline);
  box-shadow: 0 14px 34px rgba(0, 0, 0, .45);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.menu-dropdown[hidden] { display: none; }
.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 11px;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: var(--text-dim);
  font-family: inherit;
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
}
.menu-item:hover { background: var(--surface-2); color: var(--text); }
.menu-item svg { flex-shrink: 0; }
/* Same danger hue as the confirmation modal this item opens. */
.menu-delete { color: var(--danger); }
.menu-delete:hover { background: var(--danger-soft); color: var(--danger); }

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 26px 24px 10px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.empty-chat {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 14px;
  padding: 24px;
}
.empty-chat-icon {
  width: 88px;
  height: 88px;
  border-radius: var(--card-radius);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  background: var(--accent-soft);
}
.empty-chat-title {
  font-size: 19px;
  font-weight: 600;
  color: var(--text);
}
.empty-chat-text {
  max-width: 320px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-dim2);
}
.day {
  align-self: center;
  padding: 4px 14px;
  margin-bottom: 8px;
  border-radius: 999px;
  background: var(--surface-1);
  border: 1px solid var(--hairline);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-dim2);
}
.msg { display: flex; animation: fadeUp .3s ease; }
/* Stored message with nothing to draw — a turn that is only a photo offer. It
   stays in the DOM because app.js counts .msg nodes to seed its /state cursor,
   but it must not take a row (and a gap) in the thread. */
.msg-empty { display: none; }
.msg-user { justify-content: flex-end; }
.msg-bot { justify-content: flex-start; }
.bubble {
  max-width: 74%;
  padding: 12px 16px;
  border-radius: 18px;
  font-size: 14px;
  line-height: 1.5;
}
.bubble-user {
  border-bottom-right-radius: 6px;
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--accent-lamp);
}
.bubble-bot {
  border-bottom-left-radius: 6px;
  background: var(--surface-1);
  border: 1px solid var(--hairline);
  color: var(--text);
}

.typing-row { display: flex; justify-content: flex-start; }

/* Send-failure notice: the bot bubble plus its retry button, laid out like a
   .msg-bot row but deliberately not one — app.js seeds its poll cursor from the
   number of .msg nodes, and this notice is not a stored message. */
.failed-row { display: flex; justify-content: flex-start; align-items: center; gap: 8px; flex-wrap: wrap; animation: fadeUp .3s ease; }
/* The look comes from .chip, which follows this rule and so wins on size/weight;
   this only stops the <button> falling back to the UA font family. */
.retry-btn { font: inherit; }

/* Closeness level-up divider, drawn where the meter crossed a level. A break in
   the thread rather than another bubble in it: nobody said this, it happened.
   Deliberately not a .msg, like .failed-row above — app.js seeds its poll cursor
   from the number of .msg nodes, and this divider is derived from the messages
   rather than being one of them. */
.level-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 6px 0;
  animation: fadeUp .3s ease;
}
/* Rules either side of the badge, fading out towards the edges of the thread. */
.level-row::before,
.level-row::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent-soft));
}
.level-row::after { background: linear-gradient(90deg, var(--accent-soft), transparent); }
.level-badge {
  flex: none;
  /* Same ceiling as .bubble, so the unlock line wraps inside the thread's column
     instead of pushing the rules off it. */
  max-width: 74%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 6px 14px;
  border-radius: 999px;
  border: 1px solid var(--accent-soft);
  background: var(--accent-glow);
  text-align: center;
  font: inherit;
  cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
.level-badge:hover {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 28%, transparent);
}
.level-title {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .3px;
  color: var(--accent);
}
.level-heart { font-size: 12px; line-height: 1; }
/* Only on the levels that opened a new rung of the ladder, so its rarity is the
   signal that something actually changed. */
.level-unlock {
  font-size: 11px;
  line-height: 1.4;
  color: var(--text-dim2);
}

/* Prompt for a photo the assistant offered: a self-contained card, not a bubble
   followed by two buttons. Built out of .bubble and .chip it was indistinguishable
   from the quick-reply row under the thread, which hid the fact that this row is
   the one that spends coins. Deliberately not a .msg, like .failed-row above —
   nothing here is a stored message. */
.photo-offer-row { display: flex; animation: fadeUp .3s ease; }
.photo-offer-card {
  flex: 1;
  min-width: 0;
  /* Same ceiling as .bubble, so the thread keeps a single left column. */
  max-width: 74%;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 13px 15px;
  border-radius: 18px;
  /* Gold — the colour of the coin this card spends, and the one thing on the page
     the rose quick-reply chips cannot be confused with. */
  background: var(--gold-soft);
  border: 1px solid var(--gold-border);
}
.photo-offer-head { display: flex; align-items: center; gap: 9px; }
.photo-offer-ico { display: flex; flex: none; color: var(--gold); }
.photo-offer-ask { font-size: 14px; font-weight: 600; color: var(--text); }
/* The price sits at the far edge of the card, opposite the question. */
.photo-offer-head .photo-cost { margin-left: auto; flex: none; color: var(--gold); }
.photo-offer-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.photo-offer-yes, .photo-offer-no {
  font: inherit;
  padding: 10px 18px;
  border-radius: 13px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
/* The lit accent fill of .send-btn: this is the only button in the thread that
   charges the user, so it has to read as the action of the card. */
.photo-offer-yes {
  background: var(--accent);
  color: var(--on-accent);
  border: none;
  box-shadow: var(--accent-lamp);
}
.photo-offer-yes:hover { box-shadow: var(--accent-lamp-hover); }
.photo-offer-no {
  background: transparent;
  border: 1px solid var(--hairline);
  color: var(--text-dim2);
}
.photo-offer-no:hover { background: var(--surface-1); color: var(--text); }
/* The paywall variant sells a subscription, not a photo, so it drops the gold:
   gold in this thread means the coin the confirm card spends, and this card spends
   nothing. The accent it takes instead is the one the rest of the subscription
   surface already uses (.header-sub--free, .sub-modal). */
.photo-offer-card--locked {
  background: var(--accent-soft);
  border-color: var(--accent);
}
.photo-offer-card--locked .photo-offer-ico { color: var(--accent); }
/* Only the paywall card has a body line: the confirm card is a question, this one
   has to say what a subscription actually buys. */
.photo-offer-text {
  /* Pulls the line up under the heading it explains, against the card's 12px gap. */
  margin-top: -4px;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--text-dim2);
}
/* Sells a subscription, so it wears the premium glow rather than the flat accent
   the confirm button takes — the same split the header makes between
   .header-sub--free and everything else. */
.photo-offer-sub {
  font: inherit;
  padding: 10px 18px;
  border-radius: 13px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  background: var(--grad-fill) padding-box, var(--grad-border) border-box;
  color: #fff;
  border: 1px solid transparent;
  box-shadow: var(--grad-shadow);
}
.photo-offer-sub:hover { box-shadow: var(--grad-shadow-hover); }
.typing {
  display: flex;
  gap: 5px;
  align-items: center;
  padding: 14px 17px;
  border-radius: 18px;
  border-bottom-left-radius: 6px;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
}
.typing .dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .7);
  animation: blink 1.2s infinite;
}
.typing .dot:nth-child(2) { animation-delay: .2s; }
.typing .dot:nth-child(3) { animation-delay: .4s; }

.suggestions { display: flex; gap: 8px; padding: 8px 24px 4px; overflow-x: auto; }
/* The home page's .h3-chip: a 36px pill on a neutral panel, not the warm maroon
   wash it used to be. Quick replies are suggestions, not accented actions. */
.chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  height: 36px;
  padding: 0 14px;
  border-radius: var(--pill);
  font-size: 12.5px;
  font-weight: 500;
  color: var(--chip-text);
  background: var(--chip-bg);
  border: 1px solid var(--chip-border);
  cursor: pointer;
  white-space: nowrap;
}
.chip:hover { background: var(--surface-2); color: var(--text); border-color: color-mix(in srgb, var(--accent) 30%, transparent); }

.composer { padding: 12px 24px 22px; }
.input-wrap {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 8px 8px 8px 20px;
  border-radius: var(--pill);
  background: var(--surface-1);
  border: 1px solid var(--hairline);
}
.input-wrap:focus-within { border-color: var(--accent); }
.composer-input {
  flex: 1;
  /* Not cosmetic — without it the buttons get clipped off the screen. A flex
     item defaults to min-width:auto, and for an <input> that resolves to its
     intrinsic width: WebKit derives it from the size attribute (absent, so 20
     characters), measured here at 273px and, under the 16px the phone query
     sets below, 318px. The field then refuses to shrink past that, and since
     gift/photo/send are all flex:none the excess shoves them out past
     .input-wrap and off the edge, where overflow:hidden on .app/.main cuts them
     away. Was broken at every phone width, and again from 721px to ~1310px. */
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  color: var(--text);
  font-size: 14px;
  padding: 6px 0;
}
.composer-input:focus-visible { outline: none; }
.send-btn {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--on-accent);
  cursor: pointer;
  flex: none;
  box-shadow: var(--accent-lamp);
}
.send-btn:hover { box-shadow: var(--accent-lamp-hover); transform: scale(1.04); }
.photo-btn {
  font: inherit;
  min-width: 40px;
  height: 40px;
  padding: 0 14px;
  gap: 7px;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--gold-soft);
  border: 1px solid var(--gold-border);
  color: var(--gold);
  cursor: pointer;
  flex: none;
  transition: border-color .18s, box-shadow .18s, transform .12s;
}
.photo-btn-label { font-size: 13.5px; font-weight: 600; }
.photo-btn:hover {
  border-color: var(--gold);
  box-shadow: 0 6px 18px -8px var(--gold);
  transform: translateY(-1px);
}
.photo-cost {
  display: flex;
  align-items: center;
  gap: 3px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
}

/* Narrow chat column. Above the phone breakpoint the screen stops being a proxy
   for the room this row actually has: the sidebar, and past 1080px the assistant
   panel too, sit in the same flex row, so .main is only ~361px at a 721px
   viewport and ~389px again at 1081px. Left alone the field is squeezed to a
   sliver there while the screen is anything but narrow. Same compaction as the
   phone query below, which keeps its own copy because it also covers 561–720px,
   where the column is wide enough for this rule but the design still wants the
   compact row. */
@container (max-width: 560px) {
  .input-wrap { gap: 8px; padding: 6px 6px 6px 14px; }
  .gift-btn { padding: 0; width: 40px; border-color: transparent; }
  .gift-btn-label { display: none; }
  .photo-btn { padding: 0 10px; gap: 5px; }
  .photo-btn-label { display: none; }
}

/* ================= RIGHT PANEL ================= */
.assistant-panel {
  width: 316px;
  flex: none;
  display: flex;
  flex-direction: column;
  background: var(--chrome-bg);
  border: 1px solid var(--card-border);
  border-radius: var(--card-radius);
  overflow-y: auto;
  position: relative;
  z-index: 1;
}
.photo { padding: 20px 20px 6px; display: flex; flex-direction: column; align-items: flex-start; }
.photo-wrap { position: relative; width: 100%; }
.big-avatar {
  width: 96px;
  height: 96px;
  border-radius: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 34px;
  color: #fff;
  background: var(--media-bg);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .3);
}
.panel-media {
  position: relative;
  width: 100%;
  aspect-ratio: 3/4;
  border-radius: var(--card-radius);
  overflow: hidden;
  background: var(--media-bg);
  box-shadow: 0 14px 34px rgba(0, 0, 0, .38);
}
.panel-media img, .panel-media video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.panel-media video { opacity: 0; transition: opacity .25s ease; }
.panel-media:hover video { opacity: 1; }
/* Presence pill overlaid on the top-right of the photo card. */
.panel-online {
  position: absolute;
  top: 12px;
  right: 12px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  border-radius: 999px;
  background: rgba(var(--dark-rgb), .55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--online);
}
.panel-online-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--online);
  box-shadow: 0 0 8px var(--online);
}
.assistant-name { font-weight: 600; font-size: 22px; margin-top: 16px; }
.panel-tagline {
  margin-top: 4px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-dim2);
}

/* Closeness meter — level/percent derived from chat activity; fill width is set inline. */
.intimacy { padding: 6px 20px 18px; }
/* The meter opens its explainer, so it reads as a control. */
.intimacy-open { cursor: pointer; }
.intimacy-open:hover .intimacy-title,
.intimacy-open:focus-visible .intimacy-title { color: var(--text); }
.intimacy-open:hover .intimacy-info { border-color: var(--accent); color: var(--accent); }
.intimacy-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  margin-left: 6px;
  border: 1px solid rgba(255, 255, 255, .35);
  border-radius: 50%;
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
  vertical-align: text-top;
  color: var(--text-dim2);
}
.intimacy-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 9px;
}
.intimacy-title { font-size: 13px; font-weight: 500; color: var(--text-dim); }
.intimacy-value { font-size: 12px; font-weight: 600; color: var(--accent); }
.intimacy-bar {
  height: 7px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}
.intimacy-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  box-shadow: 0 0 12px var(--accent-soft);
}

/* Persona ("about me") rows under the closeness meter. */
.persona { padding: 6px 20px 18px; }
.persona-title {
  font-size: 11px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--text-dim2);
  font-weight: 500;
  margin-bottom: 12px;
}
.persona-list { display: flex; flex-direction: column; gap: 8px; }
.persona-row { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; }
.persona-row dt { color: var(--text-dim2); }
.persona-row dd { color: var(--text-dim); text-align: right; }

/* Gift CTA pinned to the bottom of the assistant panel. */
.panel-gift-wrap { margin-top: auto; padding: 8px 20px 24px; }
.panel-gift {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 14px;
  border: none;
  border-radius: 15px;
  cursor: pointer;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--on-accent);
  background: var(--accent);
  box-shadow: var(--accent-lamp);
  transition: filter .18s, transform .12s, box-shadow .18s;
}
.panel-gift:hover {
  transform: translateY(-1px);
  box-shadow: var(--accent-lamp-hover);
}

/* ================= RESPONSIVE ================= */

/* Tablet and below: the conversation needs the room, so the assistant panel
   leaves the flex layout and becomes a slide-over opened from the header. */
@media (max-width: 1080px) {
  .header-info { cursor: pointer; }

  .assistant-panel {
    position: fixed;
    inset: 0;
    z-index: 40;
    width: 100%;
    border: none;
    border-radius: 0;
    background: var(--chrome-bg);
    transform: translateX(100%);
    transition: transform .25s ease;
    will-change: transform;
  }
  .app.panel-open .assistant-panel { transform: none; }
  /* The panel is inset:0 on a viewport-fit=cover page, so it really does start
     at the physical top edge. Its hero photo is meant to run full-bleed under
     the dynamic island — only the close button has to come out from behind it.
     Absolute offsets resolve against the padding box, so padding on the panel
     would not have moved this button; it needs the inset itself. */
  .panel-close { display: flex; align-items: center; justify-content: center; top: calc(14px + env(safe-area-inset-top)); }
  .panel-gift-wrap { padding-bottom: calc(24px + env(safe-area-inset-bottom)); }
}

/* Narrow desktop and tablet: the header runs out of room well before the phone
   layout kicks in, so the CTA drops its verb and keeps the product name. The
   assistant's name is not the thing that should be getting squeezed. 900 and not
   1080, because at 1080 the assistant panel *leaves* the flex row and the chat
   column gets wider, not narrower. */
@media (max-width: 900px) {
  .header-sub-cta-full { display: none; }
  .header-sub-cta-short { display: inline; }
}

/* Phone: a single column that shows either the chat list or one conversation.
   /chats shows the list; /chats?chat=ID (the .chat-view modifier) shows the
   conversation full screen with a back arrow to the list. */
@media (max-width: 720px) {
  /* Phones go full-bleed: drop the floating-card padding/gaps and rounding. */
  .app { padding: 0; gap: 0; }
  .sidebar, .main {
    border: none;
    border-radius: 0;
  }
  .sidebar {
    width: 100%;
    border-right: none;
  }

  /* Default (list) view: only the sidebar is visible. */
  .main { display: none; }

  /* Conversation view. */
  .app.chat-view .sidebar { display: none; }
  .app.chat-view .main { display: flex; }

  .mobile-back { display: flex; }

  /* Phones are full-bleed on a viewport-fit=cover page, so this padding is the
     only thing between the bar and the physical top edge — on an iPhone the
     dynamic island sat right on top of the avatar and the name. The inset is
     added to the padding rather than max()'d with it: what is wanted above the
     bar is the usual gap *below* the island, not a row flush against it. */
  .chat-header { padding: calc(12px + env(safe-area-inset-top)) 14px 12px; gap: 10px; }
  /* Phone headers cannot hold back arrow + avatar + name + coins + a worded pill
     + menu at once; something has to give, and it must not be the assistant's
     name (the only place it appears in this layout) or the avatar. So both
     states of the pill drop to their star — the subscribed one loses the expiry
     and the tier, the free one its CTA — and each title carries the wording.
     Same padding for both, so subscribing does not reflow the bar. Note this
     also stops the phone header from naming the tier: Premium and VIP look
     alike here, and only the title tells them apart. */
  .header-sub-until,
  .header-sub-tier,
  .header-sub--free .header-sub-cta { display: none; }
  .header-sub { padding: 6px 10px; }
  /* Trim the header on phones: hide the "replies instantly" suffix so the name +
     status stay on one line. */
  .status-instant { display: none; }
  .messages { padding: 18px 14px 8px; }
  .bubble { max-width: 86%; }
  .photo-offer-card { max-width: 86%; }
  .suggestions { padding: 8px 14px 4px; }
  /* The page is 100dvh with viewport-fit=cover, so this padding is all that
     stands between the buttons and the bottom edge — on a home-indicator iPhone
     they would sit under the indicator without the inset. max() and not a sum:
     the indicator strip is already clearance, and adding 16px on top of it left
     a visibly empty band under the composer. Where there is no inset the 14px
     still applies. */
  .composer { padding: 10px 14px max(14px, env(safe-area-inset-bottom)); }
  /* 16px keeps iOS Safari from zooming the page when the input gains focus. */
  .composer-input { font-size: 16px; }
  /* Three buttons plus a text field on a 375px screen: hand the row's own
     padding and gaps back to the field, which is what actually runs out.
     Paired with the gift collapse below; see the @container rule above. */
  .input-wrap { gap: 8px; padding: 6px 6px 6px 14px; }
  /* Collapse the gift button back to an icon so the composer row stays compact. */
  .gift-btn { padding: 0; width: 40px; border-color: transparent; }
  .gift-btn-label { display: none; }
  .photo-btn { padding: 0 10px; gap: 5px; }
  .photo-btn-label { display: none; }

  /* Same top edge as .chat-header, one view over: on phones the sidebar *is*
     the list screen, so without the inset the island would cover the wordmark
     exactly as it covered the conversation header. The inset sits on the row
     rather than on .brand because the balance pill shares that row. */
  .sidebar-top { padding: calc(18px + env(safe-area-inset-top)) 16px 12px; }
  .chat-list { padding: 0 8px 12px; }
  .lang-switch { margin: 10px 14px 0; }

  /* The list screen's bottom: one tab bar instead of the balance card and four
     stacked rows, which together ran to ~260px of a 667px screen. Their jobs are
     taken by the balance pill in the brand row, the tabs themselves, and
     /profile (which now carries "install app" and "log out"). */
  .mobile-balance { display: inline-flex; }
  .chat-bottomnav { display: flex; }
  .sidebar .balance-card,
  .sidebar .sidebar-nav { display: none; }
}

/* ================= LEGAL PAGE HEADER ================= */
.gallery-header {
  position: sticky; top: 0; z-index: 60;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  /* Pinned to top:0 on a viewport-fit=cover page, so the bar owns the strip the
     dynamic island sits in and has to pad itself out from under it. */
  padding: calc(14px + env(safe-area-inset-top)) 20px 14px;
  background: var(--chrome-bg);
  border-bottom: 1px solid var(--hairline);
}
.gallery-brand {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--text); text-decoration: none;
  font-weight: 700; font-size: 1.05rem; letter-spacing: .02em;
  padding: 6px 10px 6px 6px; border-radius: 10px;
  transition: background .2s, color .2s;
}
.gallery-brand:hover { background: var(--surface-2); color: var(--accent); }
.gallery-brand svg { width: 18px; height: 18px; }
.gallery-header-actions { display: flex; align-items: center; gap: 10px; }
.gallery-nav-btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 9px 18px; border-radius: 11px; text-decoration: none;
  font-size: .9rem; font-weight: 600; letter-spacing: .01em;
  color: var(--on-accent); background: var(--accent);
  box-shadow: var(--accent-lamp);
  transition: transform .15s ease, box-shadow .25s ease;
}
.gallery-nav-btn:hover { transform: translateY(-1px); box-shadow: var(--accent-lamp-hover); }
.gallery-nav-btn:active { transform: translateY(0); }

.avatar-img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; display: block; }

.bubble-photo {
  padding: 4px;
}

.bubble-photo img {
  display: block;
  width: 100%;
  max-width: 240px;
  border-radius: 12px;
  cursor: zoom-in;
}

/* Small muted caption under a delivered photo showing coins charged for it. */
.photo-charge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 4px;
  padding-left: 2px;
  font-size: 11.5px;
  color: var(--text-dim2);
}
.photo-charge .coin-ico { width: 12px; height: 12px; }

/* Notice shown in place of a photo the user could not afford. */
.bubble-photo-locked {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  font-style: italic;
  opacity: 0.9;
}

.bubble-photo-locked .coin-ico { width: 16px; height: 16px; }

/* Full-screen overlay showing a chat photo enlarged. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  /* inset:0 on a viewport-fit=cover page reaches the physical edges, so the
     photo needs padding out of the island and the home indicator. */
  padding: calc(24px + env(safe-area-inset-top)) 24px calc(24px + env(safe-area-inset-bottom));
  background: rgba(0, 0, 0, .8);
  backdrop-filter: blur(6px);
  cursor: zoom-out;
}

.lightbox[hidden] { display: none; }

.lightbox-img {
  max-width: 100%;
  /* Percentages, not viewport units: they resolve against the padded content
     box, so the safe-area padding above actually holds the photo back instead
     of being measured over by a 92vh that knows nothing about it. */
  max-height: 100%;
  border-radius: 12px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, .5);
}

/* Subscription prompt shown when a free-tier user hits the message or photo limit. */
.sub-modal {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  /* Deliberately not align-items:center. A dialog taller than the screen gets
     centred into overflow at BOTH ends, and the half above the top edge is
     unreachable — nothing here is a scroll container, and the page behind is
     overflow:hidden. The subscription modal did exactly that on every phone:
     965px of dialog in a 667px viewport put its close button 134px above the
     top edge and cut the terms link off the bottom. Start-alignment plus auto
     margins on the box centres it just the same whenever it fits, and scrolls
     instead of clipping when it does not. */
  align-items: flex-start;
  justify-content: center;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* viewport-fit=cover, and this overlay now really does reach the edges. */
  padding: calc(20px + env(safe-area-inset-top)) 20px calc(20px + env(safe-area-inset-bottom));
  /* Dim the page only slightly — the modal stays the focus without hiding the
     conversation behind it. */
  background: rgba(var(--dark-rgb), .72);
  backdrop-filter: blur(2px);
  animation: sub-modal-fade .18s ease-out;
}

@keyframes sub-modal-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

.sub-modal[hidden] { display: none; }

.sub-modal-box {
  position: relative;
  /* Centres the box while it fits and collapses to zero once it does not — the
     half of the fix that lives on the item; see .sub-modal above. */
  margin: auto;
  width: 100%;
  max-width: 420px;
  background: var(--chrome-bg);
  border: 1px solid var(--hairline);
  border-radius: var(--card-radius);
  padding: 32px 26px 24px;
  text-align: center;
  box-shadow: 0 30px 80px rgba(0, 0, 0, .5);
  animation: sub-modal-pop .22s cubic-bezier(.2, .8, .3, 1);
}

@keyframes sub-modal-pop {
  from { opacity: 0; transform: translateY(12px) scale(.97); }
  to { opacity: 1; transform: none; }
}

.sub-modal-x {
  position: absolute;
  top: 14px;
  right: 15px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--text-dim2);
  border-radius: 50%;
  cursor: pointer;
  transition: background .2s, color .2s;
}

.sub-modal-x svg { width: 15px; height: 15px; }
.sub-modal-x:hover { background: var(--surface-2); color: var(--text); }

.sub-modal-icon {
  width: 60px;
  height: 60px;
  margin: 2px auto 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 18px;
  background: var(--accent-soft);
  color: var(--accent);
  box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 06%, transparent);
}

.sub-modal-title {
  font-size: 1.35rem;
  font-weight: 600;
  margin: 0 0 8px;
}

.sub-modal-text {
  color: var(--text-dim);
  font-size: .92rem;
  line-height: 1.5;
  margin: 0 auto 22px;
  max-width: 320px;
}

/* Closeness explainer: the ladder of tiers, then how the meter is filled. Rows
   are read-only, so unlike .plan-row they carry no hover or selected state —
   only the tier the chat is on is picked out. */
.intimacy-tiers {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 20px;
  padding: 0;
  list-style: none;
  text-align: left;
}

.intimacy-tier {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 14px;
  border: 1px solid var(--hairline);
  border-radius: 14px;
  background: var(--surface-1);
}

.intimacy-tier.is-current {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.intimacy-tier-range {
  flex: none;
  min-width: 96px;
  font-size: .78rem;
  font-weight: 600;
  color: var(--text-dim2);
}

.intimacy-tier.is-current .intimacy-tier-range { color: var(--accent); }

.intimacy-tier-label {
  flex: 1;
  font-size: .9rem;
}

/* The "you are here" chip only exists on the current tier. */
.intimacy-tier-now { display: none; }

.intimacy-tier.is-current .intimacy-tier-now {
  display: inline-block;
  flex: none;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: .68rem;
  font-weight: 600;
  text-transform: lowercase;
}

/* Countdown to the next level, between the ladder and the earning rules. */
.intimacy-next {
  margin: 0 0 20px;
  color: var(--accent);
  font-size: .88rem;
  font-weight: 600;
}

.intimacy-earn { text-align: left; }

.intimacy-earn-title {
  font-size: .82rem;
  font-weight: 600;
  color: var(--text-dim);
  margin-bottom: 8px;
}

.intimacy-earn-list {
  margin: 0 0 12px;
  padding-left: 18px;
  color: var(--text-dim);
  font-size: .86rem;
  line-height: 1.7;
}

.intimacy-earn-note {
  margin: 0;
  color: var(--text-dim2);
  font-size: .78rem;
}

/* Duration plans: a vertical radio group of three rows. */
.sub-modal-plans {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 20px;
  text-align: left;
}

.plan-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 14px 16px;
  border: 1px solid var(--hairline);
  border-radius: 16px;
  background: var(--surface-1);
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
  transition: border-color .18s, background .18s, box-shadow .18s;
}

.plan-row:hover { border-color: color-mix(in srgb, var(--accent) 40%, transparent); }

.plan-row.is-selected {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  box-shadow: 0 8px 26px color-mix(in srgb, var(--accent) 16%, transparent);
}

.plan-radio {
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .3);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color .18s;
}

.plan-row.is-selected .plan-radio { border-color: var(--accent); }

.plan-dot {
  width: 0;
  height: 0;
  border-radius: 50%;
  background: var(--accent);
  transition: width .18s, height .18s;
}

.plan-row.is-selected .plan-dot {
  width: 11px;
  height: 11px;
  box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 70%, transparent);
}

.plan-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

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

.plan-name {
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
}

.plan-badge {
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: .62rem;
  font-weight: 800;
  letter-spacing: .04em;
  white-space: nowrap;
}

.plan-badge--best {
  background: var(--accent);
  color: var(--on-accent);
}

.plan-permonth {
  font-size: .8rem;
  color: var(--text-dim2);
}

.plan-pricing {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

.plan-amount {
  font-size: 1.2rem;
  font-weight: 800;
  color: var(--text);
}

.plan-old {
  font-size: .8rem;
  color: var(--text-dim2);
  text-decoration: line-through;
}

/* Repeated on every plan card so a longer period does not read as more charges. */
.plan-once {
  font-size: .72rem;
  color: var(--text-dim2);
}

/* Payment method selector. */
.pay-methods-wrap { margin-bottom: 20px; text-align: left; }

.pay-methods-label {
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-dim2);
  margin-bottom: 10px;
}

.pay-methods {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

/* Horizontal padding is tighter than vertical, and the row may wrap: a disabled
   tile carries an icon, a label and the "soon" badge inside one grid column. */
.pay-method {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 13px 10px;
  border: 1px solid var(--hairline);
  border-radius: 14px;
  background: var(--surface-1);
  color: var(--text-dim);
  font-family: inherit;
  font-size: .88rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color .18s, background .18s, color .18s;
}

.pay-method.is-active {
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--text);
}

/* :hover matches disabled buttons too, so an unconfigured method would light up
   under the cursor without the :not(:disabled) guard. */
.pay-method:hover:not(.is-active):not(:disabled) {
  border-color: rgba(255, 255, 255, .16);
  color: var(--text);
}

.pay-method:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--accent) 80%, transparent);
  outline-offset: 2px;
}

/* Method whose provider is not configured: present, readable, not choosable. */
.pay-method:disabled {
  border-color: var(--hairline);
  background: var(--surface-1);
  color: var(--text-dim2);
  cursor: not-allowed;
}

.pay-method-soon {
  padding: 2px 6px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-dim2);
  font-size: .58rem;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* One-time payment notice, the last thing read before the Pay CTA. */
.sub-modal-once {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 14px;
  padding: 12px 14px;
  border: 1px solid var(--hairline);
  border-radius: 14px;
  background: var(--surface-1);
  text-align: left;
}

.sub-modal-once svg {
  flex: none;
  margin-top: 1px;
  color: var(--accent);
}

.sub-modal-once-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sub-modal-once-title {
  font-size: .84rem;
  font-weight: 700;
  color: var(--text);
}

.sub-modal-once-text {
  font-size: .76rem;
  line-height: 1.45;
  color: var(--text-dim2);
}

.sub-modal-pay {
  width: 100%;
  padding: 15px;
  border: none;
  border-radius: var(--pill);
  background: var(--accent);
  color: var(--on-accent);
  font-family: inherit;
  font-size: 1rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: var(--accent-lamp);
  transition: box-shadow .18s;
}

.sub-modal-pay:hover { box-shadow: var(--accent-lamp-hover); }

.sub-modal-note {
  margin: 14px 0 0;
  font-size: .72rem;
  color: var(--text-dim2);
}

.sub-modal-note a { color: var(--accent); }

/* Stacked modal actions: the secondary, ghost-styled choice sits below the
   primary CTA. Shared by the push prompt and the clear/delete confirmation. */
.modal-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* The home page's .h3-btn-ghost: a hairline pill that fills with the panel tint
   rather than brightening its border. */
.modal-btn-ghost {
  width: 100%;
  padding: 13px;
  border: 1px solid var(--hairline);
  border-radius: var(--pill);
  background: transparent;
  color: var(--text-dim);
  font-family: inherit;
  font-size: .92rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color .18s, color .18s;
}

.modal-btn-ghost:hover { background: var(--surface-1); color: var(--text); }

/* Confirmation modal for clearing or deleting a chat. Reuses the .sub-modal
   shell (backdrop, pop-in, close cross) and replaces the icon tile with the
   assistant's avatar plus a danger badge, so it is obvious whose chat is at
   stake. */
.confirm-box { max-width: 380px; }

.confirm-avatar {
  position: relative;
  width: 76px;
  height: 76px;
  margin: 2px auto 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--card-radius);
  border: 1px solid var(--hairline);
  background: var(--accent);
  color: var(--on-accent);
  font-size: 1.8rem;
  font-weight: 600;
  box-shadow: 0 14px 34px -12px rgba(0, 0, 0, .7);
}

.confirm-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

.confirm-avatar-badge {
  position: absolute;
  right: -6px;
  bottom: -6px;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--danger);
  color: #fff;
  /* Matches the top of the .sub-modal-box gradient, so the badge reads as
     punched out of the avatar. */
  border: 3px solid var(--chrome-bg);
}

/* One badge per action: the modal carries data-kind, so only the matching icon
   stays visible. The trash is the default, which is also what shows in the
   (unreachable) state before app.js has set a kind. */
.confirm-modal .confirm-ico-clear { display: none; }
.confirm-modal[data-kind="clear"] .confirm-ico-clear { display: block; }
.confirm-modal[data-kind="clear"] .confirm-ico-delete { display: none; }

.confirm-go {
  width: 100%;
  padding: 15px;
  border: none;
  border-radius: var(--pill);
  background: var(--danger);
  color: var(--page-bg);
  font-family: inherit;
  font-size: 1rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 14px 34px var(--danger-glow);
  transition: filter .18s;
}

.confirm-go:hover { filter: brightness(1.08); }
.confirm-go:disabled { opacity: .55; cursor: default; box-shadow: none; filter: none; }
.modal-btn-ghost:disabled { opacity: .55; cursor: default; }

.confirm-msg {
  margin: 12px 0 0;
  font-size: .82rem;
  color: var(--danger);
}

.confirm-msg:empty { display: none; }

body.slots-open { overflow: hidden; }
body.gift-modal-open { overflow: hidden; }

/* ===== Gifts ===== */
/* Composer gift button: a soft accent pill with icon + "Gift" label. It sits next
   to the gradient send button, so it stays quiet and only warms up on hover. */
.gift-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  height: 40px;
  padding: 0 14px;
  border: 1px solid var(--accent-soft);
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  cursor: pointer;
  flex: 0 0 auto;
  transition: border-color .18s, box-shadow .18s, transform .12s;
}
.gift-btn-label { font-size: 13.5px; font-weight: 600; }
.gift-btn:hover {
  border-color: var(--accent);
  box-shadow: 0 6px 18px -8px var(--accent-glow);
  transform: translateY(-1px);
}
.gift-btn:focus-visible,
.panel-gift:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.photo-btn:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

.gift-launch { cursor: pointer; border: none; background: none; text-align: left; }

.gift-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.gift-modal[hidden] { display: none; }
.gift-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(var(--dark-rgb), .78);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  animation: gift-fade .2s ease;
}
/* The card speaks the same language as the slots modal: a radial accent wash at
   the top, an accent hairline and a deep glow. Head and wallet stay put, only the
   grid scrolls, so the send button is always reachable. */
.gift-card {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(480px, 100%);
  max-height: min(90vh, 760px);
  color: var(--text);
  background: radial-gradient(120% 80% at 50% -12%, color-mix(in srgb, var(--accent) 20%, var(--chrome-bg)) 0%, var(--chrome-bg) 52%, var(--page-bg) 100%);
  border: 1px solid var(--accent-soft);
  border-radius: var(--card-radius);
  box-shadow: 0 30px 80px -30px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, .06);
  animation: sub-modal-pop .22s cubic-bezier(.2, .8, .3, 1);
}
.gift-close {
  position: absolute;
  top: 14px;
  right: 15px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-dim);
  cursor: pointer;
  transition: background .2s, color .2s;
}
.gift-close:hover { background: rgba(255, 255, 255, .13); color: var(--text); }
.gift-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.gift-head { flex: none; padding: 26px 22px 0; text-align: center; }
.gift-brand { font-size: 22px; font-weight: 600; color: #fff; }
.gift-sub { margin-top: 6px; font-size: 13px; line-height: 1.45; color: var(--text-dim2); }

/* Wallet: the same gold pill as the header balance, so coins read the same
   everywhere. */
.gift-wallet {
  flex: none;
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 18px 22px 0;
  padding: 8px 8px 8px 14px;
  border: 1px solid var(--gold-border);
  border-radius: 999px;
  background: var(--gold-soft);
}
.gift-wallet .coin-ico { width: 22px; height: 22px; }
.gift-wallet-num { font-size: 18px; font-weight: 700; color: var(--gold); }
.gift-wallet-cap { font-size: 12px; color: var(--text-dim2); }
.gift-buy-more {
  margin-left: auto;
  flex: none;
  padding: 9px 15px;
  border: none;
  border-radius: 999px;
  background: var(--gold);
  color: var(--page-bg);
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  transition: filter .18s;
}
.gift-buy-more:hover { filter: brightness(1.06); }
.gift-buy-more:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
/* Nudged when the user taps a gift they cannot afford. */
.gift-buy-more.is-hinted { animation: gift-nudge .45s ease 2; }

.gift-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 22px 4px; }
.gift-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: 12px;
}
/* Tile: the gift icon floats on a soft accent podium. The icons are transparent
   PNG-style webp, so the glow behind them does the framing. */
.gift-item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 16px 8px 13px;
  border: 1px solid var(--hairline);
  border-radius: 20px;
  background: var(--surface-1);
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  transition: border-color .18s, background .18s, box-shadow .18s, transform .12s;
}
.gift-item::before {
  content: "";
  position: absolute;
  top: 12px;
  left: 50%;
  width: 110px;
  height: 110px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent-soft), transparent 68%);
  opacity: .75;
  pointer-events: none;
  transition: opacity .18s, transform .18s;
}
.gift-item-icon {
  position: relative;
  width: 96px;
  height: 96px;
  object-fit: contain;
  filter: drop-shadow(0 10px 16px rgba(0, 0, 0, .45));
  transition: transform .18s;
}
.gift-item-name { font-size: 13px; text-align: center; color: var(--text-dim); }
.gift-item-price {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border: 1px solid var(--gold-border);
  border-radius: 999px;
  background: var(--gold-soft);
  color: var(--gold);
  font-size: 12px;
  font-weight: 700;
}
.gift-item:hover { border-color: color-mix(in srgb, var(--accent) 40%, transparent); transform: translateY(-3px); }
.gift-item:hover::before { opacity: 1; transform: translateX(-50%) scale(1.08); }
.gift-item:hover .gift-item-icon { transform: translateY(-3px) scale(1.04); }
.gift-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gift-item.is-selected {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  box-shadow: 0 8px 26px color-mix(in srgb, var(--accent) 16%, transparent);
}
.gift-item.is-selected::before { opacity: 1; }
.gift-item.is-locked { opacity: .45; cursor: not-allowed; }
.gift-item.is-locked:hover { border-color: var(--hairline); transform: none; }
.gift-item.is-locked:hover::before { opacity: .75; transform: translateX(-50%); }
.gift-item.is-locked:hover .gift-item-icon { transform: none; }

/* Footer: the confirm step. The button carries the choice, so a stray tap on a
   tile never spends coins. */
.gift-foot {
  flex: none;
  padding: 14px 22px 20px;
  border-top: 1px solid var(--hairline);
  background: linear-gradient(180deg, rgba(var(--dark-rgb), 0), rgba(var(--dark-rgb), .55));
}
.gift-send {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 15px;
  border: none;
  border-radius: 16px;
  background: var(--accent);
  box-shadow: var(--accent-lamp);
  color: var(--on-accent);
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  transition: box-shadow .18s;
}
.gift-send:hover:not(:disabled) { box-shadow: var(--accent-lamp-hover); }
.gift-send:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gift-send:disabled {
  background: var(--surface-1);
  box-shadow: none;
  color: var(--text-dim2);
  cursor: default;
}
.gift-send .coin-ico { width: 16px; height: 16px; }
.gift-remain {
  min-height: 16px;
  margin-top: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  font-size: 12px;
  color: var(--text-dim2);
}
.gift-remain .coin-ico { width: 13px; height: 13px; }
.gift-msg { min-height: 18px; margin-top: 4px; text-align: center; font-size: 13px; color: var(--accent-text); }

@keyframes gift-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes gift-nudge {
  0%, 100% { transform: translateX(0); }
  30% { transform: translateX(-3px); }
  70% { transform: translateX(3px); }
}
@keyframes gift-card-pop {
  0% { opacity: 0; transform: scale(.7); }
  60% { transform: scale(1.06); }
  100% { opacity: 1; transform: scale(1); }
}

/* The gift in the transcript is a keepsake card, not the usual gradient bubble:
   the icon keeps its podium and the name sits under it as a caption. Declared
   after .bubble-user so it wins the background at equal specificity. */
.bubble-gift {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 18px 22px 14px;
  border-radius: var(--card-radius);
  border-bottom-right-radius: 6px;
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  box-shadow: 0 14px 34px -16px color-mix(in srgb, var(--accent) 55%, transparent);
  animation: gift-card-pop .32s cubic-bezier(.2, .9, .3, 1.2);
}
.bubble-gift::before {
  content: "";
  position: absolute;
  top: 12px;
  left: 50%;
  width: 150px;
  height: 150px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, .16), transparent 66%);
  pointer-events: none;
}
.bubble-gift .gift-icon {
  position: relative;
  width: 128px;
  height: 128px;
  object-fit: contain;
  filter: drop-shadow(0 12px 20px rgba(0, 0, 0, .45));
}
.bubble-gift .gift-name {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-dim);
}

@media (prefers-reduced-motion: reduce) {
  .gift-card,
  .gift-backdrop,
  .bubble-gift,
  .row-typing,
  .gift-buy-more.is-hinted { animation: none; }
  .gift-item,
  .gift-item::before,
  .gift-item-icon,
  .gift-btn,
  .photo-btn { transition: none; }
  .new-chat-avatar,
  .new-chat-avatar svg { transition: none; }
  .new-chat-row:hover .new-chat-avatar { transform: none; }
  .new-chat-row:hover .new-chat-avatar svg { transform: none; }
}

/* Phones: the picker becomes a bottom sheet, like the slots modal. */
@media (max-width: 640px) {
  .gift-modal { padding: 0; align-items: flex-end; }
  .gift-card {
    width: 100%;
    /* A bottom sheet grows upwards, so its ceiling is what can end up under the
       dynamic island; 92vh left no room for it on a tall catalogue. */
    max-height: calc(100dvh - env(safe-area-inset-top) - 12px);
    border-radius: 24px 24px 0 0;
  }
  .gift-grid { grid-template-columns: repeat(2, 1fr); }
  .gift-foot { padding-bottom: calc(20px + env(safe-area-inset-bottom)); }
}

/* ===== Daily reward ===== */
.daily-box {
  max-width: 400px;
  padding: 28px 26px 22px;
}

.daily-box .sub-modal-title { font-size: 1.3rem; }
.daily-box .sub-modal-text { margin-bottom: 20px; }

.daily-icon {
  width: 56px;
  height: 56px;
  margin: 2px auto 14px;
  border-radius: 17px;
  border: 1px solid var(--gold-border);
  background: rgba(255, 199, 96, .16);
  color: var(--gold);
  box-shadow: 0 0 0 6px rgba(255, 199, 96, .05);
}

.daily-strip {
  display: flex;
  gap: 4px;
  margin: 0 0 10px;
  padding: 0;
  list-style: none;
}

.daily-day {
  flex: 1;
  min-width: 0;
  height: 6px;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: var(--surface-2);
}

.daily-day > span { display: none; }

.daily-day.is-done { background: rgba(255, 199, 96, .45); }

.daily-day.is-tomorrow {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px rgba(255, 199, 96, .45);
}

.daily-day.is-today {
  background: var(--gold);
  box-shadow: 0 0 10px rgba(255, 199, 96, .4);
}

.daily-streak {
  margin: 0 0 4px;
  font-size: .82rem;
  color: var(--gold);
}

.daily-broken {
  margin: 0 0 4px;
  font-size: .82rem;
  color: var(--text-dim);
}

.daily-next {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  margin: 14px 0 0;
  font-size: .95rem;
  font-weight: 600;
  color: var(--gold);
}

.daily-next .coin-ico { flex: none; }

.daily-claim {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 18px;
  padding: 15px 20px;
  border: none;
  border-radius: 16px;
  background: linear-gradient(135deg, #ffd77e, #f0a92e);
  color: var(--page-bg);
  font-family: inherit;
  font-size: 1rem;
  font-weight: 700;
  cursor: pointer;
  transition: transform .16s ease, box-shadow .16s ease;
}

.daily-claim:hover { transform: translateY(-1px); box-shadow: 0 10px 26px rgba(255, 199, 96, .28); }
.daily-claim:disabled { opacity: .55; cursor: default; transform: none; box-shadow: none; }
.daily-claim:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

.daily-msg {
  margin: 12px 0 0;
  font-size: .84rem;
  color: var(--text-dim);
}

.daily-msg:empty { display: none; }

/* The unclaimed marker on both launchers. The sidebar row is a flex line, so the
   dot rides at its end; the tab-bar column stacks, so there it is pinned to the
   icon instead. */
.daily-dot {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--gold);
  box-shadow: 0 0 0 3px rgba(255, 199, 96, .16);
}

/* What the dot turns into once the day is collected: the rung reached, so the
   launcher keeps saying how far along the ladder the streak is. Dimmer than the
   dot on purpose — it reports, it does not ask. */
.daily-streak-pill {
  flex: none;
  min-width: 17px;
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--gold-soft);
  color: rgba(255, 207, 106, .75);
  font-size: .68rem;
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
}

.sidebar-foot.daily-launch { color: var(--gold); }
.sidebar-foot.daily-launch .daily-dot,
.sidebar-foot.daily-launch .daily-streak-pill { margin-left: auto; }

.chat-bottomnav > .daily-launch { position: relative; color: var(--gold); }
.chat-bottomnav > .daily-launch .daily-dot,
.chat-bottomnav > .daily-launch .daily-streak-pill {
  position: absolute;
  top: 7px;
  left: 50%;
  margin-left: 8px;
}

/* Phones: the modal becomes a bottom sheet, like the gift picker and the slots
   machine. The strip needs no phone treatment any more — seven pill segments
   share out whatever width there is, and at 320px they are still 34px wide. */
@media (max-width: 640px) {
  .daily-modal { padding: 0; align-items: flex-end; }
  .daily-box {
    width: 100%;
    /* Overrides .sub-modal-box{margin:auto}, which wins over the overlay's
       align-items and would leave the sheet floating mid-screen while wearing
       top-only rounded corners. */
    margin: auto 0 0;
    max-height: calc(100dvh - env(safe-area-inset-top) - 12px);
    border-radius: 24px 24px 0 0;
    padding-bottom: calc(22px + env(safe-area-inset-bottom));
  }
}

@media (prefers-reduced-motion: reduce) {
  .daily-claim { transition: none; }
  .daily-claim:hover { transform: none; box-shadow: none; }
}

/* ================= SHORT VIEWPORT ================= */
/* A laptop is a desktop by width and a phone by height. The foot of the sidebar
   — .balance-card plus five .sidebar-foot rows — runs to ~270px, and since
   .chat-list is the only stretching child of the column, every one of those
   pixels comes out of the chat list: at a 750px window it leaves room for four
   rows. The phone already answered this (see the 720px query): the balance
   becomes the pill in the brand row and the rows become one horizontal strip.
   Same answer here, with the desktop's own set of five items kept intact.

   Why the file ends with this section rather than filing it next to .sidebar:
   it has to outrank two same-specificity rules that live further down —
   .sidebar-foot.daily-launch .daily-dot and the 721px .brand-wordmark override.

   900px rather than something tighter: a 1080p monitor full-screen keeps the
   roomy layout, laptop windows (~750-880px) get the strip. */
@media (min-width: 721px) and (max-height: 900px) {
  .sidebar-top { padding: 14px 16px 10px; }
  /* Back to the base 26px logo. At 231px the wordmark spans the column and the
     balance pill has nowhere to sit. */
  .sidebar-top .brand-wordmark { width: auto; height: 26px; }
  .mobile-balance { display: inline-flex; }
  .balance-card { display: none; }

  .sidebar-nav {
    flex-direction: row;
    align-items: stretch;
    padding: 6px 4px 8px;
    border-top: 1px solid var(--hairline);
  }
  .sidebar-nav .sidebar-foot {
    position: relative;
    /* Basis auto, not 0: equal fifths of a 312px column give 60px each, and
       "Установить" needs 58 of them — every cell would sit at the edge of an
       ellipsis to keep a grid nobody reads. Sized to their labels the five come
       to 213px, and the 90px left over spreads evenly, so the cells stay
       comfortably above a 44px touch target. */
    flex: 1 1 auto;
    min-width: 0;
    flex-direction: column;
    justify-content: center;
    gap: 3px;
    margin: 0;
    padding: 8px 3px;
    font-size: .72rem;
    letter-spacing: 0;
    text-align: center;
  }
  .sidebar-nav .sidebar-foot > span {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Same corner treatment the phone tab bar gives these two: margin-left:auto
     means nothing in a column cell, and the marker would push the label down. */
  .sidebar-nav .daily-launch .daily-dot,
  .sidebar-nav .daily-launch .daily-streak-pill {
    position: absolute;
    top: 6px;
    /* Over the icon's top-right corner. The tab bar offsets this by 8px, but
       its cells are a fixed fifth of the screen; here "Бонус" is the narrowest
       label in the strip and 8px hung the streak pill off the cell. */
    left: 50%;
    margin-left: 2px;
  }
  /* The tier badge is built for margin-left:auto in a row. /profile carries it,
     which is what the phone relies on too. */
  .sidebar-nav .sub-badge { display: none; }
}
