@import url("./fonts.css");

/* ─────────────────────────────────────────────────────────────
   BLUE — a faithful iOS Messages recreation
   Values read off an iPhone dark-mode screenshot.

   Typeface note: on Apple hardware this resolves to the real SF Pro,
   which is the only genuinely correct face for this design. DM Sans is
   vendored locally as the fallback everywhere else.
   ───────────────────────────────────────────────────────────── */

:root {
  --font: -apple-system, "SF Pro Text", BlinkMacSystemFont, "DM Sans",
          "Segoe UI", sans-serif;

  /* dark (default — matches the reference) */
  --bg:            #000000;
  --bg-elev:       #1C1C1E;
  --bubble-out:    #0A84FF;
  --bubble-out-tx: #FFFFFF;
  --bubble-in:     #26262A;
  --bubble-in-tx:  #FFFFFF;
  --label:         #FFFFFF;
  --label-2:       #8E8E93;
  --label-3:       #636366;
  --hairline:      #2C2C2E;
  --field:         #1C1C1E;
  --field-border:  #3A3A3C;
  --tint:          #0A84FF;
  --nav-bg:        rgba(20, 20, 22, 0.82);
  --dot-dim:       #8E8E93;
  --dot-lit:       #C7C7CC;

  --radius:        18px;

  /* Motion tokens.
     --spring is a sampled UIKit spring: it overshoots to 1.043 and settles,
     which a cubic-bezier cannot express. Used for anything that should feel
     physical - bubbles arriving, screens pushing.
     --ease-out is for things that should simply get out of the way. */
  --spring:        linear(0, .009 1.2%, .036 2.5%, .146 5.2%, .51 12%,
                          .816 19%, .94 24%, 1.017 29%, 1.043 34%, 1.03 42%,
                          1.001 55%, .99 71%, 1);
  --spring-bez:    cubic-bezier(0.17, 0.89, 0.32, 1.12);
  --ease-out:      cubic-bezier(0.32, 0.72, 0, 1);
  /* --ease was used in three rules but never defined, so those transitions
     were silently falling back to the UA default (a symmetric ease that
     accelerates on the way out - wrong for a tap response). Colour and
     shadow changes want a fast start and a soft landing. */
  --ease:          cubic-bezier(0.25, 0.46, 0.45, 0.94);
  --ease-in:       cubic-bezier(0.4, 0, 1, 1);
  --kb-curve:      cubic-bezier(0.38, 0.70, 0.125, 1);
  --kb-dur:        250ms;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg:            #FFFFFF;
    --bg-elev:       #F2F2F7;
    --bubble-out:    #007AFF;
    --bubble-out-tx: #FFFFFF;
    --bubble-in:     #E9E9EB;
    --bubble-in-tx:  #000000;
    --label:         #000000;
    --label-2:       #8E8E93;
    --label-3:       #AEAEB2;
    --hairline:      #D1D1D6;
    --field:         #FFFFFF;
    --field-border:  #D1D1D6;
    --tint:          #007AFF;
    --nav-bg:        rgba(249, 249, 249, 0.82);
    --dot-dim:       #A3A3A8;
    --dot-lit:       #6E6E73;
  }
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* The UA rule for [hidden] is `display: none`, which ANY author display rule
   beats — and .sheet, .quick and .kb all set display:flex, so the attribute
   silently did nothing on exactly the three things that rely on it. */
[hidden] { display: none !important; }

/* Lock the document. On iOS the page itself must never scroll or the whole
   UI rubber-bands away from under the keyboard. */
html, body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
  position: fixed;
  inset: 0;
  width: 100%;
  touch-action: manipulation;   /* kills the 300ms double-tap zoom */
}

body {
  margin: 0;
  background: #0a0a0c;
  font-family: var(--font);
  color: var(--label);
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  letter-spacing: -0.01em;
  /* Chrome is UI, not a document - don't let it be selected or long-pressed */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

/* message text is the exception - you should be able to copy it */
.bubble, .row-preview { user-select: text; -webkit-user-select: text; }

/* the phone — on desktop this reads as a device, on mobile it's full bleed.
   --app-h is driven by visualViewport so the keyboard never covers the
   composer; it falls back to dvh before the script runs. */
.app {
  position: relative;
  width: 100%;
  max-width: 430px;
  height: var(--app-h, 100dvh);
  margin: 0 auto;
  background: var(--bg);
  overflow: hidden;
}

/* Two screens side by side, twice the width of the window they show through.
   Sliding this - rather than .app - keeps the clip box where it belongs. */
.rail {
  display: grid;
  grid-template-columns: 50% 50%;
  width: 200%;
  height: 100%;
  /* A screen push glides to a stop - it must not overshoot. The springy
     curve is for things that arrive, not for navigation. */
  transition: transform 420ms var(--ease-out);
  will-change: transform;
}

.app[data-screen="chat"] .rail { transform: translateX(-50%); }

/* Desktop: draw a device around it. Skipped entirely once installed. */
@media (min-width: 500px) {
  .app {
    height: min(920px, var(--app-h, 100dvh));
    margin-top: max(0px, calc((100dvh - 920px) / 2));
    border-radius: 44px;
    box-shadow: 0 0 0 11px #1a1a1d, 0 0 0 13px #303035,
                0 40px 90px -20px rgba(0, 0, 0, 0.9);
  }
}

/* Installed to the home screen: no frame, no letterboxing, edge to edge. */
@media (display-mode: standalone), (display-mode: fullscreen) {
  body { background: var(--bg); }
  .app {
    max-width: none;
    height: var(--app-h, 100dvh);
    margin-top: 0;
    border-radius: 0;
    box-shadow: none;
  }
}

.screen {
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ── nav bars ─────────────────────────────────────────────── */

.nav {
  flex: none;
  background: var(--nav-bg);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 0.5px solid var(--hairline);
  /* The blurred bar extends up BEHIND the status bar (that's the point of
     black-translucent), but its contents must clear it. max() rather than a
     bare env(): when the inset resolves to 0 - Safari tab, desktop, or before
     viewport-fit is honoured - the avatar ends up sliced by the clock. */
  padding-top: max(env(safe-area-inset-top, 0px), 6px);
  z-index: 5;
}

/* Installed, the status bar genuinely overlays the page. env() is correct on
   a notched device, but guarantee clearance on the older/flat-top ones where
   it resolves small. */
@media (display-mode: standalone), (display-mode: fullscreen) {
  .nav { padding-top: max(env(safe-area-inset-top, 0px), 24px); }
}

/* Children must never use the `padding` shorthand - it silently resets the
   safe-area padding-top set on .nav, which is what put the avatar under the
   status bar on a notched iPhone. Longhand only. */

/* list screen */
.nav-list { padding-right: 16px; padding-bottom: 8px; padding-left: 16px; }

.nav-list h1 {
  font-size: 34px;
  font-weight: 700;
  letter-spacing: -0.028em;
  margin: 4px 0 10px;
}

.search {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--bg-elev);
  border-radius: 10px;
  padding: 8px 10px;
  color: var(--label-2);
  font-size: 17px;
}

.search svg { flex: none; opacity: 0.75; }

.search input {
  border: 0;
  background: none;
  outline: none;
  color: var(--label);
  font: inherit;
  font-size: 17px;
  width: 100%;
  padding: 0;
}

.search input::placeholder { color: var(--label-2); }

/* chat screen — matches the reference: chevron / centred avatar+name / info */
.nav-chat {
  display: grid;
  grid-template-columns: 60px 1fr 60px;
  align-items: center;
  padding-right: 8px;
  padding-bottom: 8px;
  padding-left: 8px;
}

.back, .info {
  appearance: none;
  border: 0;
  background: none;
  color: var(--tint);
  cursor: pointer;
  display: flex;
  align-items: center;
  padding: 6px;
  font: inherit;
}

.back { gap: 2px; }
.info { justify-self: end; }

.back:active, .info:active { opacity: 0.4; }

.peer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-width: 0;
}

.peer .avatar { width: 50px; height: 50px; font-size: 23px; }

.peer-name {
  font-size: 11px;
  font-weight: 500;
  color: var(--label);
  display: flex;
  align-items: center;
  gap: 3px;
  letter-spacing: 0;
}

.peer-name svg { opacity: 0.5; }

/* ── avatars ──────────────────────────────────────────────── */

.avatar {
  flex: none;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 24px;
  line-height: 1;
  background: var(--accent, #48484A);
  box-shadow: inset 0 -14px 24px rgba(0, 0, 0, 0.22);
  user-select: none;
}

/* ── conversation list ────────────────────────────────────── */

.list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 12px;
  align-items: center;
  padding: 9px 16px;
  cursor: pointer;
  position: relative;
  /* Highlight appears instantly on press and only fades on RELEASE. A
     symmetric transition makes every tap feel ~120ms late. */
  transition: background 400ms ease;
}

.row:active {
  background: var(--bg-elev);
  transition: background 0s;
}

.row::after {
  content: "";
  position: absolute;
  left: 80px; right: 0; bottom: 0;
  height: 0.5px;
  background: var(--hairline);
}

.row:last-child::after { display: none; }

.row-body { min-width: 0; }

