/* ==========================================================================
   Deeper Than Your Skills

   One chat transcript, composer pinned to the bottom, nothing above the
   thread. Tokens and component idioms lifted from known.thegirlieplaybook.com
   so this reads as the same house: 2px radii, hairline rules, no box shadows
   in the content, mono labels, Fraunces on the things that matter.
   ========================================================================== */

:root {
  --cream: #F7F5F0;
  --cream-warm: #FAF8F4;
  --ink: #241E26;
  --plum: #4A2244;
  --plum-light: #6E3A63;
  --gold: #C4A548;
  --gold-deep: #B08D46;
  --line: rgba(36, 30, 38, 0.14);

  --serif: 'Fraunces', Georgia, serif;
  --mono: 'IBM Plex Mono', monospace;
  --sans: 'Inter', system-ui, sans-serif;

  --column: 480px;
  --frame-w: 402px;
  --frame-h: 858px;
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--cream);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

:focus-visible {
  outline: 2px solid var(--gold-deep);
  outline-offset: 2px;
}

/* ── Shell ─────────────────────────────────────────────────────────────
   On mobile .stage and .frame are inert wrappers and .app is the whole
   screen. The phone frame is a desktop-only device. */

.stage { min-height: 100dvh; }

.app {
  display: flex;
  flex-direction: column;
  height: 100dvh;
  background: var(--cream);
  overflow: hidden;
}

/* ── Header ────────────────────────────────────────────────────────────
   A flex sibling of the scroller, so it holds the top of the column without
   position: fixed and without the thread having to reserve room for it. On
   desktop that puts it inside the phone frame, clipped by its corners. */

.topbar {
  flex: 0 0 auto;
  background: var(--plum);
  /* Cream at low opacity. The bar and the thread are different grounds, and
     this finishes the bottom edge rather than letting it just stop. */
  border-bottom: 1px solid rgba(247, 245, 240, 0.16);
  padding: 13px 18px 12px;
}

.topbar-inner {
  max-width: var(--column);
  margin: 0 auto;
  text-align: center;
}

.topbar-eyebrow {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 3px;
}

.topbar-title {
  font-family: var(--serif);
  font-weight: 500;
  font-optical-sizing: auto;
  font-size: 19px;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--cream);
}

/* ── Transcript ────────────────────────────────────────────────────── */

.transcript {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  /* Deliberately not smooth. Appends land one on top of another and a queued
     glide from the last one drags the next one off target. The bubbles have
     their own entrance; the scroll itself should just be there. */
  padding: 26px 18px 14px;
}

.thread {
  max-width: var(--column);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* Every append scrolls deliberately, so the browser re-anchoring on its own
     only fights us. Left on, it drags the result card back off the top. */
  overflow-anchor: none;
}

.turn {
  display: flex;
  animation: rise 340ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* Squared off, not soft. Nothing in here goes above 12px. */
.bubble {
  max-width: 86%;
  padding: 13px 16px;
  border-radius: 10px;
  font-size: 16.5px;
  line-height: 1.55;
  white-space: pre-wrap;
}

/* Her guide, on the left, in cream. */
.turn-guide { justify-content: flex-start; }
.turn-guide .bubble {
  background: var(--cream-warm);
  border: 1px solid var(--line);
  color: var(--ink);
}

/* Her, on the right, in plum. */
.turn-her { justify-content: flex-end; }
.turn-her .bubble {
  background: var(--plum);
  color: var(--cream);
  border: 1px solid var(--plum);
  overflow-wrap: anywhere;   /* long dictated words and email addresses */
}

/* Anything that went through the markdown pass carries its own block
   structure, so pre-wrap would double every gap it already has. */
.bubble.is-rich { white-space: normal; }

.bubble.is-rich p { margin-bottom: 10px; }
.bubble.is-rich p:last-child { margin-bottom: 0; }

.bubble.is-rich strong { font-weight: 600; }

/* Hanging bullets, so the text of a long option stays in one column instead of
   wrapping back under the dot. */
.bubble.is-rich ul {
  list-style: none;
  margin: 10px 0;
  padding: 0;
}
.bubble.is-rich ul:first-child { margin-top: 0; }
.bubble.is-rich ul:last-child { margin-bottom: 0; }

.bubble.is-rich li {
  position: relative;
  padding-left: 17px;
  margin-bottom: 9px;
}
.bubble.is-rich li:last-child { margin-bottom: 0; }
.bubble.is-rich li::before {
  content: '';
  position: absolute;
  left: 3px;
  top: 0.62em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--gold-deep);
}