.row-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.row-name {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.022em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.row-time { font-size: 15px; color: var(--label-2); flex: none; }

.row-preview {
  font-size: 15px;
  line-height: 1.29;
  color: var(--label-2);
  margin-top: 1px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  letter-spacing: -0.01em;
}

.row-chev { color: var(--label-3); opacity: 0.5; }


/* ── browse v2: filter rail, shelves, richer cards ─────────── */

/* The rail pins to the top of the scroller so the filter is still reachable
   forty cards down. `.list` is the scroll container, so top:0 is correct
   here and would be wrong on the page. */
.browse-bar {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px 10px;
  background: linear-gradient(var(--bg) 68%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
}

.chips {
  display: flex;
  gap: 7px;
  overflow-x: auto;
  scroll-behavior: smooth;
  /* The rail scrolls; the page must not move with it. */
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding: 2px 0;
  flex: 1;
  min-width: 0;
}
.chips::-webkit-scrollbar { height: 0; }

.chip {
  appearance: none;
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font: 500 13.5px/1 var(--font);
  letter-spacing: -0.01em;
  color: var(--label);
  background: var(--bg-elev);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 8px 13px;
  cursor: pointer;
  white-space: nowrap;
  transition: background 160ms var(--ease), color 160ms var(--ease),
              border-color 160ms var(--ease), transform 160ms var(--spring-bez);
}
.chip:active { transform: scale(0.94); }
.chip.on {
  color: #fff;
  background: var(--tint);
  border-color: var(--tint);
  font-weight: 600;
}

.chip-n {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--label-2);
  opacity: 0.85;
}
.chip.on .chip-n { color: rgba(255, 255, 255, 0.8); }

.chip-dice {
  flex: none;
  padding: 8px 10px;
  line-height: 0;
  color: var(--label-2);
}
.chip-dice:hover { color: var(--tint); }

/* Section headings. Same type as the nav title, quieter. */
.shelf-h {
  display: flex;
  align-items: baseline;
  gap: 7px;
  margin: 4px 0 10px;
  padding: 0 16px;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.022em;
  color: var(--label);
}
.grid-h { margin-top: 14px; }

.shelf-n {
  font-size: 14px;
  font-weight: 500;
  color: var(--label-2);
  font-variant-numeric: tabular-nums;
}

/* New arrivals: a horizontal shelf, deliberately a different shape from the
   grid below so it reads as a suggestion rather than as the start of the
   list. */
.shelf { padding-top: 6px; }

.shelf-rail {
  display: flex;
  gap: 11px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding: 2px 16px 6px;
}
.shelf-rail::-webkit-scrollbar { height: 0; }

.tile {
  appearance: none;
  flex: none;
  scroll-snap-align: start;
  width: 124px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 14px 12px 12px;
  border-radius: 18px;
  cursor: pointer;
  text-align: left;
  font-family: var(--font);
  position: relative;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.055);
  border: 1px solid rgba(255, 255, 255, 0.11);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  animation: cardIn 380ms var(--spring) both;
  animation-delay: calc(min(var(--d, 0), 220) * 1ms);
  transition: transform 200ms var(--spring-bez);
}
.tile:active { transform: scale(0.95); }

.tile::before {
  content: "";
  position: absolute;
  inset: -60% -20% 30% -20%;
  background: radial-gradient(circle at 50% 40%,
              var(--accent) 0%, transparent 70%);
  opacity: 0.75;
  z-index: 0;
}
.tile > * { position: relative; z-index: 1; }

/* The shelf tile's face. Round, so it reads as a person rather than as a
   second smaller browse card. */
.tile-face {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  overflow: hidden;
  display: grid;
  place-items: center;
  font-size: 19px;
  line-height: 1;
  margin-bottom: 7px;
  background: var(--accent, #48484A);
  box-shadow: inset 0 -10px 18px rgba(0, 0, 0, 0.24);
}
.tile-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tile-name {
  font-size: 14.5px;
  font-weight: 600;
  color: #fff;
  letter-spacing: -0.015em;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tile-line {
  font-size: 11.5px;
  line-height: 1.3;
  color: rgba(255, 255, 255, 0.72);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The pill in the card's top-left: genre, or what you've already started. */
.card-pill {
  position: absolute;
  top: 9px; left: 9px;
  z-index: 2;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.92);
  background: rgba(0, 0, 0, 0.42);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 999px;
  padding: 2px 7px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.card-pill.is-new {
  background: rgba(10, 132, 255, 0.9);
  border-color: rgba(255, 255, 255, 0.35);
}
.card-pill.is-known {
  background: rgba(255, 255, 255, 0.9);
  border-color: transparent;
  color: #000;
}

/* A card you've talked to sits slightly forward of the ones you haven't. */
.card-known { border-color: rgba(255, 255, 255, 0.2); }

.empty-clear {
  display: block;
  margin: 0 auto;
  appearance: none;
  font: 600 15px/1 var(--font);
  color: var(--tint);
  background: none;
  border: 0;
  padding: 10px 16px;
  cursor: pointer;
}

@media (prefers-color-scheme: light) {
  .browse-bar { background: linear-gradient(var(--bg) 68%, transparent); }
  .tile {
    background: rgba(255, 255, 255, 0.45);
    border-color: rgba(255, 255, 255, 0.7);
  }
  .tile-name { color: #000; }
  .tile-line { color: rgba(0, 0, 0, 0.6); }
}

@media (min-width: 620px) {
  .tile { width: 148px; }
}

/* ── browse cards ─────────────────────────────────────────── */

.cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 13px;
  padding: 14px 16px calc(26px + env(safe-area-inset-bottom, 0px));
}

.card {
  appearance: none;
  position: relative;
  display: flex;
  flex-direction: column;
  /* Portrait, like a playing card. 3:4.4 reads as "card" where a square
     reads as "tile". */
  aspect-ratio: 3 / 4.4;
  padding: 0;
  border-radius: 20px;
  overflow: hidden;
  cursor: pointer;
  text-align: left;
  font-family: var(--font);

  /* The glass itself: a translucent pane over the accent wash below. */
  background: rgba(255, 255, 255, 0.055);
  border: 1px solid rgba(255, 255, 255, 0.11);
  backdrop-filter: blur(18px) saturate(150%);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  box-shadow: 0 10px 26px -12px rgba(0, 0, 0, 0.75),
              inset 0 1px 0 rgba(255, 255, 255, 0.09);

  /* 480ms was too slow to begin with, and an uncapped stagger made it worse:
     12 cards at 45ms apart meant the last one only started at 495ms and the
     grid wasn't settled for nearly a second. The cap is here rather than in
     JS so the ceiling holds however many characters exist. */
  animation: cardIn 380ms var(--spring) both;
  animation-delay: calc(min(var(--d, 0), 260) * 1ms);
  transition: transform 220ms var(--spring), box-shadow 220ms var(--ease);
}

@keyframes cardIn {
  from { opacity: 0; transform: translateY(14px) scale(0.965); }
  to   { opacity: 1; transform: none; }
}

.card:active {
  transform: scale(0.965);
  box-shadow: 0 6px 16px -10px rgba(0, 0, 0, 0.8);
}

/* The accent wash the glass is blurring. Sits behind everything, bleeding
   out of the top of the card. */
.card::before {
  content: "";
  position: absolute;
  inset: -30% -30% 40% -30%;
  background: radial-gradient(circle at 50% 30%,
              var(--accent) 0%, transparent 68%);
  opacity: 0.85;
  z-index: 0;
}

.card-art {
  position: relative;
  z-index: 1;
  flex: 1;
  min-height: 0;
  display: grid;
  place-items: center;
}

.card-art img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}

/* The stand-in when a character has no photograph yet: their initial, set
   large and thin over their own accent wash. It has to read as "not shot
   yet" rather than as a decoration, which is why it is type and not a
   glyph. */
.card-glyph {
  font-size: 62px;
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.03em;
  color: rgba(255, 255, 255, 0.9);
  text-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
}

/* An avatar with no photo. .avatar already paints --accent behind it, so
   this only has to make the letter look deliberate. */
.avatar.is-mono,
.tile-face.is-mono {
  font-weight: 600;
  letter-spacing: -0.02em;
  color: rgba(255, 255, 255, 0.94);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}

.card-badge {
  position: absolute;
  top: 9px; right: 9px;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: #fff;
  background: rgba(0, 0, 0, 0.45);
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 5px;
  padding: 1px 4px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.card-info {
  position: relative;
  z-index: 1;
  flex: none;
  padding: 11px 12px 13px;
  /* Legibility floor under the name, whatever the accent does above it. */
  background: linear-gradient(to bottom,
              rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.42) 32%,
              rgba(0, 0, 0, 0.58) 100%);
}

.card-name {
  display: flex;
  align-items: baseline;
  gap: 5px;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: #fff;
  min-width: 0;
}

/* min-width:0 on both: a flex item's default min-width is auto, which
   refuses to shrink below its content and pushes the age off the card. */
.card-who {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.card-age {
  font-size: 14px;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.68);
  flex: none;
}

.card-desc {
  margin-top: 3px;
  font-size: 12.5px;
  line-height: 1.32;
  color: rgba(255, 255, 255, 0.72);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Light mode: the glass needs to darken rather than lighten. */
@media (prefers-color-scheme: light) {
  .card {
    background: rgba(255, 255, 255, 0.42);
    border-color: rgba(255, 255, 255, 0.65);
    box-shadow: 0 10px 26px -14px rgba(0, 0, 0, 0.4),
                inset 0 1px 0 rgba(255, 255, 255, 0.7);
  }
}

@media (min-width: 620px) {
  .cards { grid-template-columns: repeat(3, 1fr); }
}

.list-empty {
  padding: 44px 30px;
  text-align: center;
  color: var(--label-2);
  font-size: 15px;
  line-height: 1.45;
  animation: rise 500ms var(--spring) both;
}

/* staggered entrance for the conversation list */
.list .row {
  animation: rise 380ms var(--spring) both;
  animation-delay: calc(min(var(--d, 0), 200) * 1ms);
}

@keyframes rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.avatar img {
  width: 100%; height: 100%;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}

/* ── thread ───────────────────────────────────────────────── */

.thread {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* hidden, not visible: the tail's eraser pseudo-element overhangs by 26px
     and would otherwise give the thread a horizontal scrollbar */
  overflow-x: hidden;
  overscroll-behavior: contain;   /* no pull-to-refresh, no chained bounce */
  -webkit-overflow-scrolling: touch;
  padding: 10px 12px 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* "iMessage / Today 12:11 PM" — exactly as in the reference */
.daymark {
  text-align: center;
  font-size: 11px;
  color: var(--label-2);
  margin: 10px 0 12px;
  letter-spacing: 0;
}

.daymark b { font-weight: 600; color: var(--label-2); }

/* The entrance animation lives on .bubble, NOT on .msg.
   .msg carries the drag-to-peek transform, and an animation with fill:both
   pins its own final transform forever - which silently killed the peek on
   every newly arrived message while older ones still worked. */
.msg {
  display: flex;
  margin-top: 2px;
}

/* Incoming: rises into place. */
.msg .bubble {
  animation: pop 360ms var(--spring) both;
  transform-origin: left bottom;
}

/* Outgoing: launches from the composer, so it starts smaller, lower and
   anchored to the right - the direction it actually came from. */
.msg.out .bubble {
  animation: launch 340ms var(--spring) both;
  transform-origin: right bottom;
}

.msg.no-anim .bubble { animation: none; }

@keyframes launch {
  from { opacity: 0; transform: translateY(14px) scale(0.72); }
  to   { opacity: 1; transform: none; }
}

/* consecutive messages from one sender sit tighter, iOS-style */
.msg.same { margin-top: 2px; }
.msg.turn { margin-top: 8px; }

.msg.out { justify-content: flex-end; }
.msg.in  { justify-content: flex-start; }

/* Drag the thread left to reveal per-message timestamps, as in Messages.
   Transform-only so it composites on the GPU and stays at 60fps. */
.msg {
  position: relative;
  transform: translateX(var(--peek, 0px));
  will-change: transform;
}

.thread:not(.dragging) .msg {
  transition: transform 320ms var(--ease-out);
}

.thread:not(.dragging) .msg time {
  transition: opacity 260ms ease;
}

.msg time {
  position: absolute;
  right: -62px;
  top: 50%;
  transform: translateY(-50%);
  width: 56px;
  font-size: 11px;
  color: var(--label-2);
  white-space: nowrap;
  /* --peek-ratio is unitless and set from JS. Deriving it in CSS from the px
     value is not possible: calc() cannot divide a length into a number, and
     the invalid result silently resolves to 1 - leaving every timestamp
     permanently visible. */
  opacity: var(--peek-ratio, 0);
  pointer-events: none;
  /* Above the tail's ::after mask, which is a background-coloured block
     sitting 26px to the right of an outgoing bubble - exactly where the
     timestamp lands. Without this it eats half the time on every blue
     bubble while incoming ones look fine. */
  z-index: 3;
}

@keyframes pop {
  from { opacity: 0; transform: translateY(9px) scale(0.94); }
  to   { opacity: 1; transform: none; }
}

.bubble {
  position: relative;
  /* ~265pt is where Apple caps a bubble on a 390pt phone; the percentage
     alone gets too wide on a larger screen. */
  max-width: min(68%, 265px);
  /* 7 + 22 + 7 = 36px, which is exactly twice the 18px radius, so a
     single-line bubble is a perfect stadium. */
  padding: 7px 12px;
  border-radius: var(--radius);
  font-size: 17px;
  line-height: 22px;
  letter-spacing: -0.022em;
  /* anywhere, not break-word: a pasted URL with no spaces is one token and
     break-word alone lets it push the bubble past the screen edge */
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  z-index: 1;
}

.out .bubble { background: var(--bubble-out); color: var(--bubble-out-tx); }
.in  .bubble { background: var(--bubble-in);  color: var(--bubble-in-tx); }

/* photos fill the bubble, no padding, corners clipped to the same radius */
.bubble.photo {
  padding: 0;
  overflow: hidden;
  background: var(--bubble-in);
  line-height: 0;
  max-width: min(62%, 240px);
}

.bubble.photo img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: inherit;
}

/* "taking a photo" beside the dots, so a 60s wait is explained */
.typing .stage {
  font-size: 12px;
  line-height: 1;
  color: var(--label-2);
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.typing .stage:not(:empty) { margin-left: 4px; }

/* the classic tail: a rounded wedge, then a background-coloured mask
   punched out beside it to carve the curve */
.msg.tail .bubble::before,
.msg.tail .bubble::after { content: ""; position: absolute; bottom: 0; }

.out.tail .bubble::before {
  right: -7px;
  width: 20px; height: 25px;
  background: var(--bubble-out);
  border-bottom-left-radius: 16px 14px;
}
.out.tail .bubble::after {
  right: -26px;
  width: 26px; height: 25px;
  background: var(--bg);
  border-bottom-left-radius: 10px;
}

.in.tail .bubble::before {
  left: -7px;
  width: 20px; height: 25px;
  background: var(--bubble-in);
  border-bottom-right-radius: 16px 14px;
}
.in.tail .bubble::after {
  left: -26px;
  width: 26px; height: 25px;
  background: var(--bg);
  border-bottom-right-radius: 10px;
}

/* Delivered */
.receipt {
  align-self: flex-end;
  font-size: 11px;
  font-weight: 500;
  color: var(--label-2);
  margin: 2px 4px 4px 0;
  animation: fade 260ms var(--ease-out) both;
}

@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

/* typing indicator */
.typing .bubble {
  padding: 13px 15px;
  display: flex;
  gap: 5px;
  align-items: center;
}

.typing .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--dot-dim);
  animation: blink 1.2s infinite var(--ease);
}

.typing .dot:nth-child(2) { animation-delay: 0.15s; }
.typing .dot:nth-child(3) { animation-delay: 0.30s; }

/* The old curve was ease-in-out across a symmetric 30%-peak keyframe, so each
   dot rose and fell at the same rate - which reads as a blink rather than a
   breath. Real ones swell fast and settle slowly, so the peak sits at 22% and
   the fall gets half again as long as the rise. */
@keyframes blink {
  0%,   100% { opacity: 0.34; transform: translateY(0) scale(0.82);
               background: var(--dot-dim); }
  22%        { opacity: 1;    transform: translateY(-3px) scale(1.06);
               background: var(--dot-lit); }
  58%        { opacity: 0.34; transform: translateY(0) scale(0.82);
               background: var(--dot-dim); }
}

/* ── composer ─────────────────────────────────────────────── */

.composer {
  flex: none;
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 7px 10px calc(9px + env(safe-area-inset-bottom, 0px));
  background: var(--nav-bg);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-top: 0.5px solid var(--hairline);
}

/* With the keyboard up there is no home indicator to clear, so the extra
   inset just wastes a row of pixels. */
.app.kb .composer { padding-bottom: 7px; }

.plus {
  appearance: none;
  flex: none;
  width: 33px; height: 33px;
  border-radius: 50%;
  border: 0;
  background: var(--bg-elev);
  color: var(--label-2);
  display: grid;
  place-items: center;
  cursor: pointer;
  margin-bottom: 1px;
}

.plus:active { opacity: 0.5; }

.field {
  flex: 1;
  display: flex;
  align-items: flex-end;
  gap: 6px;
  background: var(--field);
  border: 1px solid var(--field-border);
  border-radius: 18px;
  padding: 6px 6px 6px 12px;
  min-width: 0;
}

.field .input {
  flex: 1;
  border: 0;
  outline: 0;
  background: none;
  color: var(--label);
  font: inherit;
  font-size: 17px;          /* under 16px and iOS zooms the page on focus */
  line-height: 1.294;
  letter-spacing: -0.022em;
  max-height: 108px;
  overflow-y: auto;
  padding: 1px 0 2px;
  min-width: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  /* contenteditable is not a form control, so it needs these back */
  user-select: text;
  -webkit-user-select: text;
  -webkit-touch-callout: default;
}

/* contenteditable has no ::placeholder */
.field .input:empty::before {
  content: attr(data-placeholder);
  color: var(--label-2);
  pointer-events: none;
}

.send {
  appearance: none;
  flex: none;
  width: 28px; height: 28px;
  border: 0;
  border-radius: 50%;
  background: var(--tint);
  color: #fff;
  display: grid;
  place-items: center;
  cursor: pointer;
  transform: scale(0.4);
  opacity: 0;
  pointer-events: none;
  transition: transform 260ms var(--spring-bez), opacity 160ms ease;
}

.send.ready {
  transform: scale(1);
  opacity: 1;
  pointer-events: auto;
}

.send.ready:active { transform: scale(0.86); transition: transform 0s; }

.send:disabled { opacity: 0.4; pointer-events: none; }

/* Apple's own send arrow is small, but the TAP TARGET must not be. These
   pseudo-elements push the touchable area out to 44px without changing
   anything you can see. */
.send::after, .plus::after, .back::after, .info::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.send, .plus, .back, .info { position: relative; }

/* Rows are already tall enough; make sure they never fall under 44. */
.row { min-height: 60px; }

/* ── list header: title row, compose, segmented control ───── */

.title-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px;
}

.compose {
  appearance: none;
  border: 0;
  background: none;
  color: var(--tint);
  padding: 6px;
  margin-bottom: 8px;
  cursor: pointer;
  position: relative;
  transition: transform 160ms var(--spring), opacity 140ms;
}

.compose::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: 44px; height: 44px;
  transform: translate(-50%, -50%);
}

.compose:active { transform: scale(0.86); opacity: 0.55; }

/* iOS segmented control */
.seg {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 2px;
  background: var(--bg-elev);
  border-radius: 9px;
  padding: 2px;
  margin: 0 0 10px;
}

.seg button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--label);
  font-family: var(--font);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: -0.01em;
  padding: 6px 4px;
  border-radius: 7px;
  cursor: pointer;
  transition: background 180ms var(--ease), box-shadow 180ms var(--ease);
}

.seg button[aria-pressed="true"] {
  background: var(--seg-on, #636366);
  font-weight: 600;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
}

@media (prefers-color-scheme: light) {
  .seg button[aria-pressed="true"] { background: #FFFFFF; }
}

/* ── swipe a row to delete ────────────────────────────────── */

.row { touch-action: pan-y; }

.row-slide {
  display: contents;
}

.row .row-del {
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: 88px;
  border: 0;
  background: #FF3B30;
  color: #fff;
  font-family: var(--font);
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
  transform: translateX(100%);
  transition: transform 260ms var(--spring);
}

.row.open .row-del { transform: none; }

/* The content must slide aside to make room for Delete. This rule was missing:
   .row.open styled .row-del but nothing held .row-shift over, so releasing a
   swipe cleared the inline transform and sprang the content straight back to 0
   while the red panel - which is absolute, right:0 - slid in on top of the
   timestamp and chevron. Keep 88px in step with W in wireSwipe(). */
.row.open .row-shift { transform: translateX(-88px); }

.row-shift {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  transition: transform 260ms var(--spring);
}

.row.dragging .row-shift, .row.dragging .row-del { transition: none; }

/* unstarted characters in Browse read quieter */
.row.unstarted .row-preview { font-style: italic; opacity: 0.75; }

.row-badge {
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: #FF453A;
  border: 1px solid currentColor;
  border-radius: 4px;
  padding: 0 3px;
  margin-left: 6px;
  vertical-align: 1px;
}

/* ── character sheet ──────────────────────────────────────── */


@keyframes sheetUp   { from { transform: translateY(100%); } to { transform: none; } }
@keyframes sheetDown { from { transform: none; } to { transform: translateY(100%); } }


/* A flex column shrinks its children when the content overflows, which
   collapsed the name/tagline block to zero height. Nothing in the sheet
   should ever shrink - it scrolls instead. */
.sheet-body > * { flex: none; }


.avatar.big {
  width: 92px; height: 92px;
  font-size: 42px;
  box-shadow: inset 0 -20px 34px rgba(0, 0, 0, 0.22);
}

.avatar.big img {
  width: 100%; height: 100%;
  border-radius: 50%;
  object-fit: cover;
}

.pick-av-actions {
  display: flex;
  gap: 14px;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}

.pick-av-actions button {
  appearance: none; border: 0; background: none;
  color: var(--tint); font-family: var(--font); font-size: 15px;
  cursor: pointer; padding: 2px;
  transition: opacity 200ms var(--ease);
}

.pick-av-actions button:disabled { cursor: default; }

/* The price, set into the button itself. Tabular so it does not reflow the
   word next to it when the balance ticks over a digit. */
.av-cost {
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--label-2);
  background: rgba(120, 120, 128, 0.22);
  border-radius: 999px;
  padding: 1px 6px;
  margin-left: 1px;
}

.av-cost::after {
  /* Spelled out rather than a coin glyph: at 12px an icon is a smudge. */
  content: " cr";
  font-weight: 400;
  opacity: 0.7;
}

/* Generating takes tens of seconds, and a button that simply greys out for
   that long reads as broken. It breathes instead. */
#avGen.busy { opacity: 0.55; animation: avPulse 1.5s ease-in-out infinite; }
#avGen.busy .av-cost { visibility: hidden; }

@keyframes avPulse {
  0%, 100% { opacity: 0.42; }
  50%      { opacity: 0.78; }
}

@media (prefers-reduced-motion: reduce) {
  #avGen.busy { animation: none; }
}

.av-note {
  margin: 9px 0 0;
  font-size: 12.5px;
  line-height: 1.35;
  text-align: center;
  color: var(--label-2);
  max-width: 30ch;
  animation: avNoteIn 260ms var(--ease-out) both;
}

.av-note.bad { color: #FF9F9A; }

@keyframes avNoteIn {
  from { opacity: 0; transform: translateY(-3px); }
  to   { opacity: 1; transform: none; }
}

.swatches {
  display: flex;
  gap: 9px;
  justify-content: center;
  flex-wrap: wrap;
}

.swatches button {
  appearance: none;
  width: 30px; height: 30px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
  transition: transform 160ms var(--spring);
}

.swatches button[aria-pressed="true"] {
  border-color: var(--label);
  transform: scale(1.16);
}


/* iOS switch. These were written for the other app's stylesheet and never
   existed here, so the control rendered as a bare checkbox. */
.toggle {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
}

.toggle input { position: absolute; opacity: 0; pointer-events: none; }

.toggle .track {
  flex: none;
  width: 51px;
  height: 31px;
  border-radius: 31px;
  background: var(--bg-elev);
  border: 1px solid var(--hairline);
  position: relative;
  transition: background 260ms var(--ease), border-color 260ms var(--ease);
}

.toggle .knob {
  position: absolute;
  top: 2px; left: 2px;
  width: 25px; height: 25px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.28);
  transition: transform 260ms var(--spring);
}

.toggle input:checked + .track {
  background: #30D158;
  border-color: #30D158;
}

.toggle input:checked + .track .knob { transform: translateX(20px); }

.toggle input:focus-visible + .track {
  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.35);
}