/* The close, under the card. An ordinary guide message with paragraphs. */
.bubble.is-close { white-space: normal; }
.bubble.is-close p {
  font-size: 15.5px;
  line-height: 1.55;
  margin-bottom: 11px;
}
.bubble.is-close p:last-child { margin-bottom: 0; }
.bubble.is-close a {
  color: var(--plum);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Three dots while the guide is composing */
.thinking {
  display: flex;
  gap: 5px;
  align-items: center;
  height: 30px;
  padding: 0 4px;
}
.thinking span {
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--gold-deep);
  animation: pulse 1.25s ease-in-out infinite;
}
.thinking span:nth-child(2) { animation-delay: 0.16s; }
.thinking span:nth-child(3) { animation-delay: 0.32s; }

@keyframes pulse {
  0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
  30%           { opacity: 1;    transform: translateY(-3px); }
}

/* ── Chips ─────────────────────────────────────────────────────────────
   They sit where her reply will land, because that is what they become. */

.chips {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  margin-top: 2px;
  animation: rise 340ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

.chip {
  max-width: 88%;
  text-align: left;
  font-family: var(--sans);
  font-size: 15.5px;
  line-height: 1.4;
  color: var(--plum);
  background: transparent;
  border: 1px solid rgba(74, 34, 68, 0.4);
  border-radius: 8px;
  padding: 11px 15px;
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease;
}
.chip:hover {
  background: rgba(74, 34, 68, 0.06);
  border-color: var(--plum);
}

/* ── The result: the three lines she came for ──────────────────────── */

.result-card {
  background: var(--plum);
  border-top: 2px solid var(--gold);
  border-radius: 12px;
  padding: 24px 22px;
  margin-top: 4px;
  scroll-margin-top: 16px;
  /* A fade, not a rise. revealCard measures this element's own box to put it
     at the top of the view, and a transform on it moves that box for the whole
     length of the animation, so the scroll converges on a target that is still
     sliding. The children carry the entrance instead. */
  animation: fade 480ms ease both;
}

/* There is only ever one card. A refinement rebuilds these rows inside it,
   and the fade is what makes an in-place edit read as a change rather than as
   text that was always there. */
.result-line {
  margin-bottom: 20px;
  animation: fade 420ms ease both;
}
.result-line:last-child { margin-bottom: 0; }

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

.result-key {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 7px;
}

.result-val {
  font-family: var(--serif);
  font-weight: 400;
  font-optical-sizing: auto;
  font-size: 19px;
  line-height: 1.34;
  letter-spacing: -0.005em;
  color: var(--cream);
}

/* The brand mark on the thing she screenshots. The header bar is above the
   scroll and never makes it into a shot of the card alone, so the card carries
   its own. Same mono caps as the field labels, smaller and dimmer, because it
   is a watermark and it must not read as a fourth field. */
.result-mark {
  margin-top: 22px;
  text-align: right;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.18em;
  line-height: 1;
  text-transform: uppercase;
  color: rgba(196, 165, 72, 0.55);
}

/* Bought scroll room under the close so the card can sit at the top of the
   view. Empty by design, and always the last child of the thread. */
.spacer { flex: 0 0 auto; }

/* Malformed response fallback: render the raw text rather than an error. It
   stays inside the card, so this is a plainer card and never a lost one. */
.result-raw {
  font-size: 16.5px;
  line-height: 1.55;
  color: var(--cream);
}
.result-raw p { margin-bottom: 10px; }
.result-raw p:last-child { margin-bottom: 0; }

/* Under the close, once, at the very end. */
.credit {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  line-height: 1.45;
  color: rgba(36, 30, 38, 0.5);
  padding: 0 2px;
  animation: rise 340ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

/* ── Composer ──────────────────────────────────────────────────────── */

.composer {
  flex: 0 0 auto;
  border-top: 1px solid var(--line);
  background: var(--cream-warm);
  padding: 12px 18px calc(12px + env(safe-area-inset-bottom));
}

.composer-inner {
  max-width: var(--column);
  margin: 0 auto;
}

.composer-row {
  display: flex;
  align-items: flex-end;
  gap: 10px;
}

.composer-field {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  min-height: 46px;
  max-height: 132px;
  resize: none;
  font-family: var(--sans);
  font-size: 16px; /* 16px or iOS zooms the viewport on focus */
  line-height: 1.4;
  color: var(--ink);
  background: #FFFFFF;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 13px 15px;
}
.composer-field::placeholder { color: rgba(36, 30, 38, 0.38); }
.composer-field:focus-visible { border-color: var(--gold-deep); }

.send {
  flex: 0 0 auto;
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  color: var(--ink);
  background: var(--gold);
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: background 140ms ease;
}
.send svg { width: 21px; height: 21px; }
.send:hover { background: var(--gold-deep); }

.composer-note {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  line-height: 1.45;
  color: rgba(36, 30, 38, 0.5);
  margin-top: 9px;
}

/* Locked covers the opening sequence, every turn in flight, and the end of
   the session. The bar keeps its shape either way, so nothing jumps while
   she is trying to take a screenshot. */
.composer.is-locked .composer-field,
.composer.is-locked .send {
  opacity: 0.45;
  pointer-events: none;
}

/* At the end the field carries the last word, so it has to stay readable
   through the lock. */
.composer[data-mode='done'] .composer-field {
  opacity: 1;
  background: transparent;
  border-color: transparent;
  color: rgba(36, 30, 38, 0.55);
}
.composer[data-mode='done'] .composer-field::placeholder {
  color: rgba(36, 30, 38, 0.55);
}

/* ==========================================================================
   Desktop: the same chat column, inside a phone frame on the animated room.
   Everything below this line is desktop-only, including the image fetch.
   ========================================================================== */

@media (min-width: 1024px) {
  body { overflow: hidden; }

  .stage {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #1B0F1A;
  }

  .bg { position: absolute; inset: 0; overflow: hidden; }

  .bg-image {
    position: absolute;
    inset: -3%;
    background-image: url('/assets/background.webp');
    background-size: cover;
    background-position: center;
    filter: blur(2.5px);
    transform: scale(1.06);
    will-change: transform;
    animation: kenburns 56s ease-in-out infinite alternate;
  }

  /* Slower than feels right. If it reads as movement, it is too fast. */
  @keyframes kenburns {
    from { transform: scale(1.06) translate3d(-9px, -6px, 0); }
    to   { transform: scale(1.06) translate3d(9px, 6px, 0); }
  }

  /* Rain, masked to the middle and right window panes. The left pane is where
     the woman sits, and rain across her face breaks the whole illusion.
     The clip box stays put; the gradient layer inside it is what moves, and it
     is one tile taller than the box so the band is never partly empty. */
  .bg-rain {
    position: absolute;
    left: 40%; right: 14%; top: 0; height: 62%;
    overflow: hidden;
  }
  .bg-rain::before {
    content: '';
    position: absolute;
    left: 0; right: 0; top: -190px; bottom: 0;
    background-image: repeating-linear-gradient(
      101deg,
      transparent 0 7px,
      rgba(214, 226, 246, 0.085) 7px 8px,
      transparent 8px 23px,
      rgba(214, 226, 246, 0.05) 23px 24px,
      transparent 24px 41px
    );
    background-size: 100% 190px;
    will-change: transform;
    animation: rainfall 1.5s linear infinite;
  }
  @keyframes rainfall {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(0, 190px, 0); }
  }

  .bg-twinkle { position: absolute; inset: 0; }
  .bg-twinkle i {
    position: absolute;
    display: block;
    border-radius: 1px;
    will-change: opacity;
    animation-name: twinkle;
    animation-iteration-count: infinite;
    animation-timing-function: ease-in-out;
  }
  @keyframes twinkle {
    0%, 100% { opacity: var(--lo); }
    50%      { opacity: var(--hi); }
  }
  /* A few windows go dark and stay dark for a while. That is what makes it
     read as people rather than decoration. */
  @keyframes blackout {
    0%, 38%   { opacity: var(--hi); }
    44%, 78%  { opacity: 0; }
    86%, 100% { opacity: var(--hi); }
  }

  .bg-glow {
    position: absolute;
    border-radius: 50%;
    will-change: opacity;
  }
  .bg-glow-lamp {
    left: 36.5%; top: 43%;
    width: 12%; height: 16%;
    background: radial-gradient(circle, rgba(255, 197, 112, 0.34) 0%, rgba(255, 197, 112, 0) 68%);
    animation: breathe 7.5s ease-in-out infinite;
  }
  .bg-glow-candle {
    left: 86.5%; top: 84%;
    width: 6%; height: 9%;
    background: radial-gradient(circle, rgba(255, 178, 92, 0.42) 0%, rgba(255, 178, 92, 0) 66%);
    animation: flicker 2.3s ease-in-out infinite;
  }
  @keyframes breathe {
    0%, 100% { opacity: 0.72; }
    50%      { opacity: 1; }
  }
  @keyframes flicker {
    0%, 100% { opacity: 0.62; }
    18%      { opacity: 1; }
    34%      { opacity: 0.72; }
    57%      { opacity: 0.95; }
    76%      { opacity: 0.66; }
  }

  /* Fine noise, tiled from a data URI. Ties the composited layers together
     so they stop looking pasted on. */
  .bg-grain {
    position: absolute;
    inset: -60px;
    opacity: 0.05;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
    will-change: transform;
    animation: grain 900ms steps(4) infinite;
  }
  @keyframes grain {
    0%   { transform: translate3d(0, 0, 0); }
    25%  { transform: translate3d(-14px, 8px, 0); }
    50%  { transform: translate3d(9px, -11px, 0); }
    75%  { transform: translate3d(-6px, -5px, 0); }
    100% { transform: translate3d(0, 0, 0); }
  }

  /* Static. Exists before anything moves, and does the work of making the
     frame readable against every part of the image. */
  .bg-scrim {
    position: absolute;
    inset: 0;
    background:
      radial-gradient(ellipse 78% 72% at 50% 48%, rgba(20, 8, 18, 0) 42%, rgba(20, 8, 18, 0.62) 100%),
      linear-gradient(rgba(74, 34, 68, 0.30), rgba(74, 34, 68, 0.30));
  }

  /* Right of center, so the woman, lamp and wine glass stay clear on the
     left and enough city shows for the twinkle to read. Lifted slightly so
     the rug shows beneath. */
  .frame {
    position: relative;
    width: var(--frame-w);
    height: min(var(--frame-h), 86dvh);
    margin-left: clamp(180px, 20vw, 300px);
    margin-top: -20px;
    background: #17101A;
    border-radius: 44px;
    padding: 11px;
    box-shadow:
      0 2px 4px rgba(0, 0, 0, 0.5),
      0 34px 70px -14px rgba(0, 0, 0, 0.72),
      0 0 0 1px rgba(255, 255, 255, 0.07) inset;
  }

  .frame .app {
    height: 100%;
    border-radius: 34px;
    overflow: hidden;
  }

  .topbar { padding: 15px 22px 13px; }
  .transcript { padding: 26px 22px 14px; }
  .composer { padding: 14px 22px 18px; }
}

@media (min-width: 1024px) and (max-width: 1320px) {
  .frame { margin-left: 20vw; }
}

/* Short desktop windows: give the chrome less to work with, not the content. */
@media (min-width: 1024px) and (max-height: 780px) {
  .frame { border-radius: 34px; padding: 8px; margin-top: -10px; }
  .frame .app { border-radius: 27px; }
}

/* ── Motion and battery ────────────────────────────────────────────── */

/* Pause everything while the tab is hidden. This is decoration and it should
   not burn a laptop battery in a background tab. */
body.is-hidden .bg-image,
body.is-hidden .bg-rain::before,
body.is-hidden .bg-grain,
body.is-hidden .bg-glow,
body.is-hidden .bg-twinkle i {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  /* The still plus the static vignette has to still look finished. */
  .bg-image { transform: scale(1.02); }
  .bg-rain, .bg-grain { display: none; }
  .bg-glow-lamp { opacity: 0.85; }
  .bg-glow-candle { opacity: 0.8; }
  .bg-twinkle i { opacity: var(--hi); }
}