.toggle-text {
  font-size: 16px;
  color: var(--label);
  line-height: 1.3;
  padding-top: 5px;
}

.toggle-text small {
  display: block;
  margin-top: 3px;
  font-size: 12.5px;
  color: var(--label-2);
}


@keyframes spin { to { transform: rotate(360deg); } }


.danger {
  appearance: none;
  border: 0;
  background: var(--bg-elev);
  color: #FF453A;
  font-family: var(--font);
  font-size: 16px;
  font-weight: 500;
  padding: 13px;
  border-radius: 11px;
  cursor: pointer;
}

.danger:active { opacity: 0.6; }

/* ── quick replies ────────────────────────────────────────── */

.quick {
  flex: none;
  display: flex;
  gap: 7px;
  padding: 0 10px 8px;
  overflow-x: auto;
  scrollbar-width: none;
  background: var(--nav-bg);
  animation: quickIn 300ms var(--spring) both;
}

.quick::-webkit-scrollbar { display: none; }

@keyframes quickIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

.quick button {
  appearance: none;
  flex: 0 0 auto;
  max-width: 74%;
  border: 1px solid var(--edge-quick, rgba(10, 132, 255, 0.42));
  background: rgba(10, 132, 255, 0.13);
  color: var(--tint);
  font-family: var(--font);
  font-size: 14.5px;
  letter-spacing: -0.01em;
  padding: 7px 13px;
  border-radius: 17px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: transform 120ms var(--spring), background 140ms;
}

.quick button:active {
  transform: scale(0.94);
  background: rgba(10, 132, 255, 0.26);
}

@media (prefers-color-scheme: light) {
  .quick button {
    background: rgba(0, 122, 255, 0.10);
    border-color: rgba(0, 122, 255, 0.32);
  }
}

/* ── our own keyboard ─────────────────────────────────────── */

:root {
  --kb-bg:      #1B1B1D;
  --kb-key:     #6A6A6E;
  --kb-mod:     #48484C;
  --kb-key-tx:  #FFFFFF;
  --kb-shadow:  rgba(0, 0, 0, 0.5);
}

@media (prefers-color-scheme: light) {
  :root {
    --kb-bg:     #D1D3D9;
    --kb-key:    #FFFFFF;
    --kb-mod:    #ADB3BC;
    --kb-key-tx: #000000;
    --kb-shadow: rgba(0, 0, 0, 0.28);
  }
}

.kb {
  flex: none;
  background: var(--kb-bg);
  padding: 8px 3px calc(6px + env(safe-area-inset-bottom, 0px));
  display: flex;
  flex-direction: column;
  gap: 11px;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  /* --kb-curve/--kb-dur are the real iOS keyboard values and were defined for
     exactly this, but nothing ever referenced them - the rule had drifted onto
     the generic ease-out. The system curve is flatter at the end, so ours now
     lands in step with the one it's standing in for. */
  animation: kbUp var(--kb-dur) var(--kb-curve) both;
}

@keyframes kbUp {
  from { transform: translateY(100%); }
  to   { transform: none; }
}

.kb-row {
  display: flex;
  gap: 6px;
  justify-content: center;
  padding: 0 3px;
}

.k {
  appearance: none;
  border: 0;
  flex: 1 1 0;
  min-width: 0;
  height: 42px;
  border-radius: 5px;
  background: var(--kb-key);
  color: var(--kb-key-tx);
  font-family: var(--font);
  font-size: 22px;
  font-weight: 400;
  letter-spacing: 0;
  box-shadow: 0 1px 0 var(--kb-shadow);
  cursor: pointer;
  /* No transition: a key must light the instant it is touched. */
}

.k.down { background: var(--kb-mod); }

.k-mod  { background: var(--kb-mod); font-size: 17px; }
.k-mod.down { background: var(--kb-key); }

.k-sm   { font-size: 15px; flex: 0 0 42px; }
.k-shift, .k-back { flex: 0 0 42px; font-size: 19px; }

.k-shift[data-state="on"]   { background: var(--kb-key); }
.k-shift[data-state="lock"] { background: var(--kb-key); text-decoration: underline; }

.k-space { flex: 1 1 auto; font-size: 15px; }

.k-send {
  flex: 0 0 78px;
  background: var(--tint);
  color: #fff;
  font-size: 15px;
  font-weight: 600;
}

.kb-emoji {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: 4px;
  padding: 0 3px;
}

.k-emoji {
  height: 38px;
  background: transparent;
  box-shadow: none;
  font-size: 24px;
}

.k-emoji.down { background: var(--kb-mod); }

/* With our keyboard up there is no system keyboard to make room for. */
body.kb-open .composer { padding-bottom: 7px; }

/* offline banner */
.offline {
  flex: none;
  background: #48484A;
  color: #fff;
  font-size: 12px;
  text-align: center;
  padding: 5px;
  display: none;
}

.offline.show { display: block; }

/* ── scrollbars: invisible, like iOS ──────────────────────── */

.list::-webkit-scrollbar, .thread::-webkit-scrollbar { width: 0; height: 0; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
  /* The blanket rule above would crush the typing dots to a 0.001ms loop
     running forever - a stuttering strobe, which is the opposite of what
     reduced motion is asking for. Hold them still instead. */
  .typing .dot {
    animation: none !important;
    opacity: 0.6;
    background: var(--dot-dim);
  }

  /* The universal rule above crushes animation-DURATION but leaves
     animation-DELAY alone, and every entrance here is `both`-filled. A 176ms
     stagger delay would therefore hold content pinned at opacity 0 long after
     an "instant" cut - reduced motion showing LESS than full motion. */
  .w-step, .w-step > *, .w-chip, .w-rule, .w-folio, .w-ai .bubble {
    animation-delay: 0ms !important;
  }
}

/* ══ character walkthrough ═══════════════════════════════════
   Six screens over a backdrop built from the character's own avatar. The
   depth is the point, so the rules below are unusually strict about what may
   move: the blurred layers rasterize once and only ever cross-fade. */

.walk {
  position: absolute;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
  animation: sheetUp 400ms var(--ease-out) both;

  /* Dark in BOTH colour schemes - this is an immersive view, the way Camera
     and full-screen Photos stay dark whatever the system is set to. Every
     reused control (.seg .toggle .swatches .bubble) reads these tokens, so
     without re-declaring them here the light block would put a white
     segmented control and a black swatch ring on top of a dark photo. */
  color-scheme: dark;
  --bg:           #08080A;
  --bg-elev:      rgba(255, 255, 255, 0.10);
  --field:        rgba(255, 255, 255, 0.08);
  --field-border: rgba(255, 255, 255, 0.18);
  --hairline:     rgba(255, 255, 255, 0.16);
  --label:        #FFFFFF;
  --label-2:      rgba(255, 255, 255, 0.62);
  --label-3:      rgba(255, 255, 255, 0.42);
  --seg-on:       rgba(255, 255, 255, 0.26);
  --bubble-in:    rgba(38, 38, 42, 0.90);
  --bubble-in-tx: #FFFFFF;
  --tint:         #0A84FF;
  color: #FFFFFF;
}

.walk.closing { animation: sheetDown 280ms var(--ease-in) both; }

/* The light block sets this to #FFFFFF, which inside the walk is a white slab
   on a dark pane. Same specificity trick, scoped. */
.walk .seg button[aria-pressed="true"] { background: var(--seg-on); }

/* ── the backdrop ──────────────────────────────────────────── */

.walk-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* The colour wash. Present from the first frame and never removed - once a
   photo arrives it stays underneath, so a dark or narrow portrait still fills
   the frame with the character's colour instead of letterboxing. */
.walk-wash {
  position: absolute;
  inset: 0;
  /* Plain declaration FIRST: iOS below 16.4 drops the color-mix rule and
     keeps this one rather than rendering nothing at all. */
  /* Was a full-strength wash of her accent colour, which next to the
     app's black read as a different product. Now the app's black IS the
     background and her colour is a glow in the corners - present, not
     dominant. */
  background: #000;
  background:
    radial-gradient(70% 44% at 20% 8%,
      color-mix(in oklab, var(--accent, #3C6DA8) 30%, #000) 0%,
      transparent 64%),
    radial-gradient(78% 46% at 84% 88%,
      color-mix(in oklab, var(--accent, #3C6DA8) 20%, #000) 0%,
      transparent 66%),
    #000;
  transition: background 420ms var(--ease);
}

/* Before there's a photo - which is all of step 01 and the normal case for a
   new character - this is the backdrop: their emoji, enormous and defocused.
   Derived from the character from the very first frame, so the flow is never
   generic and never empty. */
.walk-glyph {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 260px;
  line-height: 1;
  filter: blur(52px);
  opacity: 0.20;
  transition: opacity 520ms var(--ease-out);
}

.walk[data-face="1"] .walk-glyph { opacity: 0; }

/* Three copies of one image at three blur radii. All three share a single
   decode because WebKit keys its cache on the URL. */
.walk-f {
  position: absolute;
  inset: 0;
  background-image: var(--face);
  background-size: cover;
  background-position: 50% 30%;
  opacity: 0;
  /* The ONLY property that may ever animate on this layer. Transitioning
     `filter` re-runs the Gaussian every frame; animating `transform` on it
     forces a re-rasterize. Both are why this stays a pure opacity crossfade. */
  transition: opacity 520ms var(--ease-out);
}

/* Overscaled past 1 + 2*blur/375 so the blur's own transparent edge bleed is
   pushed off-screen. Clears a 430px viewport too. Static declarations - these
   rasterize once at load. */
.walk-f1 { filter: blur( 8px) saturate(140%) brightness(0.72); transform: scale(1.12); }
.walk-f2 { filter: blur(24px) saturate(150%) brightness(0.68); transform: scale(1.30); }
.walk-f3 { filter: blur(60px) saturate(160%) brightness(0.62); transform: scale(1.60); }

.walk-grain {
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  background-size: 128px 128px;
  opacity: 0.045;
}

/* Dark where the type is - under the nav and under the button - and breathing
   in the middle where the face is. --veil is measured from the photo itself
   (see measure() in app.js) so a blown-out portrait gets a heavier scrim. */
.walk-scrim {
  position: absolute;
  inset: 0;
  background-color: rgba(0, 0, 0, var(--veil, 0.52));
  background-image: linear-gradient(180deg,
    rgba(0, 0, 0, 0.86) 0%, rgba(0, 0, 0, 0.46) 30%,
    rgba(0, 0, 0, 0.56) 66%, rgba(0, 0, 0, 0.92) 100%);
  transition: background-color 520ms var(--ease-out);
}

/* The focus rack. Semantic, not monotonic: sharpest where you are looking at
   them (look, file), deepest where you are inside their head (voice). 520ms
   against the content's 340ms, so the room settles after the content does -
   that lag is the whole parallax. */
.walk[data-step="premise"] .walk-f1 { opacity: 0; }
.walk[data-step="premise"] .walk-f2 { opacity: 0.30; }
.walk[data-step="premise"] .walk-f3 { opacity: 1; }

.walk[data-step="look"]    .walk-f1 { opacity: 1; }
.walk[data-step="look"]    .walk-f2 { opacity: 0.10; }
.walk[data-step="look"]    .walk-f3 { opacity: 0; }

.walk[data-step="wear"]    .walk-f1 { opacity: 0.55; }
.walk[data-step="wear"]    .walk-f2 { opacity: 1; }
.walk[data-step="wear"]    .walk-f3 { opacity: 0; }

.walk[data-step="traits"]  .walk-f1 { opacity: 0.15; }
.walk[data-step="traits"]  .walk-f2 { opacity: 1; }
.walk[data-step="traits"]  .walk-f3 { opacity: 0.35; }

.walk[data-step="voice"]   .walk-f1 { opacity: 0; }
.walk[data-step="voice"]   .walk-f2 { opacity: 0.40; }
.walk[data-step="voice"]   .walk-f3 { opacity: 1; }

.walk[data-step="file"]    .walk-f1 { opacity: 1; }
.walk[data-step="file"]    .walk-f2 { opacity: 0; }
.walk[data-step="file"]    .walk-f3 { opacity: 0; }

/* The photoless path racks too. `filter` differs between static rules and is
   never transitioned, so each variant rasterizes once on the step it belongs
   to rather than on every frame of the change. */
.walk[data-step="look"] .walk-glyph,
.walk[data-step="file"] .walk-glyph { opacity: 0.52; filter: blur(30px); }

/* ── chrome ────────────────────────────────────────────────── */

.w-nav {
  position: relative;
  z-index: 2;
  flex: none;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 8px;
  padding-top: max(env(safe-area-inset-top, 0px), 10px);
  padding-bottom: 11px;
  padding-inline: 20px;
}

.w-back, .w-save {
  appearance: none;
  border: 0;
  background: none;
  /* Blue, like .sheet-cancel and every other cancel in the app. White was
     part of the over-the-photo treatment that has gone. */
  color: var(--tint);
  font-family: var(--font);
  font-size: 17px;
  cursor: pointer;
  padding: 4px 0;
}

.w-back { justify-self: start; }
.w-save { justify-self: end; font-weight: 600; }
.w-back:active, .w-save:active { opacity: 0.45; }

/* Not a progress bar and not decorative dots - an iOS page control that
   lengthens. 380ms matches .rail's push, so moving inside the flow feels
   like the same machine as moving between screens. */
.w-ticks { display: flex; gap: 4px; align-items: center; }

.w-tick {
  width: 6px;
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.24);
  transition: width 380ms var(--ease-out),
              background-color 240ms var(--ease);
}

.w-tick.done { background: rgba(255, 255, 255, 0.55); }
.w-tick.done { background: var(--accent, #fff); }
.w-tick.now  { width: 22px; background: #fff; }
.w-tick.now  { width: 22px; background: color-mix(in oklab, var(--accent, #fff) 55%, #fff); }
.walk[data-edit="1"] .w-tick { cursor: pointer; }

.w-body {
  position: relative;
  z-index: 2;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 18px 20px 24px;
}

.w-body::-webkit-scrollbar { width: 0; }

.w-foot {
  position: relative;
  z-index: 2;
  flex: none;
  padding: 14px 20px calc(14px + env(safe-area-inset-bottom, 0px));
}

.w-cta {
  appearance: none;
  width: 100%;
  border: 0;
  border-radius: 13px;
  background: var(--tint);
  color: #fff;
  font-family: var(--font);
  font-size: 17px;
  font-weight: 600;
  padding: 14px;
  cursor: pointer;
  transition: transform 160ms var(--spring), opacity 160ms var(--ease);
}

.w-cta:active { transform: scale(0.97); }
.w-cta.busy { opacity: 0.55; }

/* ── a step ────────────────────────────────────────────────── */

.w-step { display: flex; flex-direction: column; }

/* 18px, not the full width. The backdrop is stationary, so a full-width push
   would tear the depth: if the content travels the screen and the photo
   behind it doesn't, the photo reads as printed ON the content rather than
   sitting behind it. Hold this when a bigger slide gets proposed. */
@keyframes wOutF { to   { opacity: 0; transform: translateX(-18px); } }
@keyframes wInF  { from { opacity: 0; transform: translateX( 22px); } }
@keyframes wOutB { to   { opacity: 0; transform: translateX( 18px); } }
@keyframes wInB  { from { opacity: 0; transform: translateX(-22px); } }

.w-step.out-f { animation: wOutF 190ms var(--ease-in) both; }
.w-step.out-b { animation: wOutB 190ms var(--ease-in) both; }
.w-step.in-f  { animation: wInF 340ms var(--ease-out) both; animation-delay: 70ms; }
.w-step.in-b  { animation: wInB 340ms var(--ease-out) both; animation-delay: 70ms; }

/* Capped in CSS rather than JS for the same reason .card's stagger is: the
   ceiling then holds however many children a step ends up with. */
.w-step > * {
  animation: wRise 320ms var(--ease-out) both;
  animation-delay: calc(min(var(--i, 0), 4) * 44ms);
}

@keyframes wRise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.w-folio {
  /* Was 84px of magazine watermark. The tick row in the nav already says
     where you are, and it says it the way iOS does. */
  display: none;
}

@keyframes folioIn {
  from { opacity: 0; transform: translateY(20px) scale(1.05); }
  to   { opacity: 1; transform: none; }
}

.w-title {
  /* Matches .nav-list h1 - the same large title the conversation list
     uses, so the flow reads as part of the app rather than a guest in it. */
  font-size: 28px;
  line-height: 1.1;
  font-weight: 700;
  letter-spacing: -0.028em;
  text-wrap: balance;
  margin: 4px 0 5px;
}

.w-deck {
  font-size: 14.5px;
  line-height: 1.4;
  color: var(--label-2);
  max-width: 34ch;
  margin-bottom: 18px;
}

.w-rule {
  /* An editorial device, and the one thing that most made this look like
     a printed page. iOS separates with space, not with lines. */
  display: none;
}

@keyframes ruleDraw { from { transform: scaleX(0); } to { transform: none; } }

/* ── panes ─────────────────────────────────────────────────── */

.w-pane {
  position: relative;
  border-radius: 14px;
  padding: 12px 13px;
  margin-bottom: 14px;
  /* Legible on its own over ANY photo. The @supports below only lightens it
     once the blur is genuinely doing work - a translucent pane that assumes
     backdrop-filter is the classic way this kind of screen becomes
     unreadable on the one device where the blur silently no-ops. */
  /* An iOS inset grouped row: solid fill, soft radius, no border and no
     shadow. The glass-over-photo treatment was legible but belonged to a
     different design language. */
  background: var(--bg-elev);
  border: 0;
  transition: background 600ms var(--ease);
}

/* No @supports blur here any more: the pane is opaque by design, so the
   backdrop shows around it rather than through it. */

/* The accept flash: a tint of the row rather than a border, since the row
   no longer has one. */
.w-pane.inked { background: color-mix(in oklab, var(--tint) 22%, var(--bg-elev)); }
.w-pane.bad   { box-shadow: inset 2px 0 0 #FF453A; }

/* Above its field, not beside it. This is the deliberate break from
   .row-in's 88px label column and it is the single thing that stops the flow
   reading as an iOS Settings pane. */
.w-label {
  display: block;
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--label-2);
  margin-bottom: 3px;
}

.w-pane input,
.w-pane textarea {
  display: block;
  width: 100%;
  border: 0;
  outline: 0;
  background: none;
  color: #fff;
  font-family: var(--font);
  /* >=16px is what stops iOS zooming the viewport on focus. 17px matches
     .bubble, so what you type looks like what it becomes. */
  font-size: 17px;
  line-height: 1.42;
  resize: none;
  min-width: 0;
}

.w-pane input::placeholder,
.w-pane textarea::placeholder { color: rgba(255, 255, 255, 0.34); }

.w-mast {
  font-size: 28px !important;
  font-weight: 700;
  letter-spacing: -0.03em;
}

#fAge { font-variant-numeric: tabular-nums; }

.w-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.w-pair .w-pane { margin-bottom: 14px; }

.w-toggle {
  background: rgba(18, 18, 20, 0.62);
  border: 0.5px solid rgba(255, 255, 255, 0.13);
  border-radius: 14px;
  padding: 13px;
  margin-bottom: 14px;
  align-items: center;
}

.w-toggle .toggle-text { padding-top: 0; }

/* ── look step ─────────────────────────────────────────────── */

.w-face {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
}

.avatar.big.sharp { box-shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.9); }

.w-face .pick-av-actions button { color: var(--tint); font-weight: 500; }

.walk .swatches { margin-bottom: 18px; }
.walk .swatches button[aria-pressed="true"] { border-color: #fff; }

/* ── voice step ────────────────────────────────────────────── */

.w-settings { display: flex; flex-direction: column; gap: 14px; }
.w-set .w-label { margin-bottom: 7px; }
.w-seg { margin: 0; }

/* ── chips ─────────────────────────────────────────────────── */

.w-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 16px;
}

.w-chip {
  appearance: none;
  height: 32px;
  padding: 0 13px;
  border-radius: 16px;
  border: 0.5px solid rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.10);
  color: #fff;
  font-family: var(--font);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: -0.005em;
  cursor: pointer;
  animation: wRise 300ms var(--ease-out) both;
  animation-delay: calc(min(var(--i, 0), 5) * 40ms);
  transition: background 180ms var(--ease), border-color 180ms var(--ease);
}

.w-chip:active { transform: scale(0.94); transition: transform 140ms var(--spring); }
.w-chip[aria-pressed="true"] {
  background: var(--accent, var(--tint));
  border-color: transparent;
  font-weight: 600;
}
.w-chip.primary {
  background: #fff;
  border-color: transparent;
  color: #000;
  font-weight: 600;
}
.w-chip:disabled { opacity: 0.45; }

/* ── the AI's turn ─────────────────────────────────────────── */

/* The assist arrives as a real incoming message, because that is what this
   app is made of. The bubble is never the tap target - you accept from the
   chip row underneath it, so the thing you press doesn't move or resize. */
.w-ai { margin-bottom: 4px; }
.w-ai:empty { display: none; }

.w-ai .msg {
  display: flex;
  max-width: 100%;
  margin: 0 0 8px;
}

.w-ai .bubble {
  max-width: 88%;
  animation: pop 360ms var(--spring) both;
}

/* NEVER .tail here: .in.tail .bubble::after is an eraser wedge painted in
   var(--bg), which over a photo is a visible black notch. */

.w-nudge { display: flex; flex-wrap: wrap; gap: 7px; }

.w-ai.thinking .bubble { animation: none; }

@keyframes wAccept {
  to { opacity: 0; transform: translateY(-8px) scale(0.97); }
}

.w-ai.taking .msg { animation: wAccept 200ms var(--ease-in) both; }

@keyframes inkIn {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

.w-pane.inked input,
.w-pane.inked textarea { animation: inkIn 260ms var(--ease-out) both; }

.w-err {
  font-size: 13.5px;
  color: var(--label-2);
  margin-bottom: 8px;
}

/* ── the file ──────────────────────────────────────────────── */

/* The one alignment change in the flow. Centring the name and tagline under
   a sharp portrait is what makes the last screen read as an arrival rather
   than as another form. */
.w-port {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  margin-bottom: 22px;
}

.w-port .w-mast,
.w-port .w-tag {
  width: 100%;
  border: 0;
  outline: 0;
  background: none;
  color: #fff;
  font-family: var(--font);
  text-align: center;
}

.w-port .w-tag {
  font-size: 15px;
  color: var(--label-2);
  font-style: italic;
}

.w-port input::placeholder { color: rgba(255, 255, 255, 0.30); }

.w-rows {
  border-radius: 14px;
  overflow: hidden;
  background: rgba(18, 18, 20, 0.62);
  border: 0.5px solid rgba(255, 255, 255, 0.13);
  margin-bottom: 16px;
}

.w-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 11px 13px;
  border-bottom: 0.5px solid var(--hairline);
}

.w-row:last-child { border-bottom: 0; }
.w-row.bad { box-shadow: inset 2px 0 0 #FF453A; }

.w-row-k {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--label-2);
  margin-bottom: 3px;
}

.w-row-v {
  font-size: 14.5px;
  line-height: 1.35;
  color: #fff;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.w-row-v.empty { color: var(--label-3); font-style: italic; }
.w-row.bad .w-row-v { color: #FF453A; }

.w-retake {
  appearance: none;
  flex: none;
  border: 0.5px solid rgba(255, 255, 255, 0.24);
  background: none;
  color: #fff;
  font-family: var(--font);
  font-size: 12.5px;
  font-weight: 500;
  border-radius: 14px;
  padding: 5px 11px;
  cursor: pointer;
}

.w-retake:active { opacity: 0.5; }

.w-raw { margin-bottom: 16px; }

.w-raw summary {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--label-2);
  cursor: pointer;
  padding: 6px 3px;
  list-style: none;
}

.w-raw summary::-webkit-details-marker { display: none; }
.w-raw summary::before { content: "▸ "; }
.w-raw[open] summary::before { content: "▾ "; }

.w-raw textarea {
  width: 100%;
  border-radius: 12px;
  border: 0.5px solid rgba(255, 255, 255, 0.16);
  background: rgba(10, 10, 12, 0.72);
  color: #fff;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.45;
  padding: 11px 12px;
  outline: 0;
  resize: vertical;
}

.w-hint {
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--label-3);
  padding: 7px 3px 0;
}

.walk .danger { margin-top: 4px; }

/* ══ accounts and credits ════════════════════════════════════
   Reuses the walkthrough's visual language - dark in both schemes, glass
   panes over an accent wash - rather than introducing a third look. */

/* ── the balance chip ──────────────────────────────────────── */

.title-actions { display: flex; align-items: center; gap: 10px; }

.wallet {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 28px;
  padding: 0 11px;
  border-radius: 14px;
  border: 0.5px solid var(--hairline);
  background: var(--bg-elev);
  color: var(--label);
  font-family: var(--font);
  font-size: 13.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  cursor: pointer;
  transition: transform 160ms var(--spring), background 200ms var(--ease),
              color 200ms var(--ease);
}

.wallet svg { opacity: 0.65; flex: none; }
.wallet:active { transform: scale(0.94); }

/* Running low is worth noticing before the moment it stops working. */
.wallet.low {
  background: rgba(255, 159, 10, 0.16);
  border-color: rgba(255, 159, 10, 0.45);
  color: #FF9F0A;
}
.wallet.low svg { opacity: 1; }

.wallet.empty {
  background: rgba(255, 69, 58, 0.16);
  border-color: rgba(255, 69, 58, 0.45);
  color: #FF453A;
}
.wallet.empty svg { opacity: 1; }

/* The number changing is the entire feedback loop for spending, so it gets a
   beat of its own rather than silently ticking down. */
@keyframes walletBump {
  0%   { transform: none; }
  38%  { transform: scale(1.14); }
  100% { transform: none; }
}

.wallet.bump { animation: walletBump 420ms var(--spring); }

/* ── sign in ───────────────────────────────────────────────── */

.gate {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  place-items: center;
  padding: 24px;
  overflow-y: auto;
  color-scheme: dark;
  color: #fff;
  --label-2: rgba(255, 255, 255, 0.62);
  --hairline: rgba(255, 255, 255, 0.16);
  animation: fade 320ms var(--ease-out) both;
}

.gate.closing { animation: gateOut 300ms var(--ease-in) both; }
@keyframes gateOut { to { opacity: 0; transform: scale(1.02); } }

.gate-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  /* The crimson pool went with the rest of the warm palette; the marketing
     site and the app are both blue on black now and this is the seam
     between them. */
  background: #04050A;
  background-image:
    radial-gradient(70% 50% at 22% 8%, rgba(10, 132, 255, 0.34) 0%, transparent 64%),
    radial-gradient(74% 54% at 82% 88%, rgba(100, 210, 255, 0.18) 0%, transparent 68%),
    linear-gradient(180deg, #070A12, #030408);
}

.gate-card {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 340px;
  padding: 30px 24px 22px;
  border-radius: 22px;
  background: rgba(18, 18, 20, 0.72);
  border: 0.5px solid rgba(255, 255, 255, 0.13);
  box-shadow: 0 30px 70px -30px rgba(0, 0, 0, 0.95),
              inset 0 1px 0 rgba(255, 255, 255, 0.07);
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .gate-card {
    background: rgba(18, 18, 20, 0.55);
    backdrop-filter: blur(24px) saturate(150%);
    -webkit-backdrop-filter: blur(24px) saturate(150%);
  }
}

.gate-card > * {
  animation: wRise 380ms var(--ease-out) both;
  animation-delay: calc(min(var(--i, 0), 5) * 50ms);
}

.gate-mark  { --i: 0; }
.gate-title { --i: 1; }
.gate-deck  { --i: 2; }
.gate-form  { --i: 3; }
.gate-swap  { --i: 4; }

.gate-mark {
  /* Was a 💬 emoji, which sat oddly on a screen whose whole app had just
     had emoji taken out of it. The wordmark is the same one the marketing
     site uses, so arriving here reads as the same company. */
  font-family: var(--font);
  font-size: 25px;
  font-weight: 700;
  letter-spacing: -0.03em;
  color: #fff;
  margin-bottom: 20px;
}

.gate-mark b { color: var(--tint); font-weight: 700; }

.gate-title {
  font-size: 27px;
  font-weight: 700;
  letter-spacing: -0.035em;
  margin: 0 0 5px;
}

.gate-deck {
  font-size: 14.5px;
  line-height: 1.45;
  color: var(--label-2);
  margin: 0 0 20px;
}

.gate-form { display: flex; flex-direction: column; gap: 11px; }

.gate-field { display: block; }

.gate-field > span {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.42);
  margin-bottom: 5px;
}

.gate-field input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  /* 17px: under 16 makes iOS zoom the viewport on focus, and a login form
     that jumps as you tap it reads as broken before you have even used it. */
  font-size: 17px;
  padding: 11px 13px;
  border-radius: 12px;
  border: 0.5px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.07);
  color: #fff;
  outline: none;
  transition: border-color 180ms var(--ease), background 180ms var(--ease);
}

.gate-field input:focus {
  border-color: rgba(10, 132, 255, 0.75);
  background: rgba(255, 255, 255, 0.10);
}

.gate-field input::placeholder { color: rgba(255, 255, 255, 0.30); }

.gate-adult {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--label-2);
  cursor: pointer;
  padding: 2px 0;
}

.gate-adult input { position: absolute; opacity: 0; pointer-events: none; }

.gate-check {
  flex: none;
  width: 20px; height: 20px;
  margin-top: 1px;
  border-radius: 6px;
  border: 1.5px solid rgba(255, 255, 255, 0.34);
  position: relative;
  transition: background 180ms var(--ease), border-color 180ms var(--ease);
}

.gate-adult input:checked + .gate-check {
  background: #0A84FF;
  border-color: #0A84FF;
}

.gate-adult input:checked + .gate-check::after {
  content: "";
  position: absolute;
  left: 6px; top: 2px;
  width: 5px; height: 10px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(43deg);
}

.gate-adult input:focus-visible + .gate-check {
  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.4);
}

.gate-err {
  font-size: 13.5px;
  line-height: 1.4;
  color: #FF453A;
  margin: 0;
  animation: wRise 260ms var(--ease-out) both;
}

.gate-go {
  appearance: none;
  width: 100%;
  border: 0;
  border-radius: 13px;
  background: #0A84FF;
  color: #fff;
  font-family: var(--font);
  font-size: 17px;
  font-weight: 600;
  padding: 13px;
  margin-top: 3px;
  cursor: pointer;
  transition: transform 160ms var(--spring), opacity 160ms var(--ease);
}

.gate-go:active { transform: scale(0.975); }
.gate-go:disabled { opacity: 0.5; }

.gate-swap {
  appearance: none;
  display: block;
  width: 100%;
  margin-top: 16px;
  border: 0;
  background: none;
  color: var(--label-2);
  font-family: var(--font);
  font-size: 13.5px;
  cursor: pointer;
  padding: 6px;
}

.gate-swap:active { opacity: 0.55; }

/* ── the credits sheet ─────────────────────────────────────── */

.sheetlite {
  position: absolute;
  inset: 0;
  z-index: 45;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  animation: sheetUp 400ms var(--ease-out) both;
}

.sheetlite.closing { animation: sheetDown 280ms var(--ease-in) both; }

.sl-nav {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: max(env(safe-area-inset-top, 0px), 10px);
  padding-bottom: 11px;
  padding-inline: 20px;
  border-bottom: 0.5px solid var(--hairline);
}

.sl-title { font-size: 17px; font-weight: 600; }

.sl-done {
  appearance: none; border: 0; background: none;
  color: var(--tint);
  font-family: var(--font); font-size: 17px; font-weight: 600;
  cursor: pointer; padding: 4px 0;
}

.sl-done:active { opacity: 0.45; }

.sl-body {
  flex: 1; min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 24px 20px calc(24px + env(safe-area-inset-bottom, 0px));
}

.cr-hero { text-align: center; margin-bottom: 26px; }

.cr-big {
  font-size: 62px;
  font-weight: 700;
  letter-spacing: -0.05em;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  animation: folioIn 460ms var(--ease-out) both;
}

.cr-sub {
  font-size: 13px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--label-2);
  margin-top: 8px;
}

.cr-log {
  background: var(--bg-elev);
  border-radius: 13px;
  overflow: hidden;
  margin-bottom: 22px;
}

.cr-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 14px;
  border-bottom: 0.5px solid var(--hairline);
  font-size: 14.5px;
}

.cr-row:last-child { border-bottom: 0; }

.cr-row-what {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cr-row-when { font-size: 11.5px; color: var(--label-3); flex: none; }

.cr-amt {
  flex: none;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.cr-amt.plus { color: #30D158; }
.cr-amt.minus { color: var(--label-2); }

.cr-empty {
  padding: 18px 14px;
  color: var(--label-2);
  font-size: 14px;
  text-align: center;
}

.sl-signout {
  appearance: none;
  width: 100%;
  border: 0;
  border-radius: 13px;
  background: var(--bg-elev);
  color: #FF453A;
  font-family: var(--font);
  font-size: 16px;
  font-weight: 500;
  padding: 13px;
  cursor: pointer;
}

.sl-signout:active { opacity: 0.6; }

/* An app note in the thread - not her voice, so it borrows the day divider's
   register, tinted so it is clearly a system line rather than a date. */
.daymark.note { color: #FF9F0A; }

/* ── buying credits ────────────────────────────────────────── */

.cr-shop { margin-bottom: 22px; }

.cr-shop-head {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--label-2);
  margin: 0 3px 9px;
}

.cr-packs { display: flex; flex-direction: column; gap: 9px; }

.cr-pack {
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  text-align: left;
  padding: 13px 15px;
  border-radius: 14px;
  border: 0.5px solid var(--hairline);
  background: var(--bg-elev);
  color: var(--label);
  font-family: var(--font);
  cursor: pointer;
  transition: transform 160ms var(--spring), border-color 180ms var(--ease);
}

.cr-pack:active { transform: scale(0.98); }
.cr-pack:disabled { opacity: 0.55; cursor: default; }

/* One pack carries a mark rather than a colour: the row is already dark on
   dark, and a second accent competes with the balance above it. */
.cr-pack.best { border-color: var(--tint); }

.cr-pack-n {
  font-size: 19px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.cr-pack-sub {
  display: block;
  font-size: 12px;
  font-weight: 400;
  color: var(--label-2);
  margin-top: 2px;
  letter-spacing: 0;
}

.cr-pack-price {
  flex: none;
  font-size: 16px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.cr-pack-tag {
  display: inline-block;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--tint);
  border: 1px solid currentColor;
  border-radius: 4px;
  padding: 0 4px;
  margin-left: 7px;
  vertical-align: 2px;
}

.cr-pending {
  font-size: 12.5px;
  line-height: 1.45;
  color: #FF9F0A;
  margin: 11px 3px 0;
}

/* ── the price on a quick reply ────────────────────────────── */

/* The number sits inside the chip rather than in a caption above the row.
   A price you have to go looking for is not a price you were shown. */
.quick button .qc {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-left: 7px;
  padding: 1px 5px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.13);
  font-size: 10.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  opacity: 0.85;
  vertical-align: 1px;
}

@media (prefers-color-scheme: light) {
  .quick button .qc { background: rgba(0, 0, 0, 0.10); }
}

/* Not affordable: still visible, obviously inert, and it says why on tap. */
.quick button.cant { opacity: 0.45; }
.quick button.cant .qc { background: rgba(255, 69, 58, 0.28); }

/* ── out of credits ────────────────────────────────────────── */

.dry {
  position: absolute;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: 26px;
  background: rgba(0, 0, 0, 0.62);
  animation: fade 260ms var(--ease-out) both;
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .dry {
    background: rgba(0, 0, 0, 0.42);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
  }
}

.dry.closing { animation: gateOut 240ms var(--ease-in) both; }

.dry-card {
  width: 100%;
  max-width: 320px;
  padding: 26px 22px 18px;
  border-radius: 20px;
  background: var(--bg-elev);
  border: 0.5px solid var(--hairline);
  box-shadow: 0 30px 70px -30px rgba(0, 0, 0, 0.95);
  text-align: center;
  animation: pop 360ms var(--spring) both;
}

.dry-mark {
  width: 46px; height: 46px;
  margin: 0 auto 14px;
  border-radius: 13px;
  display: grid; place-items: center;
  font-size: 22px;
  background: rgba(255, 69, 58, 0.16);
  color: #FF453A;
}

.dry-title { font-size: 19px; font-weight: 700; margin: 0 0 6px;
             letter-spacing: -0.02em; }

.dry-deck { font-size: 14px; line-height: 1.45; color: var(--label-2);
            margin: 0 0 18px; }

.dry-go {
  appearance: none; width: 100%; border: 0; border-radius: 13px;
  background: var(--tint); color: #fff;
  font-family: var(--font); font-size: 16px; font-weight: 600;
  padding: 13px; cursor: pointer;
  transition: transform 160ms var(--spring);
}

.dry-go:active { transform: scale(0.975); }

.dry-later {
  appearance: none; width: 100%; margin-top: 8px; border: 0; background: none;
  color: var(--label-2); font-family: var(--font); font-size: 14px;
  padding: 8px; cursor: pointer;
}

.dry-later:active { opacity: 0.55; }

/* ── a photo, full size ────────────────────────────────────── */

/* Inside .app rather than fixed to the page, so on a desktop it fills the
   phone frame instead of the whole browser window - the same thing tapping a
   photo does in a real messaging app. */
.lightbox {
  position: absolute;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  background: #000;
  animation: fade 200ms var(--ease-out) both;
  cursor: zoom-out;
}

.lightbox.closing { animation: lbOut 180ms var(--ease-in) both; }
@keyframes lbOut { to { opacity: 0; } }

.lightbox img {
  max-width: 100%;
  /* Leaves the close button clear of a notch at the top and the home
     indicator at the bottom. */
  max-height: calc(100% - env(safe-area-inset-top, 0px)
                        - env(safe-area-inset-bottom, 0px));
  object-fit: contain;
  animation: lbIn 260ms var(--ease-out) both;
}

@keyframes lbIn {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: none; }
}

.lb-close {
  position: absolute;
  top: max(env(safe-area-inset-top, 0px), 12px);
  right: 14px;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  font-family: var(--font);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
  padding: 0 0 2px;
}

.lb-close:active { background: rgba(255, 255, 255, 0.26); }

/* A photo in the thread is now a tap target, so say so. */
.bubble.photo img { cursor: zoom-in; }

/* The two actions at the foot of the file screen. Stacked, destructive one
   last, with the softer of the two visually quieter - clearing a thread and
   deleting a person should not look like the same weight of decision. */
.w-acts { display: flex; flex-direction: column; gap: 9px; margin-top: 6px; }
.danger.soft { color: var(--label); background: var(--bg-elev); }


/* ==========================================================================
   The briefing
   ========================================================================== */

.brief {
  position: absolute;
  inset: 0;
  z-index: 38;                 /* under the walkthrough, over both screens */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);

  /* Dark in both colour schemes, like .walk: this is an immersive view sat
     on top of a photograph, and a light one would put white cards over it. */
  color-scheme: dark;
  --bg:       #000000;
  --bg-elev:  #1C1C1E;
  --label:    #FFFFFF;
  --label-2:  rgba(235, 235, 245, 0.60);
  --label-3:  rgba(235, 235, 245, 0.30);
  --hairline: rgba(255, 255, 255, 0.11);
  --tint:     #0A84FF;
  color: #FFFFFF;

  /* A navigation push, not a sheet: it comes from the right and glides to a
     stop. No spring - things that arrive can overshoot, things you navigate
     to must not. */
  animation: briefIn 380ms var(--ease-out) both;
}

.brief.closing { animation: briefOut 300ms var(--ease-in) both; }

@keyframes briefIn {
  from { transform: translateX(100%); }
  to   { transform: none; }
}

@keyframes briefOut {
  from { transform: none; }
  to   { transform: translateX(100%); }
}

.brief-scroll {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  /* Clears the action bar, which floats over the end of the content. */
  padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px));
}

/* ── the photo ─────────────────────────────────────────────────────────── */

.brief-hero {
  position: relative;
  /* Tall enough to be a portrait, short enough that the first fact peeks
     above the fold and says there is more below. */
  height: min(62vh, 520px);
  overflow: hidden;
  isolation: isolate;
}

.brief-photo {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center 22%;   /* faces sit high in a portrait crop */
  background-repeat: no-repeat;
  animation: briefPhoto 900ms var(--ease-out) both;
}

/* No photo yet: their initial, huge, on their own colour. */
.brief-photo.is-mono {
  display: grid;
  place-items: center;
  font-size: 168px;
  font-weight: 600;
  letter-spacing: -0.04em;
  color: rgba(255, 255, 255, 0.92);
  background: linear-gradient(160deg,
    color-mix(in oklab, var(--accent, #3C6DA8) 82%, #fff) 0%,
    var(--accent, #3C6DA8) 46%,
    color-mix(in oklab, var(--accent, #3C6DA8) 55%, #000) 100%);
}

@keyframes briefPhoto {
  from { transform: scale(1.06); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

/* Their accent, bleeding out of the top edge. The one piece of them that is
   theirs alone, and the only colour on the screen that is not iOS blue. */
.brief-wash {
  position: absolute;
  inset: 0 0 auto;
  height: 46%;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--accent, #3C6DA8) 62%, transparent) 0%,
    color-mix(in oklab, var(--accent, #3C6DA8) 16%, transparent) 54%,
    transparent 100%);
  mix-blend-mode: soft-light;
}

/* Measured, not guessed: heavy at the very bottom where the title sits,
   almost nothing across the face. */
.brief-scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(0, 0, 0, 0.42) 0%,
    rgba(0, 0, 0, 0.06) 26%,
    rgba(0, 0, 0, 0.10) 52%,
    rgba(0, 0, 0, 0.72) 84%,
    #000 100%);
}

.brief-head {
  position: absolute;
  z-index: 4;
  left: 20px;
  right: 20px;
  bottom: 14px;
}

.brief-name {
  margin: 0;
  /* An iOS large title, one step up because it is lying on a photograph. */
  font-size: 34px;
  line-height: 1.06;
  font-weight: 700;
  letter-spacing: -0.032em;
  text-wrap: balance;
  text-shadow: 0 2px 22px rgba(0, 0, 0, 0.6);
  animation: briefRise 520ms var(--ease-out) both;
  animation-delay: 120ms;
}

.brief-sub {
  margin: 3px 0 0;
  font-size: 14.5px;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.74);
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.7);
  animation: briefRise 520ms var(--ease-out) both;
  animation-delay: 180ms;
}

@keyframes briefRise {
  from { opacity: 0; transform: translateY(11px); }
  to   { opacity: 1; transform: none; }
}

/* ── the facts ─────────────────────────────────────────────────────────── */

.brief-body {
  padding: 16px 16px 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.brief-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  animation: briefRise 500ms var(--ease-out) both;
  animation-delay: 230ms;
}

.brief-chip {
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: -0.005em;
  padding: 5px 11px;
  border-radius: 999px;
  background: rgba(118, 118, 128, 0.24);
  color: rgba(235, 235, 245, 0.82);
}

.brief-chip.adult {
  background: color-mix(in oklab, #FF453A 24%, transparent);
  color: #FF9F9A;
}

.brief-facts {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* An inset grouped row, the same shape the rest of the app uses. */
.brief-card {
  background: var(--bg-elev);
  border-radius: 13px;
  padding: 12px 15px 13px;
  animation: briefRise 520ms var(--ease-out) both;
  /* Cascades in reading order; --i is set per card in JS. */
  animation-delay: calc(280ms + var(--i, 0) * 60ms);
}

.brief-label {
  display: block;
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--label-2);
  margin-bottom: 3px;
}

.brief-text {
  margin: 0;
  font-size: 15.5px;
  line-height: 1.42;
  letter-spacing: -0.012em;
  color: var(--label);
  /* Their appearance runs long and every word of it is real detail, so it
     is never clamped - this is the screen that exists to show it. */
}

/* ── controls ──────────────────────────────────────────────────────────── */

/* Floats over the photo rather than sitting in a nav bar: there is no bar
   here, and a chrome-less back button is what Photos and Music both do. */
.brief-back {
  position: absolute;
  z-index: 6;
  top: calc(env(safe-area-inset-top, 0px) + 12px);
  left: 12px;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  padding: 0 2px 0 0;
  appearance: none;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.34);
  color: #fff;
  cursor: pointer;
  animation: briefRise 400ms var(--ease-out) both;
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .brief-back {
    background: rgba(0, 0, 0, 0.20);
    backdrop-filter: blur(14px) saturate(150%);
    -webkit-backdrop-filter: blur(14px) saturate(150%);
  }
}

.brief-back:active { transform: scale(0.92); }

.brief-bar {
  position: absolute;
  z-index: 6;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(180deg, rgba(0, 0, 0, 0), #000 42%);
  animation: briefRise 460ms var(--ease-out) both;
  animation-delay: 300ms;
}

.brief-go {
  width: 100%;
  appearance: none;
  border: 0;
  border-radius: 13px;
  background: var(--tint);
  color: #fff;
  font-family: var(--font);
  font-size: 17px;
  font-weight: 600;
  padding: 14px;
  cursor: pointer;
  transition: transform 140ms var(--ease-out);
}

.brief-go:active { transform: scale(0.975); }

@media (prefers-reduced-motion: reduce) {
  .brief,
  .brief-photo,
  .brief-name,
  .brief-sub,
  .brief-chips,
  .brief-card,
  .brief-back,
  .brief-bar { animation: none; }
}

.brief-status {
  margin: -4px 0 0;
  font-size: 13px;
  letter-spacing: -0.01em;
  color: var(--label-3);
  animation: briefRise 500ms var(--ease-out) both;
  animation-delay: 255ms;
}

@media (prefers-reduced-motion: reduce) {
  .brief-status { animation: none; }
}

/* Policy links, at signup and at the point of purchase. Small, but they have
   to be present and they have to resolve - an adult-payments underwriter
   checks both. */
.gate-agree {
  margin: 14px 0 0;
  font-size: 12px;
  line-height: 1.5;
  text-align: center;
  color: var(--label-3);
}

.gate-agree a { color: var(--label-2); text-underline-offset: 2px; }
.gate-agree a:hover { color: var(--label); }

.cr-legal {
  margin: 18px 0 26px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--label-3);
}

.cr-legal a { color: var(--tint); text-decoration: none; }
.cr-legal a:hover { text-decoration: underline; }

/* ══════════════════════════════════════════════════════════════════════════
   Who is waiting, behind the sign-in form

   A form on an empty field is a wall. The same six faces from the marketing
   site drifting behind the glass turn it into a door - you can see what is
   on the other side, which is the entire reason anyone is filling this in.

   Heavily dimmed and blurred on purpose: this is presence, not content. It
   must never compete with the field somebody is typing into.
   ══════════════════════════════════════════════════════════════════════════ */

.gate-faces {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  display: grid;
  place-items: center;
  pointer-events: none;
  -webkit-mask-image: radial-gradient(58% 46% at 50% 50%, transparent 30%, #000 78%);
  mask-image: radial-gradient(58% 46% at 50% 50%, transparent 30%, #000 78%);
}

.gate-face-row {
  display: flex;
  gap: 18px;
  width: max-content;
  animation: gateDrift 90s linear infinite;
}

.gate-face-row img {
  width: 160px;
  height: 200px;
  object-fit: cover;
  object-position: center 22%;
  border-radius: 20px;
  opacity: 0.34;
  filter: blur(1.5px) saturate(0.85);
}

@keyframes gateDrift {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* The card sits above them and has to stay readable over a photograph, so
   the glass gets darker here than anywhere else in the app. */
.gate-card { z-index: 2; }

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .gate-card {
    background: rgba(8, 10, 16, 0.72);
    backdrop-filter: blur(26px) saturate(150%);
    -webkit-backdrop-filter: blur(26px) saturate(150%);
  }
}

/* The promise, at the moment of commitment. Somebody weighing up whether to
   type an email into an adult site wants to know it costs nothing first. */
.gate-free {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  align-self: center;
  margin: 2px 0 14px;
  padding: 6px 13px;
  border-radius: 999px;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--tint);
  background: color-mix(in oklab, var(--tint) 15%, transparent);
  border: 1px solid color-mix(in oklab, var(--tint) 34%, transparent);
}

.gate-free::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--tint);
  box-shadow: 0 0 8px var(--tint);
}

@media (max-width: 560px) {
  .gate-face-row img { width: 118px; height: 148px; }
}

@media (prefers-reduced-motion: reduce) {
  .gate-face-row { animation: none; }
}
