/* ===== SkyOS PWA — design system ===== */

@font-face {
  font-family: 'Cormorant Garamond';
  src: url('fonts/CormorantGaramond-Variable.ttf') format('truetype');
  font-weight: 300 700;
  /* swap, with the preload in index.html doing the real work.
     optional was tried and reverted: it decides at first paint and, if the font
     misses its ~100ms window, keeps the fallback for the entire page life — so a
     brief flash became a permanently wrong face on any slow load. swap always
     arrives at the right font; the preload makes the window it swaps in short
     enough that there is usually nothing to see. */
  font-display: swap;
}

:root {
  /* Brand palette — the SkyOS style guide, verbatim. */
  --white: #FFFFFF;        /* White         */
  --cloud-white: #F4F9FC;  /* Cloud White   */
  --soft-sky: #E5F2FF;     /* Soft sky      */
  --accent: #0A66C2;       /* Sky Blue      */
  --deep-sky: #053666;     /* Deep Sky      */
  --ink: #0E1E2C;          /* Midnight navy */
  --ink-2: #556B82;        /* Blue Gray     */
  /* Blue Gray's channels, so the greys below can be derived from it rather than
     invented. Same trick --fade-rgb already uses: CSS cannot pull components
     out of a hex, so the triplet has to be its own token. */
  --ink-2-rgb: 85, 107, 130;
  /* Sky Blue's channels, so it can be used at opacity the way --ink-2 already
     is. Every other accent wash in the file hardcodes "10, 102, 194". */
  --accent-rgb: 10, 102, 194;
  /* The hover wash's base: Sky Blue pulled most of the way toward Blue Gray.
     Straight Sky Blue at 3% still read as a blue tint against a row that is
     itself nearly white; straight Blue Gray read as plain grey in a UI whose
     other light surfaces all lean blue. This is the point between them, and it
     is a mix of two palette colours rather than a new one. */
  --hover-base: color-mix(in srgb, var(--accent) 45%, var(--ink-2));

  /* Sky Blue lifted toward White. The orb is Sky Blue at rest, this while it
     thinks and Deep Sky while it speaks, so the only requirement is that it
     read as clearly lighter than the other two. Derives in dark too: --accent
     changes there and this follows it, which is why dark no longer overrides
     it by hand. */
  --accent-soft: #4F91D3;
  --accent-soft: color-mix(in srgb, var(--accent) 70%, var(--white));
  --accent-wash: var(--soft-sky);
  /* Accent-coloured TEXT, as opposed to Deep Sky the fill. The two were one
     token, which works while the page is light and inverts the moment it is
     not: text on the accent wash has to move with the theme, a fill sitting
     under white type must not. Light keeps the Deep Sky value exactly. */
  --accent-ink: var(--deep-sky);
  /* Blue Gray at 72%. Captions, dates, placeholder text — the tier below
     --ink-2, which was #8A9BAD, a colour that appears nowhere in the guide. */
  --ink-3: rgba(var(--ink-2-rgb), 0.72);
  --bg-top: var(--cloud-white);
  --bg-bottom: var(--white);
  /* The welcome fade is built from these as rgba stops, so it has to be the
     background colour as RGB components rather than a hex token. */
  --fade-rgb: 244, 249, 252;
  --fade-end: var(--white);
  /* A slightly tinted surface: active rows, small menus, file chips. Named by
     role rather than colour so dark mode can give it a real value instead of
     inheriting a light one. */
  --raised: var(--cloud-white);
  /* Top of the mascot oval — Soft Sky pulled a little toward Sky Blue, so the
     plate reads as sky rather than a flat swatch. That is now what it says
     rather than a hex someone eyedropped. */
  --oval-top: #D3E8FF;
  --oval-top: color-mix(in srgb, var(--soft-sky) 92%, var(--accent));
  --surface: var(--white);
  /* Blue Gray at 16%. As an alpha it composites correctly on white cards and on
     Cloud White alike, which a fixed hex never did. */
  --separator: rgba(var(--ink-2-rgb), 0.16);
  /* The guide has no red, and an error has to be red — so there is exactly one,
     and everything else is derived from it. Chosen from the four that were in
     the file because it is the only one that clears 4.5:1 on white, and most of
     its uses are small text. */
  --danger: #C0392B;
  --danger-soft: color-mix(in srgb, var(--danger) 28%, var(--white));
  --danger-wash: color-mix(in srgb, var(--danger) 10%, var(--white));
  --shadow: rgba(14, 30, 44, 0.12);
  --shadow-strong: rgba(14, 30, 44, 0.22);

  /* Everything on desktop is drawn 10% larger. zoom is the only property that
     scales a px-built layout wholesale — rem would need every value rewritten,
     and transform:scale leaves the element occupying its old box.
     The catch is that viewport units do NOT scale with zoom: they resolve
     against the real viewport and are then multiplied by it, so 100dvh inside a
     1.1 zoom is 110% of the screen. Every viewport-derived length in the zoomed
     subtree is therefore divided by this. Change the number here and the whole
     desktop UI moves with it. */
  --ui-scale: 1;

  /* 10px corner radius everywhere; no strokes. */
  --r: 10px;

  /* Frame-accurate margins (402x874 reference). The chat buttons sit at y=66 in
     frame 1393:3326, and that frame has NO status bar in it — 402x874 is the
     app area, not the whole phone. So 67px is measured from the top of the app,
     which is already below the system clock/battery bar. Do not "correct" this
     for the status bar; it is not double-counted. */
  --top-margin: max(67px, calc(env(safe-area-inset-top) + 8px));
  --bottom-margin: max(25px, calc(env(safe-area-inset-bottom) + 12px));

  --serif: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
  --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

/* Installed, the system paints a status bar ABOVE the app on Android, and the
   Figma frame does not include one — so the design's 66px lands ~104px below
   the top of the phone and reads as far too low.
   14px is the gap below the status bar, chosen by eye on device after trying
   67, 46, 36, 28 and 20. It is not derived from the frame; the frame's origin
   and the viewport's origin are simply different things here.
   The max() makes one line cover every target, because env() reports 0 only
   where the app cannot paint under the bar:
     Android PWA   inset 0   -> 14px below the status bar
     Android native inset ~38 -> ~46px from the top of the screen
     iOS native     inset ~59 -> ~67px from the top of the screen
   The +8px is the tuning knob, and it moves both native platforms together. */
@media (display-mode: standalone), (display-mode: fullscreen), (display-mode: minimal-ui) {
  :root { --top-margin: max(14px, calc(env(safe-area-inset-top) + 8px)); }
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  /* The app is a fixed-size shell; the document itself must never scroll. Any
     few-pixel difference between 100dvh and the real viewport otherwise gives
     Android a scrollbar and lets the whole UI be dragged up and down.
     overscroll-behavior also kills the rubber-band and pull-to-refresh. */
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: manipulation;
  /* Matches the app surface so no seam shows between the status bar and the
     first painted pixel. The desktop device-frame backdrop is restored in the
     min-width:480px block below. */
  background: var(--bg-top);
  font-family: var(--sans);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

button { font-family: inherit; cursor: pointer; border: none; background: none; }
input { font-family: inherit; }

/* Phone shell — full-bleed on mobile, a centered device on desktop */
#app {
  position: relative;
  width: 100%;
  max-width: 440px;
  margin: 0 auto;
  height: 100dvh;
  overflow: hidden;
  background: linear-gradient(180deg, var(--bg-top), var(--bg-bottom));
}
@media (min-width: 480px) {
  body { display: flex; align-items: center; justify-content: center; background: rgba(var(--ink-2-rgb), 0.22); }
  #app {
    height: min(920px, 96dvh);
    border-radius: 42px;
    box-shadow: 0 30px 80px rgba(14, 30, 44, 0.30);
    border: 10px solid #0f1720;
    overflow: hidden;
  }
}

/* ===== Screens & transitions ===== */
.screen {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  padding: var(--top-margin) 0 var(--bottom-margin);
}
/* No whole-screen fade.
   Nothing inside #app paints a background — the page colour comes from <body> —
   so fading the screen from opacity 0 did not cross-fade between two views, it
   flashed the bare page colour behind the entire UI on every navigation. There
   is no "previous screen" underneath to fade from; the DOM is replaced in one
   go. Overlays, drawers and popovers keep their animations: those genuinely
   arrive over something that stays put. */
/* Onboarding question screens sit lower — their progress pill is at y=97. */
.screen.onboard { padding-top: max(97px, calc(env(safe-area-inset-top) + 38px)); }
.screen.glow::before {
  content: '';
  position: absolute;
  left: 50%; top: 60%;
  width: 460px; height: 460px;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(62, 142, 222, 0.12), transparent 62%);
  pointer-events: none;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeout { from { opacity: 1; } to { opacity: 0; } }

/* A re-render that stays on the same screen must not replay the fade — that is
   what made every button press look like a whole new screen. */
.screen.no-anim,
.drawer.no-anim, .popover.no-anim, .proj-menu.no-anim, .scrim.no-anim, .dt-modal.no-anim { animation: none; }

.pad { padding-left: 16px; padding-right: 16px; }
.spacer { flex: 1; }
.col { display: flex; flex-direction: column; }
.center { align-items: center; text-align: center; }

/* ===== Typography ===== */
.hero { font-family: var(--serif); font-weight: 600; font-size: 44px; line-height: 1.08; color: var(--ink); text-align: center; }
.title { font-family: var(--serif); font-weight: 600; font-size: 34px; line-height: 1.12; color: var(--ink); text-align: center; }
.title-lg { font-size: 40px; }
.subtitle { font-size: 16px; font-weight: 500; color: var(--ink-2); text-align: center; line-height: 1.5; }

/* ===== Buttons ===== */
.btn {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 56px;
  border-radius: 10px;
  background: var(--accent);
  color: #fff; font-size: 24px; font-weight: 600;
  box-shadow: 0 4px 7px rgba(14, 30, 44, 0.2);
  transition: transform 0.12s ease, opacity 0.12s ease;
}
.btn:active { transform: scale(0.98); opacity: 0.92; }
.btn[disabled] { opacity: 0.5; box-shadow: none; cursor: default; }

/* Continue with trailing arrow (node 1341:3337). */
.btn-arrow { position: relative; }
.btn-arrow .arw { position: absolute; right: 21px; top: 50%; transform: translateY(-50%); display: flex; color: #fff; }

/* "Tell us about yourself" — free text that seeds the global notes. */
.about-field {
  width: 100%; height: 170px; resize: none;
  padding: 16px; border: none; outline: none;
  background: var(--surface); border-radius: var(--r);
  box-shadow: 0 0 4px var(--shadow);
  font-family: var(--sans); font-size: 16px; font-weight: 300;
  line-height: 1.45; color: var(--ink);
}
.about-field::placeholder { color: var(--ink-2); font-weight: 300; }
.about-field:focus { box-shadow: 0 0 0 2px rgba(10, 102, 194, 0.35); }

.textbtn { color: var(--ink-2); font-size: 16px; font-weight: 500; padding: 8px; align-self: center; }
.textbtn.skip { font-family: var(--sans); font-size: 20px; font-weight: 300; color: var(--ink-2); }
.textbtn.underline { text-decoration: underline; }

/* ===== Fields ===== */
.field {
  display: flex; align-items: center; gap: 12px;
  height: 52px; padding: 0 16px;
  background: var(--surface);
  border: none;
  border-radius: var(--r);
  box-shadow: 0 0 4px var(--shadow);
}
.field:focus-within { box-shadow: 0 0 0 2px rgba(10, 102, 194, 0.35); }
.field .ico { color: var(--ink-2); display: flex; }
.field input {
  flex: 1; border: none; outline: none; background: none;
  font-size: 16px; color: var(--ink); min-width: 0;
}
.field input::placeholder { color: var(--ink-3); }
.field .eye { color: var(--ink-2); display: flex; }

/* ===== Card / pill / dots ===== */
.card {
  background: var(--surface);
  border: none;
  border-radius: 10px;
  box-shadow: 0 0 4px var(--shadow);
}
.pill {
  height: 34px; padding: 0 20px; border-radius: 999px;
  background: var(--accent-wash); color: var(--accent);
  font-size: 15px; font-weight: 600;
  transition: transform 0.12s ease;
}
.pill:active { transform: scale(0.95); }
.pill.filled { background: var(--accent); color: #fff; }

.dots { display: flex; gap: 8px; justify-content: center; }
.dot { width: 8px; height: 8px; border-radius: 999px; background: rgba(10, 102, 194, 0.25); transition: width 0.3s ease, background 0.3s ease; }
.dot.active { width: 20px; background: var(--accent); }

.progress { width: 180px; height: 8px; border-radius: 999px; background: rgba(10, 102, 194, 0.15); overflow: hidden; }
.progress > i { display: block; height: 100%; border-radius: 999px; background: var(--accent); transition: width 0.5s ease; }
.progress.wide { width: 240px; height: 6px; }

/* Onboarding step dots. Larger than a default dot on purpose — at 8px they
   read as decoration rather than a position in a sequence. */
.steps { display: flex; align-items: center; gap: 12px; }
.steps i {
  display: block;
  width: 12px; height: 12px;
  border-radius: 999px;
  background: rgba(10, 102, 194, 0.18);
  transition: background 0.3s ease;
}
.steps i.on { background: var(--accent); }
:root[data-theme="dark"] .steps i { background: rgba(62, 142, 222, 0.25); }

/* ===== Circle icon button ===== */
.circle-btn {
  width: 48px; height: 48px; border-radius: 999px;
  background: var(--surface); color: var(--ink);
  display: flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  box-shadow: 0 0 6px var(--shadow);
  transition: transform 0.12s ease;
}
.circle-btn:active { transform: scale(0.92); }

/* ===== Mascot ===== */
.mascot { display: block; }
.mascot .halo { }

/* ===== Login sheet ===== */
.sheet {
  margin-top: auto;
  background: var(--bg-top);
  border-radius: 28px 28px 0 0;
  box-shadow: 0 -6px 24px var(--shadow);
  padding: 24px 24px calc(env(safe-area-inset-bottom) + 28px);
  display: flex; flex-direction: column; gap: 16px;
}
/* 0.35s read as a jump rather than a slide. The curve is gentler too: the old
   one arrived at full speed and stopped dead, which is the "rough" part. */
.sheet-in { animation: sheetup 0.58s cubic-bezier(0.16, 0.84, 0.24, 1) both; }
@keyframes sheetup { from { transform: translateY(100%); } to { transform: translateY(0); }
}
.divider-row { display: flex; align-items: center; gap: 12px; }
.divider-row .ln { flex: 1; height: 1px; background: var(--separator); }
.divider-row span { font-size: 13px; color: var(--ink-2); }
.social-row { display: flex; justify-content: center; gap: 48px; }
.social { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.social .disc {
  width: 56px; height: 56px; border-radius: 999px; background: #fff;
  border: none; box-shadow: 0 0 6px var(--shadow);
  display: flex; align-items: center; justify-content: center;
}
.social span { font-size: 15px; color: var(--ink-2); }


/* 44px is the frame value at 402 wide. Below that 'Welcome to SkyOS!' wraps to
   two lines, and on a short Android viewport the second line pushed the button
   under the fold. clamp keeps the frame size where there is room and scales
   down where there is not. (The mockup this originally protected is gone; the
   wrapping problem it was measured against is not.) */
.onb-title {
  margin-top: 44px;
  font-family: var(--serif); font-weight: 600;
  font-size: 44px; line-height: 1.1;
  color: var(--ink); text-align: center;
}
/* Below ~395px 'Welcome to SkyOS!' does not fit on one line at 44px, and the
   second line steals the height the phone illustration needs. A step rather
   than a clamp: a clamp that reaches 44px at 402 leaves literally 0px of slack
   at 360, so the title wraps again the moment the webfont swaps in and the
   wider fallback is measured first. 38px leaves ~29px of room. */
@media (max-width: 395px) {
  .onb-title { font-size: 38px; }
}
/* 38px still wraps at 320 (288px of text in 288px of room). One more step for
   the smallest phones still in use. */
@media (max-width: 340px) {
  .onb-title { font-size: 33px; }
}
.onb-sub { margin-top: 8px; font-size: 16px; font-weight: 500; line-height: 1.5; color: var(--ink-2); text-align: center; }

/* Welcome (Figma 52:802). Stack is bottom-anchored: on an 874-tall frame the
   phone lands at y=147 exactly as drawn. */
.welcome-screen { justify-content: flex-end; align-items: center; padding: 0 16px; }
/* Was a fixed 260.5x514 from the frame, which simply does not fit a shorter or
   narrower phone. Now it takes the room that is left after the title, subtitle
   and button, keeping the frame's 1:1.973 ratio. min-height stops it
   collapsing to nothing on a very short screen. */
.welcome-illu {
  position: relative; overflow: hidden;
  width: min(260.5px, 68vw);
  aspect-ratio: 260.5 / 514;
  flex: 0 1 auto;
  min-height: 260px;
}
/* The phone drifts straight up and down — no rotation, no sideways movement.
   The image is 542.75 tall inside a 514 box, so there is ~28px of slack below
   it; the travel is negative-only for that reason. Going positive would pull
   the top edge down and open a gap above the device.
   The wrapper clips, so the bottom of the phone slides behind the fade instead
   of poking out of it. */
.welcome-illu img {
  display: block; position: absolute; left: 0;
  /* Parked 7px down so the float can swing both ways without either edge
     leaving the box. At top:0 the upswing pushed the device's top edge past
     the clip and sliced the top off the phone. */
  top: 7px;
  width: 100%; height: 105.6%;
  animation: welcome-device-float 6s ease-in-out infinite;
  will-change: transform;
}
@keyframes welcome-device-float {
  0%   { transform: translateY(7px); }
  50%  { transform: translateY(-7px); }
  100% { transform: translateY(7px); }
}
@media (prefers-reduced-motion: reduce) {
  .welcome-illu img { animation: none; }
}
/* The frame's fade is 330 of 874 tall — about 38% of the screen. At 117px of
   514 this was less than a quarter, so it went from clear to solid in half the
   distance and read as the phone being cut off rather than fading out.
   More stops as well: two stops on a long gradient band, which is what shows
   up as a hard edge on a phone screen. */
.welcome-fade {
  /* 318/514 of the frame — a percentage so it follows the illustration when it
     scales, instead of fading from a fixed pixel that no longer means anything. */
  position: absolute; left: 0; right: 0; top: 61.9%; bottom: 0;
  background: linear-gradient(180deg,
    rgba(var(--fade-rgb), 0) 0%,
    rgba(var(--fade-rgb), 0.18) 18%,
    rgba(var(--fade-rgb), 0.45) 36%,
    rgba(var(--fade-rgb), 0.72) 54%,
    rgba(var(--fade-rgb), 0.9) 70%,
    rgba(var(--fade-rgb), 0.98) 86%,
    var(--fade-end) 100%);
}
.welcome-cta { margin-top: 34px; padding-bottom: 44px; align-self: stretch; }

/* ===== Login sheet (Figma 931:4170) — welcome frame + scrim + rising sheet ===== */
/* No `position` here — .screen is already absolute/inset-0, and overriding it
   collapses the screen to content height, which strands the sheet mid-air. */
.login-screen { overflow: hidden; }
.login-scrim { position: absolute; inset: 0; background: rgba(14, 30, 44, 0.25); animation: fade 0.5s ease both; }
/* The login screen draws the SAME welcome background underneath, so replaying
   the whole-screen fade makes an identical picture flash in — which is what
   read as "a new screen rendering" behind the sheet. Only the scrim and the
   sheet should move. */
.screen.login-screen { animation: none; }
/* Half speed while the sheet is up: the phone is background at that point and
   should not pull the eye away from the form. */
.login-screen .welcome-illu img { animation-duration: 12s; }
.login-sheet {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: var(--bg-top);
  border-radius: var(--r) var(--r) 0 0;
  box-shadow: 0 -4px 40px rgba(0, 0, 0, 0.15);
  padding: 24px 16px calc(env(safe-area-inset-bottom) + 44px);
  display: flex; flex-direction: column;
  /* Frame 931:4170: the sheet sits at y=494 in an 874-tall screen. */
  min-height: 380px;
  /* The frame is ~690px tall. On a short phone that overflows past the top of
     the screen and clips the cloud and the title, so the sheet scrolls rather
     than growing off-screen. Bounded by the containing .screen, not by dvh:
     on desktop the phone shell is min(920px, 96dvh), so a viewport-based cap
     never binds and the sheet overflows the frame. */
  max-height: calc(100% - 24px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
/* Login — phone frame, node 931:4170 (402x874). The 1440-wide frame of the
   same name is the desktop layout and is deliberately NOT the source here.
   Offsets below are that frame's, measured from the sheet's own top (y=494). */
.login-sheet { gap: 0; }
.login-title {
  margin: 0;
  font-family: var(--serif); font-weight: 600; font-size: 36px; line-height: 1.5;
  color: #000; text-align: center;
}
/* Every row is 370 wide inside 16px margins, 10px radius, and carries the same
   soft drop shadow rather than a stroke. */
.login-google {
  display: flex; align-items: center; justify-content: center; gap: 17px;
  height: 45px; margin-top: 23px;
  background: var(--surface); border-radius: var(--r);
  box-shadow: 0 0 4px rgba(14, 30, 44, 0.15);
  font-size: 16px; font-weight: 400; color: #000;
}
.login-google svg, .login-google img { flex: 0 0 auto; display: block; }

.login-div { display: flex; align-items: center; gap: 10px; margin-top: 23px; }
.login-div i { flex: 1; height: 1px; background: rgba(85, 107, 130, 0.35); }
.login-div span { font-size: 12px; font-weight: 300; color: var(--ink-2); }

.login-sheet .lfield { margin-top: 21px; }

.login-cta {
  margin-top: 29px;
  height: 56px; border-radius: var(--r);
  font-size: 24px; font-weight: 600;
  box-shadow: 0 4px 7px rgba(14, 30, 44, 0.2);
  /* The label is a left-to-right #f4f9fc → white gradient, not flat white. */
  background-image: none;
  color: transparent;
  -webkit-background-clip: initial;
}
.login-cta span {
  background-image: linear-gradient(90deg, var(--cloud-white), var(--white));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}

.lfield {
  display: flex; align-items: center; gap: 13px;
  height: 45px; padding: 0 14px 0 9px;
  background: var(--surface); border-radius: var(--r);
  box-shadow: 0 0 4px rgba(14, 30, 44, 0.15);
}
.lfield .ico { display: flex; justify-content: center; width: 20px; flex: 0 0 auto; color: var(--ink); }
.lfield input {
  flex: 1; min-width: 0; border: none; outline: none; background: none;
  font-size: 16px; font-weight: 300; color: var(--ink);
}
.lfield input::placeholder { color: rgba(85, 107, 130, 0.8); font-weight: 300; }
.lfield .eye { display: flex; flex: 0 0 auto; color: var(--ink-2); }

.oauth-row {
  display: flex; align-items: center; justify-content: center; gap: 17px;
  height: 45px; margin-top: 26px;
  background: var(--surface); border-radius: var(--r);
  box-shadow: 0 0 4px rgba(14, 30, 44, 0.15);
  font-size: 16px; color: #000;
}
.login-sheet .spacer { min-height: 55px; }

/* ===== Integration rows ===== */
.src-row { display: flex; align-items: center; gap: 14px; padding: 12px; }
.src-row .txt { flex: 1; }
.src-name { font-size: 15px; font-weight: 400; color: var(--ink); }
.src-sub { font-size: 12px; color: var(--ink-2); }
.row-div { height: 1px; background: var(--separator); margin-left: 60px; }

/* ===== Paywall ===== */
.feat-head, .feat-row { display: flex; align-items: center; }
.feat-head { color: var(--ink-2); font-weight: 500; }
.feat-col { width: 50px; text-align: center; }
.feat-col.pro { color: var(--accent); font-weight: 600; width: 44px; }
.feat-row .label { flex: 1; display: flex; align-items: center; gap: 10px; font-size: 15px; color: var(--ink); }
.feat-div { height: 1px; background: var(--separator); }
.plan-card { position: relative; border: none; background: var(--accent-wash); border-radius: var(--r); padding: 18px; box-shadow: 0 0 4px var(--shadow); }
.plan-top { display: flex; justify-content: space-between; align-items: baseline; }
.plan-name { font-size: 18px; font-weight: 600; }
.plan-year { font-size: 15px; color: var(--ink-2); }
.plan-price { display: flex; align-items: baseline; gap: 10px; margin-top: 8px; }
.plan-price b { font-size: 24px; }
.plan-price s { font-size: 15px; color: var(--ink-2); }
.save-badge { position: absolute; top: -10px; left: 16px; background: var(--accent); color: #fff; font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 999px; }

/* ===== Chat ===== */
.topbar { display: flex; align-items: center; gap: 12px; padding: 0 16px; }
.topbar .proj-title { flex: 1; text-align: center; font-size: 17px; font-weight: 500; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upgrade { display: flex; align-items: center; gap: 6px; height: 40px; padding: 0 14px; border-radius: 999px; background: var(--surface); box-shadow: 0 2px 8px var(--shadow); color: var(--accent); font-weight: 600; font-size: 16px; }

/* overflow-x: hidden is the backstop — if any single message still manages to
   be too wide, it gets clipped here rather than widening the whole app. */
.transcript { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 27px 16px 28px; display: flex; flex-direction: column; gap: 36px; min-width: 0; -webkit-overflow-scrolling: touch; }
.transcript::-webkit-scrollbar { width: 0; }

/* Squared top-right corner, 10px elsewhere — matches the frame. No stroke. */
/* max-width was a fixed 280px from the 402-wide frame. On a narrower phone
   that alone can exceed the column, so it is capped against the container too. */
.msg-user {
  align-self: flex-end; max-width: min(280px, 84%);
  background: var(--surface); border: none;
  border-radius: var(--r) 0 var(--r) var(--r);
  padding: 12px 10px; font-size: 18px; line-height: 1.5; color: var(--ink);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.msg-ai { align-self: stretch; display: flex; flex-direction: column; gap: 11px; }
.avatar { width: 30px; height: 30px; }
/* A pasted URL is one long unbreakable word. Without this it pushes the whole
   page wider than the screen and the app scrolls sideways. */
.msg-user, .ai-text { overflow-wrap: anywhere; word-break: break-word; }

/* The row owns the right-alignment now; the bubble sizes inside it. Without
   this the actions would sit on their own transcript line, full width, with the
   icons stranded on the left. */
.msg-user-row {
  position: relative;
  align-self: flex-end;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  max-width: min(280px, 84%);
  min-width: 0;
}
.msg-user-row .msg-user { align-self: flex-end; max-width: 100%; }

/* Actions under your own message. Reserved space rather than display:none, so
   revealing them cannot reflow the conversation under the pointer. */
/* .actions is declared further down the file, so at equal specificity it wins
   on source order and its 20px gap survived. Doubling the class here is the
   fix that does not depend on where either rule sits. */
/* Out of the layout entirely.
   In flow, these added their own height below every message, which pushed the
   whole conversation down and moved the bubbles off where they used to sit —
   for a strip that is invisible until you hover. Absolute keeps the row exactly
   the height of the bubble, so nothing moves, and the icons hang into the gap
   that already exists between messages. */
.actions.user-actions {
  position: absolute;
  top: 100%;
  /* Spans the whole width of the message, not just the icons. The strip under
     the bubble is where the pointer already is on its way down to them, and at
     icon-width only that last 100px counted as hover — so they stayed hidden
     until you were almost on top of one. Invisible elements still take pointer
     events, which is what makes the empty part of the strip work. */
  left: 0;
  right: 0;
  justify-content: flex-end;
  margin-top: 2px;
  gap: 8px;
  opacity: 0;
  transition: opacity 0.14s ease;
}
/* Hovering the strip is enough on its own — the row above it is the bubble. */
.actions.user-actions:hover { opacity: 1; }
/* The buttons carry their own padding, so the row reads tight without the
   glyphs actually touching, and the hit target stays bigger than the icon. */
.actions.user-actions button { width: 30px; height: 30px; padding: 5px; border-radius: 6px; align-items: center; justify-content: center; }
.msg-user-row:hover .user-actions,
.user-actions:focus-within { opacity: 1; }

/* Tooltip. Its own element would need positioning logic on every button; a
   pseudo-element inherits the button's box for free. */
.user-actions button,
.tipped { position: relative; }
/* Tooltip colours are set per theme rather than from --ink/--bg-bottom.
   Those tokens invert, which in dark mode produced a bright white box on a
   dark page — correct by contrast (15:1) and far too loud for a hint.
   Dark mode instead lifts a surface out of the page, the way popovers already
   do. It cannot separate itself by contrast there — every dark tooltip colour
   measures 1.1–1.6 against #0F1B28, well under the 3:1 an edge needs — so the
   shadow and hairline are what define it, not the fill. */
.user-actions button::after,
.tipped::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 10px;
  border-radius: 8px;
  background: #0E1E2C;
  color: #FFFFFF;
  box-shadow: 0 4px 14px var(--shadow);
  font-size: 13px;
  line-height: 1;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.1s ease;
  z-index: 5;
}
:root[data-theme="dark"] .user-actions button::after,
:root[data-theme="dark"] .tipped::after {
  background: #243447;
  color: var(--ink);
  box-shadow: 0 4px 14px var(--shadow-strong), inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.user-actions button:hover::after,
.tipped:hover::after,
.tipped:focus-visible::after { opacity: 1; }
/* The sidebar is 84px wide, so a tip centred under an icon would be cut off by
   the pane edge. These sit to the RIGHT of the icon, vertically centred on it,
   and only while the sidebar is collapsed — expanded, the label is already
   there and a tooltip would just repeat it. */
#app.desk .dt-side .tipped::after { opacity: 0; }
#app.desk .dt-side.rail .tipped::after {
  top: 50%;
  left: calc(100% + 10px);
  transform: translateY(-50%);
}
#app.desk .dt-side.rail .tipped:hover::after,
#app.desk .dt-side.rail .tipped:focus-visible::after { opacity: 1; }
/* No hover on touch, so a tooltip there could only ever appear stuck. */
@media (hover: none) {
  .user-actions button::after { display: none; }
}

/* Editing in place. Matches the bubble's own measure so the text does not
   reflow the moment you start typing. */
.msg-user-row.editing { max-width: min(420px, 92%); width: 100%; }
.msg-edit-input {
  width: 100%;
  resize: vertical;
  padding: 12px 12px;
  border: 1px solid var(--accent);
  border-radius: var(--r);
  outline: none;
  font-family: var(--sans);
  font-size: 18px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--surface);
}
.msg-edit-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
.ai-text { font-size: 18px; color: var(--ink); line-height: 1.5; min-width: 0; }
.ai-text ul { margin: 12px 0; padding-left: 20px; margin-left: -10px; }
.ai-text li { margin: 12px 0; }
.ai-text p { margin: 12px 0; }
.ai-text p:first-child, .ai-text ul:first-child { margin-top: 0; }
.ai-text p:last-child, .ai-text ul:last-child { margin-bottom: 0; }
.ai-text b { font-weight: 600; }
/* Links in a reply. Underlined rather than colour-only: colour alone is not
   enough on its own to mark a link, and the accent here is close to the body
   ink. The offset keeps the rule off the descenders. */
/* Links sit in the body ink, not an accent colour. A reply that cites five
   sources would otherwise be five blue words competing with the sentence they
   are inside; the underline already says "link". Sky blue is held back for
   hover, so it marks the one you are actually pointing at.

   :visited is spelled out because the UA sheet turns it purple, and a purple
   word mid-paragraph reads as an error rather than as history. */
.ai-text .md-link,
.ai-text .md-link:visited {
  color: inherit;
  /* Dotted, so it stays quieter than the sentence it sits in — a solid rule at
     body ink reads almost like an underline for emphasis. */
  text-decoration: underline dotted;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  overflow-wrap: anywhere;
  transition: color 0.12s ease, text-decoration-color 0.12s ease;
}
.ai-text .md-link:hover,
.ai-text .md-link:focus-visible {
  color: var(--accent);
  text-decoration-color: currentColor;
}
/* A markdown "> " line. Was rendering the ">" as a literal character; now it
   is the rule down the left, which is what the model meant by it. */
.ai-text blockquote {
  margin: 14px 0;
  padding: 2px 0 2px 14px;
  border-left: 2px solid var(--separator);
  color: var(--ink);
}
.ai-text blockquote p { margin: 6px 0; }
.ai-text blockquote p:first-child { margin-top: 0; }
.ai-text blockquote p:last-child { margin-bottom: 0; }
/* A markdown heading, kept at body size — see mdToHtml. */
.ai-text .md-h { font-weight: 600; margin: 18px 0 8px; }
.ai-text .md-h:first-child { margin-top: 0; }
/* The Figma frame is 402 wide. Plenty of Android phones report less than that,
   and this row (5 icons plus the sources chip) was the widest thing in the app
   — it pushed past the viewport and made the whole page scroll sideways.
   Wrapping is the fix rather than shrinking: the chip drops to its own line on
   a narrow screen and the icon rhythm is left alone. */
.actions {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 20px 20px; row-gap: 12px;
  color: var(--ink-2); margin-top: 14px;
  max-width: 100%; min-width: 0;
}
.actions button { color: var(--ink-2); display: flex; flex: 0 0 auto; }
/* The Tools group sits further right than the icon rhythm (Figma: 35px gap).
   .meta-row is listed too: the sources row lives outside .actions, so scoping
   this to .actions alone left it display:block — and since each .src-link is
   itself a flex container, the favicons stacked down the page instead of
   sitting in a row before the word. */
.actions .tools,
.meta-row .tools {
  display: flex; align-items: center; gap: 10px; margin-left: 15px;
  font-size: 16px; font-weight: 300;
  min-width: 0; flex: 0 1 auto;
}
/* Once it wraps to its own line the 15px indent is wrong — it should line up
   with the icons above it. */
@media (max-width: 400px) {
  .actions .tools { margin-left: 0; }
}
/* The label must never be the thing that forces the row wider. */
.actions .tools span,
.meta-row .tools span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* About popover — Figma "Group 13" (1445:4152). 194x248, rows on a 39px pitch,
   hairline rules above Help & Support and above the version line. */
/* .popover is declared later in this file, so match its specificity to win. */
.popover.about-pop { padding: 6px 0; box-shadow: 0 0 8px rgba(14, 30, 44, 0.2); }
.about-row {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 0 14px; height: 39px;
  color: var(--ink); font-size: 13px; text-align: left;
}
.about-row .about-label { flex: 1; }
.about-row .about-ico { display: flex; color: var(--ink-2); flex: 0 0 auto; }
.about-row:not(.about-version):active { background: var(--soft-sky); }
.about-version { color: var(--ink); cursor: default; }
.about-rule { height: 1px; margin: 0 14px; background: var(--separator); }

/* ===== Profile — Figma "iPhone 17 - 1" (1477:3459) ===== */
/* The back button sits at y=48 here, not the app-wide 67. */
.screen.profile-view { padding-top: max(48px, calc(env(safe-area-inset-top) + 8px)); }
.prof-scroll { flex: 1; overflow-y: auto; padding: 0 16px calc(env(safe-area-inset-bottom) + 32px); }
.prof-scroll::-webkit-scrollbar { width: 0; }
.prof-header { position: relative; display: flex; align-items: center; justify-content: center; height: 40px; margin-bottom: 23px; }
.prof-back {
  position: absolute; left: 0; top: 0;
  width: 40px; height: 40px; border-radius: 999px;
  background: var(--surface); color: var(--ink);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 0 4px rgba(14, 30, 44, 0.15);
}
.prof-back:active { transform: scale(0.92); }
.prof-title { font-family: var(--serif); font-weight: 600; font-size: 27px; line-height: 1.1; color: var(--ink); }

.prof-label { display: block; padding-left: 9px; font-size: 17px; line-height: 1.1; color: var(--ink); }
.prof-label-lg { line-height: 1.52; margin-top: 33px; }
.prof-label-med { font-weight: 500; line-height: 1.52; margin-top: 49px; }
/* Capped so it breaks after "all" exactly as the frame does — the two-line
   height is what puts the textarea at y=497. */
.prof-note { padding-left: 9px; margin-top: 7px; max-width: 310px; font-size: 16px; line-height: 1.52; color: var(--ink-2); }

.prof-field, .prof-area {
  display: block; width: 100%; margin-top: 12px;
  background: var(--surface); border: none; outline: none;
  border-radius: var(--r);
  box-shadow: 0 0 4px rgba(14, 30, 44, 0.15);
  color: var(--ink); font-family: inherit;
}
.prof-field { height: 50px; padding: 0 18px; font-size: 18px; font-weight: 300; }
/* 18px, matching .prof-field. It was 16 — a name input and an instructions box
   in the same form rendered their text at two different sizes. */
.prof-area { height: 140px; padding: 15px 17px; font-size: 18px; font-weight: 300; line-height: 1.35; resize: none; margin-top: 16px; }
.prof-field::placeholder, .prof-area::placeholder { color: rgba(85, 107, 130, 0.8); font-weight: 300; }
.prof-label + .prof-field { margin-top: 13px; }
.prof-field + .prof-label { margin-top: 27px; }

.prof-btn {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 45px; margin-top: 19px;
  border-radius: var(--r); background: var(--accent);
  color: #fff; font-size: 20px; font-weight: 500;
  box-shadow: 0 4px 12px rgba(14, 30, 44, 0.15);
  transition: transform 0.12s ease, opacity 0.12s ease;
}
.prof-btn:active { transform: scale(0.98); opacity: 0.92; }
.prof-btn[disabled] { opacity: 0.55; box-shadow: none; }

.prof-delete {
  display: flex; align-items: center; gap: 14px;
  margin-top: 24px; padding-left: 21px;
  color: var(--danger); font-size: 18px; font-weight: 500; line-height: 1.1;
}
.prof-delete-ico { display: flex; }
/* Armed state — the first tap only arms it, so it has to look different from
   the resting label or the second tap is a surprise. */
.prof-delete.armed { font-weight: 700; }
.prof-delete[disabled] { opacity: 0.55; }
.prof-delete-warn {
  margin: 8px 0 0; padding-left: 21px;
  font-size: 13px; line-height: 1.4; color: var(--ink-2);
}

/* Memory landing — Figma "iPhone 17 - 2" (1487:3492). Shares the Profile
   screen's shell; only the link row and the taller fields differ. */
/* Adjacent margins collapse to the larger value, so the header's own gap has
   to shrink here rather than the link's grow. */
.mem-header { margin-bottom: 19px; }
.mem-link {
  display: flex; align-items: center; width: 100%;
  height: 60px; margin-top: 19px; padding: 0 18px;
  background: var(--surface); border-radius: var(--r);
  box-shadow: 0 0 4px rgba(14, 30, 44, 0.15);
  color: var(--ink); font-size: 18px; text-align: left;
}
.mem-link-label { flex: 1; }
.mem-link-chev { display: flex; color: var(--ink-2); }
.mem-link:active { transform: scale(0.99); }
/* Group heading inside Memory. Sits above the project rows the way the
   settings screen labels its groups, so the list reads as a section rather
   than more links bolted under the global one. */
.mem-section {
  padding-left: 9px;
  margin: 26px 0 10px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.2px;
  color: var(--ink-2);
}
.mem-section + .mem-link { margin-top: 0; }

.mem-note { padding-left: 9px; margin-top: 10px; max-width: 305px; font-size: 16px; font-weight: 300; line-height: 1.52; color: var(--ink-2); }
.mem-gap { margin-top: 61px; }
.prof-field.mem-field { height: 60px; margin-top: 13px; }
.prof-area.mem-area { margin-top: 13px; }

/* Appearance picker — Figma "Group 14" (1503:3566). 260x193, title then three
   48.5px rows; the selected row goes accent-blue and gains a check. */
.mode-wrap { position: absolute; inset: 0; z-index: 21; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.popover.mode-pop { position: static; pointer-events: auto; padding: 13px 0 6.5px; width: 260px; }
.mode-title { padding: 0 18px; margin-bottom: 5px; font-size: 15px; font-weight: 500; line-height: 1.52; color: var(--ink); }
.mode-row {
  display: flex; align-items: center; gap: 14px;
  width: 100%; height: 48.5px; padding: 0 18px 0 15px;
  color: var(--ink); font-size: 15px; font-weight: 500; text-align: left;
}
.mode-row .mode-ico { width: 22px; display: flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.mode-row .mode-label { flex: 1; }
.mode-row .mode-check { display: flex; color: var(--accent); flex: 0 0 auto; }
.mode-row.sel { color: var(--accent); }
.mode-row:not(.sel):active { background: var(--soft-sky); }

/* Settings gear in the drawer footer — raster export, masked so it retints. */
.foot-gear { display: flex; align-items: center; justify-content: center; padding: 6px; color: var(--ink); }
.mask-gear {
  display: block; width: 20px; height: 20px; background: currentColor;
  -webkit-mask: url('img/icon-gear.png') center / contain no-repeat;
  mask: url('img/icon-gear.png') center / contain no-repeat;
}

/* Raster glyph painted through a mask so it inherits the row's colour. */
.mask-phone {
  display: block; width: 22px; height: 22px; background: currentColor;
  -webkit-mask: url('img/icon-phone.png') center / contain no-repeat;
  mask: url('img/icon-phone.png') center / contain no-repeat;
}

/* ===== Permissions ===== */
.perm-card { margin-top: 6px; background: var(--surface); border-radius: var(--r); box-shadow: 0 0 4px rgba(14, 30, 44, 0.15); overflow: hidden; }
.perm-row { display: flex; align-items: center; gap: 16px; padding: 16px 18px; }
.perm-row + .perm-row { border-top: 1px solid var(--soft-sky); }
.perm-text { flex: 1; min-width: 0; }
.perm-label { font-size: 16px; font-weight: 500; color: var(--ink); }
.perm-sub { margin-top: 3px; font-size: 13px; line-height: 1.4; color: var(--ink-2); }
.perm-warn { color: var(--danger); }
.perm-note { margin-top: 18px; padding: 0 9px; font-size: 13px; line-height: 1.5; color: var(--ink-2); }

/* Switch */
.sw {
  position: relative; flex: 0 0 auto;
  width: 51px; height: 31px; border-radius: 999px;
  background: rgba(var(--ink-2-rgb), 0.28); transition: background 0.18s ease;
}
.sw .knob {
  position: absolute; top: 2px; left: 2px;
  width: 27px; height: 27px; border-radius: 999px; background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  transition: transform 0.18s ease;
}
.sw.on { background: var(--accent); }
.sw.on .knob { transform: translateX(20px); }


/* Clarifying-question chips. Intent, not content — see server/clarify.mjs. */
.clarify-opts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.clarify-chip {
  padding: 9px 14px; border-radius: 999px;
  background: var(--surface); color: var(--ink);
  font-size: 15px; line-height: 1;
  box-shadow: 0 0 4px rgba(14, 30, 44, 0.15);
  transition: transform 0.12s ease, background 0.12s ease;
}
.clarify-chip:active { transform: scale(0.96); background: var(--soft-sky); }

/* Per-turn memory receipt — appears only when the extractor saved a note (or
   crashed). Silent when the turn was small talk. */
.mem-badge {
  /* align-self, not inline-flex. .msg-ai is a column flex, and a flex item's
     display is blockified — inline-flex was silently becoming flex and the
     badge stretched the full width of the message, which is what made a
     footnote read as a system banner. */
  display: inline-flex; align-self: flex-start;
  align-items: center; gap: 6px;
  margin-top: 10px;
  font-size: 12px; font-weight: 400; line-height: 1;
  padding: 5px 10px; border-radius: 999px;
}
.mem-badge svg { display: block; opacity: 0.85; }
/* Quiet on purpose. The accent blue on soft-sky was the loudest thing in the
   thread for the least important line in it. --raised is unusable here: it is
   #F4F9FC, the pane's own background, so the pill would have no edge at all —
   hence surface plus a hairline. */
.mem-ok {
  background: var(--surface);
  /* Blue Gray, as in the file — but the source draws it 0.34px wide, which at a
     real 1px needs the alpha pulled back to read the same weight. */
  border: 1px solid rgba(var(--ink-2-rgb), 0.38);
  color: var(--ink-2);
  box-shadow: 0 1px 10px rgba(14, 30, 44, 0.06);
}
/* The receipt for a directive written into the user's own instructions.
   Louder than .mem-badge on purpose, and not a pill: a note is something SkyOS
   knows, this changes how every later answer is written, and it carries an
   action. Accent-tinted rather than Blue Gray so it reads as "something
   happened here" at a glance in a long thread — every value from the palette,
   nothing new. */
.pref-badge {
  align-self: flex-start; max-width: 100%;
  margin-top: 10px; padding: 10px 12px;
  background: var(--accent-wash);
  border: 1px solid rgba(var(--ink-2-rgb), 0.28);
  border-radius: 12px;
}
.pref-head {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 500; color: var(--ink-2); line-height: 1;
}
.pref-head svg { display: block; opacity: 0.85; }
.pref-lines { margin-top: 7px; }
.pref-line {
  font-size: 13px; line-height: 1.45; color: var(--ink);
}
.pref-was { color: var(--ink-2); }
/* A button, not a link: it performs an action and needs a real target. 32px
   tall keeps it thumb-reachable inside a badge that is itself only 60px. */
.pref-undo {
  margin-top: 8px; min-height: 32px; padding: 0 12px;
  font-size: 13px; font-weight: 500;
  color: var(--accent); background: var(--surface);
  border: 1px solid rgba(var(--ink-2-rgb), 0.28); border-radius: 999px;
}
.pref-undo:active { background: var(--accent-wash); }
/* The cap message. Its whole job is to say the user's own text is untouched,
   so it must not look like an error. */
.pref-skip {
  align-self: flex-start; margin-top: 8px;
  font-size: 12px; line-height: 1.45; color: var(--ink-2);
}

/* An error still has to be findable, so it keeps its colour — but it drops the
   fill and matches the receipt's shape. */
.mem-err {
  background: var(--surface);
  border: 1px solid var(--danger-soft);
  color: var(--danger);
}

/* The reply arriving. Without this the typing dots vanish and a block of text
   is simply there on the next frame — the jump is what reads as cheap, not the
   waiting. A short rise and fade gives the arrival somewhere to come from.
   Only the message that just landed carries this class, so scrolling back
   through old replies does not set the whole transcript animating. */
@keyframes msg-in {
  from { opacity: 0; transform: translateY(7px); }
  to { opacity: 1; transform: none; }
}
.msg-ai.msg-in { animation: msg-in 0.3s cubic-bezier(0.2, 0.7, 0.3, 1) both; }
@media (prefers-reduced-motion: reduce) { .msg-ai.msg-in { animation: none; } }

.typing { display: flex; gap: 6px; }
.typing i { width: 7px; height: 7px; border-radius: 999px; background: var(--ink-3); animation: blink 1.2s infinite; }
.typing i:nth-child(2) { animation-delay: 0.2s; }
.typing i:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink { 0%, 60%, 100% { opacity: 0.35; } 30% { opacity: 1; } }

/* Empty state */
/* Suggestions are left-aligned and sit just above the composer, not centred. */
/* Empty chat: mascot over a time-of-day greeting, sitting above the midpoint
   of the space between the top bar and the composer (Figma welcome frame). */
.empty {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 22px; padding: 0 16px 97px;
}
.empty-cloud { animation: cloud-sway 26s linear infinite; will-change: transform; }
.empty-cloud svg { display: block; animation: cloud-breathe 6.5s ease-in-out infinite; }

/* Breath, not orbit. A breath is a scale change, and it is asymmetric: the
   inhale takes about 40% of the cycle and the exhale the rest, which is what
   stops it reading as a pulsing logo. It rises slightly as it fills and settles
   as it empties, so the lift is on the same period and in phase with it. */
@keyframes cloud-breathe {
  0%   { transform: translateY(3px) scale(0.984); }
  40%  { transform: translateY(-4px) scale(1.022); }
  100% { transform: translateY(3px) scale(0.984); }
}
/* Underneath the breath, a wide flat ellipse — 20px across, 9px tall, with a
   1.2 degree tilt that leans into whichever side it is drifting toward.
   Sampled at eight points, not four. Keyframes are joined by straight lines, so
   four points make a diamond: the direction reverses hard at every quarter turn
   and the one at 0% reads as the loop snapping back. Eight points cut each
   corner in half and the path stops looking like it has edges. The values are
   cos/sin at 45 degree steps (0.707 of the radius on the diagonals), and the
   tilt is proportional to the horizontal offset so the lean always matches the
   drift.
   Timing stays linear: easing would slow it at the extremes and put the corners
   back in a different form. A drift with no acceleration is the one the eye
   stops noticing, which is the point — this should register as the cloud being
   alive, never as an animation playing. */
@keyframes cloud-sway {
  0%     { transform: translate(-10px, 0) rotate(-1.2deg); }
  12.5%  { transform: translate(-7.07px, -3.18px) rotate(-0.85deg); }
  25%    { transform: translate(0, -4.5px) rotate(0deg); }
  37.5%  { transform: translate(7.07px, -3.18px) rotate(0.85deg); }
  50%    { transform: translate(10px, 0) rotate(1.2deg); }
  62.5%  { transform: translate(7.07px, 3.18px) rotate(0.85deg); }
  75%    { transform: translate(0, 4.5px) rotate(0deg); }
  87.5%  { transform: translate(-7.07px, 3.18px) rotate(-0.85deg); }
  100%   { transform: translate(-10px, 0) rotate(-1.2deg); }
}
@media (prefers-reduced-motion: reduce) {
  .empty-cloud, .empty-cloud svg { animation: none; }
}

.empty-greeting { font-family: var(--serif); font-weight: 400; font-size: 26.6px; line-height: 1.1; color: var(--ink); text-align: center; }

/* Image attachments — thumbnails staged above the composer before sending. */
.attach-row { display: flex; gap: 8px; padding: 0 2px 10px; overflow-x: auto; }
.attach-row::-webkit-scrollbar { height: 0; }
.attach-chip {
  position: relative; flex: 0 0 auto;
  width: 62px; height: 62px; border-radius: var(--r); overflow: visible;
}
.attach-chip img { width: 100%; height: 100%; object-fit: cover; border-radius: var(--r); display: block; box-shadow: 0 0 4px rgba(14, 30, 44, 0.15); }
/* Non-image attachment: a labelled tile, since there's nothing to preview. */
.attach-doc {
  width: 128px; padding: 8px 10px;
  display: flex; flex-direction: column; justify-content: center; gap: 3px;
  background: var(--surface); border-radius: var(--r);
  box-shadow: 0 0 4px rgba(14, 30, 44, 0.15);
}
.attach-doc .attach-ico { color: var(--ink-2); display: flex; }
.attach-name { font-size: 12px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attach-err { font-size: 11px; color: var(--danger); }
.msg-file {
  display: inline-flex; align-items: center; gap: 8px;
  max-width: 100%; margin-bottom: 8px; padding: 9px 12px;
  background: var(--raised); border-radius: 8px;
  color: var(--ink); font-size: 15px; text-align: left;
}
.msg-file-ico { display: flex; color: var(--ink-2); flex: 0 0 auto; }
.msg-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* An attachment that aged out of the store. Says so rather than vanishing. */
.msg-file-gone { color: var(--ink-2); font-size: 14px; font-style: italic; }
.msg-attach-err { margin-top: 6px; font-size: 13px; color: var(--danger); }
.prof-save-err { margin-top: 10px; padding-left: 9px; font-size: 14px; color: var(--danger); }
.attach-x {
  position: absolute; top: -6px; right: -6px;
  width: 26px; height: 26px; border-radius: 999px;
  /* A white disc with a dark glyph, not the inverse. Both tokens flip with the
     theme, so this stays a light button on a light chip and a raised one in
     dark without a second rule. */
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--separator);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 6px rgba(14, 30, 44, 0.22);
}
/* Only where there is a pointer to hover with. On a touch screen there is no
   hover state to reveal it, so the button has to stay put — @media (hover:
   hover) is what keeps the phone exactly as it was. */
@media (hover: hover) {
  .attach-x {
    opacity: 0;
    transform: scale(0.85);
    transition: opacity 0.12s ease, transform 0.12s ease;
  }
  .attach-chip:hover .attach-x,
  .attach-doc:hover .attach-x,
  .attach-x:focus-visible {
    opacity: 1;
    transform: scale(1);
  }
}
/* Pictures you sent. They live in .msg-atts, a sibling of the text bubble
   rather than a child of it, so a screenshot is never sitting on the bubble's
   tinted card with its padding wrapped around it. */
.msg-img { display: block; width: 100%; max-width: 240px; border-radius: 8px; margin-bottom: 8px; }
/* Attachments lay out ACROSS the bubble, not down it, and hug the right edge
   the way the bubble itself does — a picture pinned to the left of a
   right-aligned message reads as belonging to the wrong side of the chat. */
.msg-atts { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
.msg-atts { max-width: 100%; }
.msg-atts .msg-img { margin-bottom: 0; }
/* An explicit width, not 100%. .msg-user-row is a column flex with
   align-items:flex-end, so .msg-atts shrinks to fit its contents — and a
   percentage width inside a shrink-to-fit parent resolves against the image's
   own intrinsic size, which silently stopped small pictures being scaled up and
   made every screenshot render smaller than it used to. The row's max-width is
   what keeps this inside the conversation. */
#app .msg-user-row .msg-atts:not(.multi) .msg-img { width: 280px; max-width: 100%; }
/* A file chip is a full-width strip, not a tile — keep it off the picture row. */
.msg-atts .msg-file { flex: 1 0 100%; }
/* One picture keeps its full size; several become a strip of thumbnails, so
   four screenshots do not turn the message into a scroll of its own.
   Bounded by HEIGHT, not cropped to a square: these are usually screenshots,
   and a square crop of a screenshot is four identical grey rectangles. Same
   height, natural width, wrapping when the row runs out of bubble.
   The selector is id-scoped deliberately: the desktop size rule below is also
   #app-scoped, and at equal specificity the one declared later wins — so a
   plain class rule here loses and every thumbnail comes back out at full
   size. */
/* Two pictures share the row and take half of it each — not a 116px contact
   sheet. Before, they were bounded by height, which made a pair roughly a third
   the size of the same picture sent on its own; side by side is the point, tiny
   is not. Half the row minus the gap is two across at any width, and a third
   picture wraps onto the next line at the same size. */
.msg-user-row.row-multi { width: 100%; }
/* min(280px, 84%) is a measure for TEXT — four or five words a line. Two
   pictures inside it are 116px each on a phone, which is the contact sheet all
   over again. A picture row gets the full column; #app.desk .msg-user-row is
   id-scoped, so desktop keeps its 66%. */
.msg-user-row.row-multi { max-width: 100%; }
/* The caption keeps the text measure even though its row no longer has one. */
.msg-user-row.row-multi .msg-user { max-width: min(280px, 100%); }
.msg-atts.multi { width: 100%; }
#app .msg-user-row .msg-atts.multi .msg-img {
  width: calc(50% - 3px);
  max-width: calc(50% - 3px);
  max-height: none;
}
/* Smaller than a picture sent on its own — a pair at the solo 340px is 686px of
   screenshot across the column, which takes over the conversation. 240 reads as
   a pair without shouting, and is still double the old 116px contact sheet. */
#app.desk .msg-user-row .msg-atts.multi .msg-img { max-width: 240px; }
/* Only when words follow the pictures. A picture-only message has no bubble at
   all, and a trailing margin there would push the hover actions down. */
.msg-atts + .msg-user { margin-top: 8px; }

/* Composer */
.composer-wrap { padding: 0 16px; }
/* align-items shifts to flex-end so the +, mic and orb stay on the bottom line
   as the textarea grows upward, instead of floating in the middle of it. */
/* Centred while it is a single-line bar — flex-end plus a margin fudge left the
   +, mic and orb sitting a few pixels off the text baseline. The stacked rule
   below switches to a real second row, which is the only case that needs it. */
.composer {
  display: flex; align-items: center; gap: 14px;
  min-height: 56px; padding: 0 13px 0 15px;
  background: var(--surface); border: none;
  border-radius: var(--r);
  box-shadow: 0 0 111px rgba(14, 30, 44, 0.13);
}
.composer .plus { color: var(--ink); display: flex; flex: 0 0 auto; }
.composer textarea {
  flex: 1; min-width: 0;
  border: none; outline: none; background: none; resize: none;
  /* 1.45 rather than 1.35: at 20px two lines sat almost touching. */
  /* 19px, 5% down from 20. The 1.45 line-height and the 13px padding are kept
     as they are: together they are what centres one line in the 56px bar, and
     scaling them with the text would move the baseline off centre. */
  font-family: var(--sans); font-size: 19px; line-height: 1.45; color: var(--ink);
  padding: 13px 0; margin: 0;
  max-height: 148px; overflow-y: hidden;
}
.composer textarea::placeholder { color: rgba(85, 107, 130, 0.8); }
/* The row buttons sit on the last line, so give them the same bottom offset. */
.composer .plus, .composer .mic, .composer .orb { margin-bottom: 0; }
/* Touch targets. The glyphs are sized by their SVG width attribute, and the
   only box around them came from the global button padding — leaving 32x22 for
   +, 36x26 for the mic and 36x36 for send, all under the 44px a fingertip
   needs, with + less than half of it. A centred pseudo-element grows the hit
   area without moving anything the eye can see.
   Vertical only, on purpose: the 56px bar has the height going spare, whereas
   widening would collide — the mic sits 3px from the orb, so a 44px-wide box on
   each would overlap and the two would steal each other's taps. */
.composer .plus, .composer .mic, .composer .orb { position: relative; }
.composer .plus::after,
.composer .mic::after,
.composer .orb::after {
  content: ''; position: absolute;
  left: 0; right: 0; top: 50%;
  height: 44px; transform: translateY(-50%);
}

/* Wrapped: the text takes the full width on its own row and the controls sit
   underneath — the layout ChatGPT uses, and the reason its composer breathes.
   flex-wrap plus order:-1 does it without changing the markup; .mic already
   carries margin-left:auto, which pushes mic and orb to the right of row two. */
.composer.stacked { flex-wrap: wrap; align-items: center; padding: 14px 13px 10px 15px; }
.composer.stacked textarea { flex: 1 0 100%; order: -1; padding: 0 0 10px; }
.composer.stacked .plus, .composer.stacked .mic, .composer.stacked .orb { margin-bottom: 0; }
/* The right-alignment lived on .mic, which is display:none as soon as there is
   text — so the send button lost it and sat next to the +. Put it on the orb
   as well; whichever of the two is visible now pushes to the right edge.
   .has-attach matters as much as .stacked: staging a picture wraps the
   textarea onto its own row exactly the way long text does, but it is a
   different class, so with an attachment and one line of text the send button
   dropped back beside the +. */
.composer.stacked .orb,
.composer.has-attach .orb { margin-left: auto; }
.composer .mic { color: var(--ink); display: flex; flex: 0 0 auto; margin-left: auto; transition: color 0.15s ease; }
/* Listening: the icon goes accent and breathes, so it is obvious the mic is
   open even when the user is looking at the text appearing, not the button. */
.composer .mic.listening { color: var(--accent); animation: mic-pulse 1.4s ease-in-out infinite; }
/* Uploading and transcribing: still busy, but no longer recording — a steady
   dimmed icon says "wait" without implying the mic is open. */
.composer .mic.working { color: var(--ink-3); animation: none; }
@keyframes mic-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
@media (prefers-reduced-motion: reduce) { .composer .mic.listening { animation: none; } }
.composer .orb { margin-left: 3px; width: 36px; height: 36px; border-radius: 999px; background: var(--accent); color: #fff; display: flex; align-items: center; justify-content: center; flex: 0 0 auto; }

/* ===== Overlays ===== */
.scrim { position: absolute; inset: 0; background: rgba(14, 30, 44, 0.25); animation: fade 0.2s ease; z-index: 20; }
.popover { position: absolute; z-index: 21; background: var(--surface); border: none; border-radius: var(--r); box-shadow: 0 12px 28px var(--shadow-strong); padding: 4px 0; animation: pop 0.18s ease; }
@keyframes pop { from { opacity: 0; transform: scale(0.92); } to { opacity: 1; transform: scale(1); } }
.popover-row { display: flex; align-items: center; gap: 14px; padding: 12px 16px; color: var(--ink); font-size: 16px; width: 100%; text-align: left; }
.popover-row .ico { width: 22px; display: flex; }

.drawer { position: absolute; z-index: 21; top: 0; left: 0; bottom: 0; width: 309px; background: var(--surface); box-shadow: 6px 0 24px var(--shadow-strong); display: flex; flex-direction: column; animation: slidein 0.3s ease; border-radius: 0 12px 12px 0; }
@keyframes slidein { from { transform: translateX(-100%); } to { transform: translateX(0); } }
@keyframes slideout { from { transform: translateX(0); } to { transform: translateX(-100%); } }

/* Exit animations — without these the panel is deleted mid-air and appears to
   blink out instead of sliding away. */
.drawer.closing { animation: slideout 0.22s cubic-bezier(0.4, 0, 1, 1) forwards; }
.popover.closing, .proj-menu.closing { animation: popout 0.15s ease forwards; }
.scrim.closing { animation: fadeout 0.2s ease forwards; }
@keyframes popout { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.94); } }

@media (prefers-reduced-motion: reduce) {
  .screen, .drawer, .popover, .proj-menu, .scrim { animation-duration: 0.01ms !important; }
}
.drawer .dh { display: flex; align-items: center; gap: 10px; padding: 16px 20px; }
.drawer .dh .name { font-family: var(--serif); font-weight: 600; font-size: 22px; }
.drawer .new { display: flex; align-items: center; gap: 12px; padding: 12px 20px; color: var(--accent); font-weight: 600; font-size: 16px; }
.drawer .sec { padding: 16px 20px 6px; font-size: 13px; font-weight: 600; color: var(--ink-2); }
.drawer .recent { display: flex; align-items: center; gap: 12px; padding: 11px 20px; color: var(--ink); font-size: 15px; }
.drawer .recent .ico { color: var(--ink-2); display: flex; }
.drawer .foot { margin-top: auto; display: flex; align-items: center; gap: 12px; padding: 16px 20px; border-top: 1px solid var(--separator); }
.avatar-initials { width: 34px; height: 34px; border-radius: 999px; background: #55697E; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; }

/* ===== Drawer (ready-for-dev redesign) ===== */
/* Mascot: 180x110 at 4.93deg, bleeding past the panel edge (node 1145:3241). */
/* Clockwise as the frame draws it, but eased off — the full 4.93° read as the
   mascot staring at the floor. */
.drawer-cloud { width: 200px; margin: 62px 0 0 -24px; transform: rotate(2.4deg); transform-origin: center; }
/* The cloud arrives after the drawer, not with it — the panel slides in over
   0.3s, so this waits 0.14s and then settles, which reads as the cloud landing
   in the drawer rather than being painted on it.
   Every keyframe repeats rotate(2.4deg) because transform is one property: a
   keyframe that sets only translate/scale would drop the tilt for the duration
   of the animation and snap it back at the end.
   `both` holds the from-state during the delay, otherwise it flashes at full
   opacity for the first frame. */
.drawer-cloud {
  animation: drawer-cloud-in 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) 0.3s both;
}
/* Small to normal, and it stops there — no keyframe goes above scale(1), so the
   cloud never grows past its real size. The 0.3s delay is the drawer's own
   slide-in duration, so the panel has fully arrived before this starts. */
@keyframes drawer-cloud-in {
  from { opacity: 0; transform: rotate(2.4deg) scale(0.78); }
  to   { opacity: 1; transform: rotate(2.4deg) scale(1); }
}
/* Three layers, because `transform` is one property and each of these needs it:
     .drawer-cloud       tilt + entrance
     .cloud-float        swing
     svg                 breath
   All the idle animations start at 0.8s — 0.3s delay plus the 0.5s entrance —
   and every one of them begins at the identity transform, so the hand-off from
   the entrance is seamless. That was the rough edge: the breath used to start
   at scale(0.982) while the entrance ended at scale(1), which snapped smaller
   the instant it took over. */
.cloud-float {
  display: block;
  animation: drawer-cloud-swing 9s ease-in-out 0.8s infinite;
}
.drawer-cloud > .cloud-float > svg {
  display: block;
  animation: drawer-cloud-breathe 7s ease-in-out 0.8s infinite;
}

/* A swing is a pendulum, so ease-in-out is right here — it should slow at the
   ends of the arc. It leans into whichever side it is moving toward, and
   returns through centre twice per cycle. */
@keyframes drawer-cloud-swing {
  0%   { transform: translateX(0) rotate(0deg); }
  25%  { transform: translateX(3.5px) rotate(0.7deg); }
  50%  { transform: translateX(0) rotate(0deg); }
  75%  { transform: translateX(-3.5px) rotate(-0.7deg); }
  100% { transform: translateX(0) rotate(0deg); }
}
/* Starts and ends at 1 so it never clashes with the entrance, and only ever
   swells — going below 1 is what made the junction visible. */
@keyframes drawer-cloud-breathe {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.035); }
  100% { transform: scale(1); }
}

/* A re-render while the drawer is already open must not replay it — that is the
   same rule the drawer itself follows. The breath keeps going, and without the
   delay, since there is no entrance to wait for. */
.drawer.no-anim .drawer-cloud { animation: none; }
.drawer.no-anim .cloud-float,
.drawer.no-anim .drawer-cloud > .cloud-float > svg { animation-delay: 0s; }
@media (prefers-reduced-motion: reduce) {
  .drawer-cloud, .cloud-float, .drawer-cloud > .cloud-float > svg { animation: none; }
}
/* Nav rows sit on a 48px pitch starting at y=244 (New Project baseline). */
.drawer-nav { padding: 0; margin-top: 45px; }

/* Projects — separate threads that share one memory. */
.drawer-scroll { flex: 1; overflow-y: auto; min-height: 0; padding-bottom: 96px; }
.drawer-scroll::-webkit-scrollbar { width: 0; }
/* "Recents" — Inter Light 15px, not an uppercase eyebrow. */
/* "Recents" — Inter Light 15px, not an uppercase eyebrow. */
.drawer-sec { font-size: 15px; font-weight: 300; color: var(--ink-2); padding: 28px 0 15px 18px; }
/* Soft-sky hairline, inset 16px, 277 wide (node 1332:3255). */
.drawer-rule { height: 1px; background: var(--soft-sky); margin: 14px 16px 0; }
/* The row is a container now, not a button: the name opens the project and the
   trailing dots open its menu, and a button cannot nest inside a button. */
.proj-row {
  display: flex; align-items: center; gap: 4px; width: 100%;
  padding: 0 6px 0 13px; margin: 0 16px 9px; width: calc(100% - 32px); height: 38px;
  border-radius: var(--r); color: var(--ink); font-size: 18px; line-height: 1.2; text-align: left;
}
.proj-open {
  display: flex; align-items: center; gap: 12px;
  flex: 1; min-width: 0; height: 100%;
  color: inherit; font-size: inherit; text-align: left;
}
/* Project rows carry no glyph; the slot stays so names align with "New project". */
.proj-row .ico { display: flex; justify-content: center; width: 20px; color: var(--ink-2); flex: 0 0 auto; }
.proj-row .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.proj-row.active { background: var(--raised); }
.proj-row .pinned { display: flex; color: var(--ink-2); flex: 0 0 auto; }
.proj-row.renaming { padding: 4px 8px; }
.proj-rename {
  width: 100%; height: 38px; padding: 0 12px;
  border: none; outline: none; border-radius: var(--r);
  background: var(--surface); box-shadow: 0 0 0 2px rgba(10, 102, 194, 0.35);
  font-family: var(--sans); font-size: 17px; color: var(--ink);
}

/* Per-project menu (node 1334:3273): 160 wide, Cloud White, 10px, soft shadow. */
.scrim-clear { background: transparent; }
/* Settings is a real modal, so its scrim both dims and blurs — the blur is what
   stops the chat behind it reading as something you could still be looking at.
   Small dropdowns use .scrim-clear instead: dimming the whole app for a 230px
   menu darkens the very thing the menu acts on. */
.scrim-blur {
  background: rgba(14, 30, 44, 0.32);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
:root[data-theme="dark"] .scrim-blur { background: rgba(3, 8, 14, 0.5); }
.proj-menu {
  position: absolute; z-index: 22; left: 195px; width: 160px; height: 170px;
  /* White, like every other popover. It used --raised (Cloud White), which was
     the odd one out — and it opens over a white drawer on mobile and a white
     sidebar on desktop, so the separation has to come from the shadow rather
     than a tint. Matched to .popover for that reason. */
  background: var(--surface); border-radius: var(--r);
  box-shadow: 0 12px 28px var(--shadow-strong);
  padding: 9px 0; animation: pop 0.16s ease;
}
.pm-row {
  display: flex; align-items: center; gap: 22px; width: 100%;
  height: 38px; padding: 0 15px; color: var(--ink); font-size: 16px; font-weight: 500; text-align: left;
}
.pm-row .ico { display: flex; width: 22px; justify-content: center; color: var(--ink); flex: 0 0 auto; }
.pm-row:active { background: var(--soft-sky); }
.pm-row.danger, .pm-row.danger .ico { color: var(--danger); }
.drawer-item { display: flex; align-items: center; gap: 21px; width: 100%; height: 48px; padding: 0 17px; color: var(--ink); font-size: 20px; font-weight: 500; }
.drawer-item .ico { display: flex; align-items: center; justify-content: center; width: 22px; color: var(--ink); flex: 0 0 auto; }
.drawer-item:not(.is-soon):active { background: var(--accent-wash); }
/* The New Project glyph's geometry fills its viewBox exactly (0,0 -> 20.03 x
   20.05) and is stroked at 1px, so the root <svg> -- which clips to its own
   viewport by default -- cut half that stroke off all four edges and the icon
   read as chopped. Letting it paint outside costs ~0.5 units, well inside the
   22px .ico box. */
.drawer-item .ico svg { overflow: visible; }

/* A row for something that does not exist yet. It keeps the shape of the real
   rows so the list still reads as a list, and loses every affordance that would
   promise it does something: no press state, no hover, no pointer. */
.drawer-item.is-soon,
.set-row.is-soon {
  color: var(--ink-3);
  cursor: default;
  pointer-events: none;
}
.drawer-item.is-soon .ico,
.set-row.is-soon .set-ico { color: var(--ink-3); }
.set-row.is-soon .set-label { color: var(--ink-3); }
.soon-tag {
  margin-left: auto;
  font-size: 11px;
  letter-spacing: 0.3px;
  color: var(--ink-3);
  background: var(--raised);
  padding: 2px 7px;
  border-radius: 999px;
  flex: 0 0 auto;
}
.drawer-foot { position: absolute; left: 0; right: 0; bottom: 0; height: 84px; display: flex; align-items: center; gap: 20px; padding: 0 28px 0 17px; background: var(--surface); box-shadow: 0 -2px 10px rgba(14,30,44,0.03); border-radius: 0 0 12px 0; }
.drawer-foot .avatar-initials { width: 33px; height: 33px; background: var(--accent-wash); color: var(--ink); font-size: 12px; font-weight: 600; }
.drawer-foot .who { flex: 1; min-width: 0; }
.drawer-foot .drawer-foot-account { display: flex; align-items: center; gap: 20px; flex: 1; min-width: 0; text-align: left; }
.drawer-foot .who-name { font-size: 20px; color: var(--ink); }
.drawer-foot .who-plan { font-size: 16px; color: var(--ink-2); font-weight: 300; }


/* ===== In-app settings screens (Integrations / Library) ===== */
.settings-nav { display: flex; align-items: center; gap: 12px; padding: 0 16px; }
.settings-nav .title { flex: 1; text-align: center; font-size: 22px; font-weight: 500; color: var(--ink); }
.settings-nav .trail { display: flex; color: var(--ink-2); padding: 8px; }
/* Bare icon (no white circle) that balances the 48px back button on the other
   side of the title — matches the info-icon pattern seen in Settings. */
.nav-icon-btn { width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; color: var(--ink-2); }
.nav-icon-btn:active { transform: scale(0.92); }

.settings-scroll { flex: 1; overflow-y: auto; padding: 8px 16px calc(env(safe-area-inset-bottom) + 24px); }
.settings-scroll::-webkit-scrollbar { width: 0; }
.settings-section { color: var(--ink-2); font-size: 18px; padding: 20px 8px 10px; }
/* No vertical padding. Each row already carries 12px of its own, and the ⋮ is
   centred on the ROW — so 6px on the card put 18px above the first button and
   18px below the last against 12px everywhere else. The first and last dots sat
   lower and higher than the rest of the column, which is the only place this
   list was out of true. overflow:hidden so the row hover fill stops at the
   rounded corners now that rows reach them. */
.settings-card { background: var(--surface); border: none; border-radius: 10px; box-shadow: 0 0 4px var(--shadow); padding: 0; }
/* The end rows carry the card's radius themselves, so a row's hover fill stops
   at the corner instead of squaring it off. */
.settings-card > .note-line:first-child { border-radius: 10px 10px 0 0; }
.settings-card > .note-line:last-child { border-radius: 0 0 10px 10px; }
.settings-row { display: flex; align-items: center; gap: 15px; padding: 14px 16px; }
.settings-row .mark { width: 22px; display: flex; justify-content: center; }
.settings-row .txt { flex: 1; min-width: 0; }
.settings-row .r-name { font-size: 15px; color: var(--ink); }
.settings-row .r-sub { font-size: 12px; color: var(--ink-2); }
.pill-connected, .pill-connect { height: 30px; padding: 0 18px; border-radius: 30px; font-size: 15px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0.5px 4px var(--shadow); }
.pill-connected { background: var(--accent); color: #fff; min-width: 110px; }
.pill-connect { background: var(--accent-wash); color: var(--ink); min-width: 100px; }
.mcp-row { display: flex; align-items: center; gap: 20px; width: 100%; padding: 16px; color: var(--ink); font-size: 18px; }
.mcp-row .ico { display: flex; color: var(--ink); }
/* Memory — read-only. Deliberately not an editable field. */
/* A note is now a row with a kebab, not a paragraph. position:relative so the
   little menu can hang off it. */
.note-line {
  position: relative;
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 16px; font-size: 16px; line-height: 1.45; color: var(--ink);
}
.note-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
/* Both actions live on the row.
   They were behind a ⋮ that opened a popover offering exactly two things, so
   every edit and every delete cost a click and a menu first. On hover the pair
   simply appears — same reveal the kebab had, one less step inside it. */
.note-tools {
  flex: 0 0 auto; align-self: center;
  display: flex; align-items: center; gap: 2px;
  /* The buttons' own padding is the tap target; the negative margin cancels it
     so the row lays out as though they were only their glyphs. */
  margin: -9px -9px -9px 0;
}
.note-tool {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 8px;
  border-radius: var(--r);
  color: var(--ink-3);
  transition: background 0.12s ease, color 0.12s ease;
}
.note-tool-danger { color: color-mix(in srgb, var(--danger) 62%, var(--ink-3)); }
/* Only where there is a pointer to hover with — a touch screen has no hover
   state to reveal them with, so on a phone they stay put. */
@media (hover: hover) {
  .note-tools { opacity: 0; transition: opacity 0.12s ease; }
  .note-line:hover .note-tools,
  .note-tools:focus-within { opacity: 1; }
  .note-tool:hover { background: rgba(var(--ink-2-rgb), 0.10); color: var(--ink); }
  /* Delete says what it is before it is pressed, not after. */
  .note-tool-danger:hover {
    background: color-mix(in srgb, var(--danger) 12%, transparent);
    color: var(--danger);
  }
}
.note-tool:active { background: rgba(var(--ink-2-rgb), 0.16); }
.note-tool-danger:active { background: color-mix(in srgb, var(--danger) 20%, transparent); color: var(--danger); }

/* The confirm, in the row rather than in a dialog, so the note being deleted is
   still readable above the question. */
.note-confirming { flex-direction: column; align-items: stretch; gap: 10px; }
.note-confirming .note-actions { display: flex; align-items: center; gap: 8px; }
.note-ask { flex: 1; min-width: 0; font-size: 14px; color: var(--ink-2); }
.pill-danger { background: var(--danger); color: #fff; }
@media (hover: hover) {
  .pill-danger:hover { background: color-mix(in srgb, var(--danger) 82%, #000); }
}
.pill-danger:active { background: color-mix(in srgb, var(--danger) 70%, #000); }

.note-editing { flex-direction: column; align-items: stretch; gap: 10px; }
.note-input {
  width: 100%; resize: vertical; padding: 10px 12px;
  border: 1px solid var(--separator); border-radius: 8px; outline: none;
  font-family: var(--sans); font-size: 16px; line-height: 1.45; color: var(--ink);
  background: var(--bg-top);
}
.note-input:focus { border-color: var(--accent); }
.note-actions { display: flex; justify-content: flex-end; gap: 8px; }
.note-err { padding: 10px 8px 0; font-size: 14px; color: var(--danger); }
.note-line + .note-line { box-shadow: inset 0 1px 0 var(--separator); }
.note-empty { padding: 14px 16px; font-size: 15px; color: var(--ink-2); }
/* When SkyOS last had a note confirmed. Its own line, quiet — it is provenance,
   not part of the fact, and it should never compete with the sentence. */
.note-when { display: block; margin-top: 3px; font-size: 12px; color: var(--ink-3); }
.note-foot { padding: 20px 8px 8px; font-size: 13px; line-height: 1.5; color: var(--ink-2); }

.lib-row { display: flex; align-items: center; gap: 14px; padding: 14px 12px; color: var(--ink); font-size: 16px; background: var(--surface); border-radius: var(--r); box-shadow: 0 0 4px var(--shadow); margin-bottom: 10px; }
.lib-row .ico { display: flex; color: var(--ink-2); }

/* ===== Dark theme ===== */
:root[data-theme="dark"] {
  --accent: #3E8EDE;
  --accent-wash: rgba(62, 142, 222, 0.16);
  /* Deep Sky itself is #053666 — on this background it scores 1.4:1 as a fill
     and 1.05:1 as text on the accent wash, which is invisible either way. It
     is still a fill here, just one lifted far enough to clear the 3:1 the
     spec asks of a non-text element while white type on it stays at 5.2:1. */
  --deep-sky: #2A6FB8;
  /* Text on the wash goes the other way: light enough to read against it
     (5.5:1) rather than a deeper shade of the background. */
  --accent-ink: #6FB0EC;
  --ink: #EAF1F8;
  --ink-2: #93A6BA;
  --ink-2-rgb: 147, 166, 186;
  /* Lifted toward white: #C0392B on #16232F is 2.6:1, unreadable. */
  --danger: color-mix(in srgb, #C0392B 45%, var(--white));
  --danger-soft: color-mix(in srgb, var(--danger) 34%, var(--surface));
  --danger-wash: color-mix(in srgb, var(--danger) 16%, var(--surface));
  --ink-3: rgba(var(--ink-2-rgb), 0.60);
  --bg-top: #0F1B28;
  --bg-bottom: #0A1420;
  --fade-rgb: 15, 27, 40;
  --fade-end: #0A1420;
  --raised: #14212D;
  --oval-top: #1B3352;
  /* Used directly as a fill (the mascot oval) and as a tap highlight, so it
     needs a dark value of its own, not the light Soft Sky. */
  --soft-sky: #16283C;
  --surface: #16232F;
  --separator: rgba(var(--ink-2-rgb), 0.14);
  --field: #16232F;
  --shadow: rgba(0, 0, 0, 0.4);
  --shadow-strong: rgba(0, 0, 0, 0.6);
}
html[data-theme="dark"], html[data-theme="dark"] body { background: #060b12; }

/* utility */
.hidden { display: none !important; }

/* ===== Settings screen (Figma 1364:3249) =====
   Cloud-white background, cards are white rounded rects with 2px gap. No strokes,
   no per-row shadow — the white-on-cloud contrast IS the card affordance. */
.settings-view { padding: 0; background: var(--cloud-white); }
.set-scroll { flex: 1; overflow-y: auto; padding: var(--top-margin) 16px calc(env(safe-area-inset-bottom) + 20px); }
.set-scroll::-webkit-scrollbar { width: 0; }

.set-header { display: flex; align-items: center; gap: 12px; padding-bottom: 16px; position: relative; }
.set-title {
  position: absolute; left: 50%; transform: translateX(-50%);
  margin: 0; font-family: var(--serif); font-weight: 600;
  font-size: 27px; line-height: 1.1; color: var(--ink);
}
.set-info-btn {
  margin-left: auto; display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; color: var(--ink-2);
}


.set-section-label {
  font-family: var(--sans); font-weight: 400; font-size: 16px;
  color: var(--ink-2); padding: 8px 4px 4px;
}
.set-group {
  display: flex; flex-direction: column; gap: 2px;
  border-radius: 10px; overflow: hidden; margin-bottom: 12px;
}
.set-row {
  display: flex; align-items: center; gap: 16px;
  padding: 18px 20px; width: 100%;
  background: #fff; border-radius: 10px; border: none;
  color: var(--ink); font-family: var(--sans); font-size: 16px; font-weight: 500;
  text-align: left; cursor: pointer;
  transition: background 0.15s ease;
}
.set-row:active { background: var(--soft-sky); }
.set-ico { display: flex; align-items: center; justify-content: center; width: 28px; color: var(--ink); flex: 0 0 auto; }
.set-labels { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.set-label { font-size: 16px; font-weight: 500; color: var(--ink); }
.set-sub { font-size: 13px; font-weight: 400; color: var(--ink-2); }
.set-chev { display: flex; color: var(--ink-2); flex: 0 0 auto; }
/* Compact rows (Email / Appearance) use 14px vertical padding per Figma. */
.set-group .set-row:has(.set-sub) { padding-top: 14px; padding-bottom: 14px; }

.set-logout-spacer { height: 16px; }
.set-logout, .set-logout .set-label, .set-logout .set-ico { color: var(--danger); font-weight: 600; }

/* Drawer footer becomes a tap-target opening Settings. */
.drawer-foot-account {
  display: flex; align-items: center; gap: 20px; flex: 1; min-width: 0;
  background: none; border: none; padding: 0; text-align: left; cursor: pointer;
}
.drawer-foot-account:active { opacity: 0.7; }

/* Dark theme override — cards use --surface, not white. */
:root[data-theme="dark"] .settings-view { background: #0A1420; }
:root[data-theme="dark"] .set-row { background: var(--surface); }

/* Login screen — post-sign-in email confirmation. */
.login-sent { display: flex; flex-direction: column; align-items: center; gap: 18px; padding: 8px 8px 4px; text-align: center; }
.login-sent p { margin: 0; font-size: 15px; line-height: 1.5; color: var(--ink-2); }
.login-sent p b { color: var(--ink); font-weight: 600; }
.login-error { font-size: 13px; color: var(--danger); padding: 2px 4px; }

/* Manual city — the reliable alternative when the browser blocks geolocation. */
.perm-city { margin-top: 26px; }
.perm-city .prof-field { margin-top: 12px; }
.perm-city .prof-btn { margin-top: 12px; }

/* Sources sit in the action row's trailing slot, matching the Tools group. */
.src-tools { color: var(--ink-2); }
.src-tools .src-link { display: flex; align-items: center; }
.src-fav { width: 18px; height: 18px; border-radius: 4px; display: block; }
.src-ico { display: flex; color: var(--ink-2); }
/* Stand-in for a site that serves no icon at either well-known path. Sized and
   rounded exactly like a real favicon so a mixed row still reads as one row. */
.src-mono {
  width: 18px; height: 18px; border-radius: 4px;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-size: 10px; font-weight: 600; line-height: 1;
  letter-spacing: 0; flex: 0 0 auto;
}
.perm-retry { padding: 0; color: var(--accent); font-size: inherit; font-family: inherit; text-decoration: underline; }
.perm-ok { color: var(--accent); }

/* ===== Voice mode ===== */
.screen.voice { padding-left: 0; padding-right: 0; }
.voice-top { display: flex; justify-content: flex-start; width: 100%; }
/* position:relative so the halo below anchors to the orb, not to the screen. */
.voice-orb-wrap { position: relative; display: flex; align-items: center; justify-content: center; height: 200px; }
/* The orb is scaled from the live mic level in JS, so the transform here is
   only the resting state — never animate `transform` or the meter fights it. */
.voice-orb {
  width: 132px; height: 132px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: #fff;
  box-shadow: 0 10px 40px rgba(10, 102, 194, 0.35);
  transition: background 0.3s ease, box-shadow 0.3s ease;
  will-change: transform;
}
.voice-orb.thinking { background: var(--accent-soft); }
.voice-orb.speaking { background: var(--deep-sky); }
.voice-orb-wrap::after {
  content: ''; position: absolute; width: 260px; height: 260px; border-radius: 999px;
  background: radial-gradient(circle, rgba(10, 102, 194, 0.16), transparent 68%);
  pointer-events: none;
}
.voice-label {
  font-family: var(--serif); font-weight: 600; font-size: 30px;
  color: var(--ink); margin: 28px 0 0; text-align: center;
}
.voice-note {
  max-width: 300px; margin: 10px auto 0; text-align: center;
  font-size: 15px; line-height: 1.45; color: var(--ink-2);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.voice-actions { display: flex; gap: 10px; justify-content: center; width: 100%; }

/* ===== Mascot plate ===== */
/* Soft Sky (#E5F2FF) from the style guide, as a clean ellipse. No blur and no
   gradient: a blurred edge over a near-white background has no clear boundary,
   which is what made it look like a smudge instead of a shape. */
.glow-wrap { position: relative; display: grid; place-items: center; }
/* The oval is positioned and the cloud is not, so without this the oval paints
   on top and hides it completely — which the old semi-transparent gradient
   masked. Stacking has to be explicit once the fill is opaque. */
.glow-wrap > svg { position: relative; z-index: 1; }
/* Size and vertical offset are set inline by glowWrap(), derived from the
   cloud so the pair scales together. */
.glow-oval {
  position: absolute;
  border-radius: 50%;
  /* Bluer at the top, settling into Soft Sky by two-thirds down. Stopping the
     gradient early rather than running it to 100% keeps the bottom edge the
     same colour as before, so the cloud still sits on a flat base. */
  background: linear-gradient(180deg, var(--oval-top) 0%, var(--soft-sky) 68%);
  pointer-events: none;
}

/* Pre-signup privacy link. Small and quiet, but present — it is the only route
   to the policy before an account exists. */
.login-legal {
  margin: 14px 0 0; text-align: center;
  font-size: 12px; line-height: 1.5; color: var(--ink-2);
}
.login-legal a { color: var(--accent); text-decoration: underline; }

/* ===== Library — files and images ===== */
/* Tabs sit under the header, in the pill idiom used elsewhere rather than the
   underline ChatGPT uses; SkyOS has no underlined controls anywhere else. */
.lib-tabs { display: flex; gap: 8px; padding-top: 4px; padding-bottom: 14px; }
.lib-tab {
  height: 34px; padding: 0 18px; border-radius: 999px;
  background: transparent; color: var(--ink-2);
  font-size: 15px; font-weight: 600;
  transition: background 0.15s ease, color 0.15s ease;
}
.lib-tab.active { background: var(--raised); color: var(--ink); }

/* Search is revealed from the ⋮ menu, so it appears above the tabs and pushes
   the grid down rather than floating over it. */
.lib-search {
  display: flex; align-items: center; gap: 10px;
  height: 44px; margin-bottom: 12px; padding: 0 14px;
  background: var(--surface); border-radius: var(--r);
  box-shadow: 0 0 4px var(--shadow);
  animation: fade 0.18s ease;
}
.lib-search .ico { display: flex; color: var(--ink-2); flex: 0 0 auto; }
.lib-search input {
  flex: 1; min-width: 0; border: none; outline: none; background: none;
  font-size: 16px; color: var(--ink);
}
.lib-search input::placeholder { color: var(--ink-3); }
.lib-search-x { display: flex; color: var(--ink-2); flex: 0 0 auto; }

/* Two columns on a phone. auto-fill rather than a fixed count so it widens
   sensibly on the desktop frame. */
.lib-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  padding-bottom: 24px;
}
.lib-tile {
  position: relative; aspect-ratio: 1;
  border-radius: 14px; overflow: hidden;
  background: var(--surface);
  box-shadow: 0 0 4px var(--shadow);
  transition: transform 0.12s ease;
}
.lib-tile:active { transform: scale(0.98); }
.lib-tile-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A document has no picture, so the name is the content — top-aligned and
   readable, with the icon as a quiet marker underneath. */
.lib-tile-file {
  display: flex; flex-direction: column; justify-content: space-between;
  align-items: flex-start; text-align: left; padding: 14px;
}
.lib-tile-name {
  font-size: 15px; font-weight: 600; line-height: 1.3; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}
.lib-tile-ico { display: flex; color: var(--accent); }

/* Export sits above Delete in Account Actions — same row rhythm, but neutral
   rather than destructive, so the red one stays the only alarming thing there. */
.prof-export {
  display: flex; align-items: center; gap: 14px;
  margin-top: 24px; padding-left: 21px;
  color: var(--ink); font-size: 18px; font-weight: 500; line-height: 1.1;
}
.prof-export-ico { display: flex; color: var(--ink-2); }
.prof-export[disabled] { opacity: 0.55; }

/* ===== Find in chat ===== */
.find-bar {
  display: flex; align-items: center; gap: 10px;
  height: 44px; margin-bottom: 6px; padding: 0 14px;
  background: var(--surface); border-radius: var(--r);
  box-shadow: 0 0 4px var(--shadow);
  animation: fade 0.18s ease;
  flex: 0 0 auto;
}
.find-bar .ico { display: flex; color: var(--ink-2); flex: 0 0 auto; }
.find-bar input {
  flex: 1; min-width: 0; border: none; outline: none; background: none;
  font-size: 16px; color: var(--ink);
}
.find-bar input::placeholder { color: var(--ink-3); }
.find-count { font-size: 14px; color: var(--ink-2); flex: 0 0 auto; }
.find-x { display: flex; color: var(--ink-2); flex: 0 0 auto; }

/* Sources and Tools now sit on their own line under the action icons, so they
   get the full width instead of the leftover space beside five buttons. */
.meta-row { display: flex; margin-top: 10px; }
.meta-row .tools { margin-left: 0; }

/* Roadmap label on the onboarding integrations list. Deliberately not a button:
   nothing is behind these yet, and a tappable pill would imply otherwise. */
.pill-soon {
  height: 30px;
  padding: 0 18px;
  border-radius: 30px;
  font-size: 15px;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 76px;
  background: var(--raised);
  color: var(--ink-3);
}

/* Timestamp line at the top of the per-message ⋮ menu. Blue Gray, not full ink:
   it is a fact about the message, not an action, and must not compete with the
   two rows under it. */
.msg-sent {
  padding: 12px 16px 10px;
  font-size: 13px;
  color: var(--ink-2);
  border-bottom: 1px solid var(--separator);
  margin-bottom: 4px;
}

/* Only rendered in an empty chat — see the comment at .composer-wrap. Sized and
   coloured to be readable but clearly secondary to the composer above it. */
.ai-note {
  margin: 10px 0 0;
  text-align: center;
  font-size: 12px;
  line-height: 16px;
  color: var(--ink-3);
}

/* ===== Desktop layout =====
   Above 1024px SkyOS stops pretending to be a phone. The bezel below 1024 is a
   deliberate choice for tablets and half-width windows — one readable column
   still beats a 300px sidebar crushed against a 600px chat — but at desk width
   it wastes most of the screen.
   The breakpoint is duplicated in app.js (DESKTOP_Q). Change both together:
   the sidebar has to be rendered into the DOM, so CSS alone cannot produce it. */
@media (min-width: 1024px) {
  /* No bezel on any screen. A phone drawn in the middle of a 2000px display
     reads as "this is not a desktop app" no matter how good the screen inside
     it is, and that is the first thing a desktop visitor sees. */
  body { display: block; background: var(--bg-top); }
  #app {
    --ui-scale: 1.1;
    zoom: var(--ui-scale);
    max-width: none;
    /* 100% needs no compensation — a percentage already resolves against the
       parent as measured INSIDE the zoom, so dividing it applied the factor
       twice and left the shell 131px short of the window. Only true viewport
       units are absolute and need it. */
    width: 100%;
    height: calc(100dvh / var(--ui-scale));
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: transparent;
  }
  #app.desk { background: var(--surface); }

  /* Everything except chat — login, onboarding, paywall, settings — stays one
     column, because that is what those screens are: a title, a short stack of
     controls, a button. Widening them would only stretch the gaps. What changes
     is the frame around them: instead of a bezel they sit on a full-bleed
     background, centred, which is how the old desktop build drew its login. */
  /* ===== Desktop welcome + sign-in =========================================
     Not the phone frame scaled up. The mockup, the scrim and the sheet are all
     answers to a phone's constraints, and none of those constraints exist in a
     1990px window — so the desktop versions are a centred stack on a full-bleed
     page, with the sign-in controls in a card. */
  /* .screen is declared below this and would win on source order at equal
     specificity, keeping the 560px cap. Doubling the class settles it without
     depending on where either rule sits. */
  #app:not(.desk) .screen.wd-screen {
    max-width: none;
    left: 0;
    right: 0;
    transform: none;
    align-items: center;
    /* Centred. The screen used to be top-aligned so a product preview could run
       off the bottom edge; skyos.ink already shows the app, so the preview is
       gone and the mascot, title, tagline and button are the whole screen.
       SAFE center, not center: in a short window plain centring overflows in
       both directions and the top of the stack goes above the scroll origin,
       where it cannot be scrolled back to. safe falls back to the top edge
       only when the content does not fit. */
    justify-content: safe center;
    /* Still scrollable, for a window too short to hold the stack — the button
       has to stay reachable. overflow-x stays hidden so nothing can widen the
       page sideways. */
    overflow-y: auto;
    overflow-x: hidden;
    padding: 96px 24px 96px;
    /* The same gradient sign-in uses, so the two screens either side of
       "Get started" read as one surface. The old stop cleared to white by 62%
       to melt the screenshot into the page, which it no longer has to do. */
    background: linear-gradient(180deg, var(--bg-top), var(--bg-bottom));
  }

  /* Sized to its content; the screen centres it. */
  .wd-stack {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
  }
  /* The mark carries more weight than the wordmark, as in the reference — at
     132 against a 58px title the cloud read as an afterthought above it. */
  /* The reference leaves roughly a mark-height of air under it before the
     wordmark. At 42px the two read as one clump. */
  .wd-mark { display: flex; margin-bottom: 62px; }
  .wd-title {
    font-family: var(--serif);
    font-weight: 500;
    font-size: 58px;
    line-height: 1.05;
    letter-spacing: -0.5px;
    color: var(--ink);
    margin: 0;
  }
  /* "Sign In" carries a form under it rather than a whole page, so it sits a
     step down from the wordmark. */
  .wd-title-sm { font-size: 46px; }
  .wd-sub {
    margin: 18px 0 0;
    font-size: 23px;
    font-weight: 400;
    color: var(--ink-2);
  }

  /* In the flow, directly under the tagline, so the four elements centre as one
     group rather than the button drifting to the floor of a tall window. */
  .wd-cta {
    margin-top: 60px;
    width: min(530px, calc((100vw - 48px) / var(--ui-scale)));
  }
  .wd-cta .btn { width: 100%; height: 64px; font-size: 22px; border-radius: 12px; }

  /* The card. A hairline plus a soft shadow, so it lifts off the page without
     turning into a modal — there is nothing behind it to be modal over. */
  /* The stack is shrink-to-fit, so a percentage here resolved against the
     text inside it and the card came out 307px wide. Give the stack the
     measure and let the card fill it. */
  .login-desk .wd-stack { width: min(520px, 100%); }
  /* Sign-in has nothing below the fold, so it stays centred. */
  #app:not(.desk) .screen.wd-screen.login-desk { justify-content: center; padding: 48px 24px; }
  .wd-card {
    width: 100%;
    margin-top: 40px;
    padding: 30px 32px 26px;
    background: var(--surface);
    border: 1px solid var(--separator);
    border-radius: 16px;
    box-shadow: 0 10px 40px var(--shadow);
    display: flex;
    flex-direction: column;
    text-align: left;
  }
  /* The sheet's own spacing assumed a sheet, and the field's top margin is
     scoped to .login-sheet — so inside the card it was zero and the field sat
     hard against the divider. One rhythm, set here. */
  .wd-card .login-google { margin-top: 0; }
  /* The reference sets the word apart from the rules rather than letting them
     run into it: caps, tracked out, and real space on both sides. At 12px
     lowercase with a 10px gap it read as a hyphen between two lines. */
  .wd-card .login-div { margin: 20px 0; gap: 18px; }
  .wd-card .login-div i { background: var(--separator); }
  .wd-card .login-div span {
    font-size: 12px;
    font-weight: 400;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    color: var(--ink-3);
  }
  .wd-card .lfield { margin-top: 0; }
  .wd-card .login-cta { margin-top: 20px; }
  .wd-card .login-legal { margin: 14px 0 0; }

  /* Onboarding steps hold together instead of stretching.
     .spacer is flex:1, which pins the button to the floor — right on a phone,
     where the floor is 60px below the field. In a 1310px window it left the
     form at the top and the button 700px away, with nothing in between. Cap the
     spacer and centre the step as one group. */
  #app:not(.desk) .screen.onboard {
    justify-content: center;
    padding-top: 48px;
    padding-bottom: 48px;
  }
  #app:not(.desk) .screen.onboard .spacer { flex: 0 0 44px; }

  /* The thing you are being asked for should not be smaller than the button
     that submits it. At 16px against a 24px "Continue" the field read as
     secondary to its own confirm. */
  #app:not(.desk) .screen.onboard .field { height: 62px; padding: 0 18px; }
  #app:not(.desk) .screen.onboard .field input { font-size: 21px; }

  #app:not(.desk) .screen {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: 100%;
    max-width: 560px;
  }
  /* The onboarding carousel is a 300%-wide track that slides. On a phone the
     track is exactly viewport-wide and #app's overflow:hidden clips it. Here
     the column is 560px inside a full-width #app, so nothing clipped it and all
     three slides sat side by side. Clip at the column instead. Scoped with
     :has() so it cannot catch a screen whose overlay needs to escape. */

  .dt-shell {
    position: absolute;
    inset: 0;
    flex-direction: row;
    padding: 0;
  }

  /* The drawer's contents without the drawer: no scrim, no slide, no shadow —
     it is furniture now, not something you summoned. */
  .dt-side {
    /* Positioned so .drawer-foot, which is absolute left:0 right:0, resolves
       against this column. Without it the nearest positioned ancestor is
       .dt-shell and the account row stretches the full window — name on the
       far left, settings gear on the far right. */
    position: relative;
    width: 420px;
    flex: 0 0 420px;
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border-right: 1px solid var(--separator);
    overflow: hidden;
  }
  /* The mascot's entrance animation is tied to the drawer sliding in. Nothing
     slides here, so it would play once on every render for no reason. */
  .dt-side .drawer-cloud { animation: none; margin-top: 40px; }

  .dt-main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    background: var(--bg-top);
    padding-bottom: 24px;
  }

  .dt-topbar {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 18px 24px 6px;
    flex: 0 0 auto;
  }
  /* The name sits left, its controls right. */
  .dt-bar-acts { margin-left: auto; display: flex; align-items: center; gap: 2px; }
  /* The editable title. Sized and padded exactly like the text it replaces so
     double-clicking does not make the bar jump. */
  .dt-proj-input {
    font-family: inherit; font-size: 19px; font-weight: 500; color: var(--ink);
    padding: 6px 8px; border-radius: var(--r);
    border: 1px solid var(--accent); outline: none;
    background: var(--surface);
    min-width: 0; max-width: 420px;
  }
  /* Project name as the title. */
  .dt-proj {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 19px;
    font-weight: 500;
    color: var(--ink);
    padding: 6px 8px;
    border-radius: var(--r);
    max-width: 340px;
  }
  .dt-proj span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .dt-proj:hover { background: var(--raised); }
  /* A name you can edit, not a button that opens something. */
  .dt-proj { cursor: text; user-select: none; }

  /* Chat holds a reading column rather than stretching to the window.
     1300 is sized off a 1920 screen: the 420px sidebar leaves 1500px here, so
     this lands ~124px of margin on each side. Wider than the usual reading
     measure, but 22px type pulls the line back to around 90 characters — and
     the alternative on this screen was 174px of empty background per side. */
  .dt-main .composer-wrap,
  .dt-main .find-bar { width: 100%; max-width: 1300px; margin-left: auto; margin-right: auto; }
  .dt-main .composer-wrap { padding-left: 24px; padding-right: 24px; }

  /* The transcript is the element that scrolls, so it has to be the element the
     pointer is over. Capping it at the reading width left ~200px of dead
     gutter on each side that belonged to .dt-main, which does not scroll — the
     wheel did nothing out there. Full width, with the reading column held in
     place by auto side padding instead. */
  .dt-main .transcript {
    width: 100%;
    max-width: none;
    padding-left: max(24px, calc((100% - 1252px) / 2));
    padding-right: max(24px, calc((100% - 1252px) / 2));
  }

  /* The empty state centres in the space instead of sitting under the header. */
  .dt-main .empty { justify-content: center; }
}

@media (min-width: 1024px) {
  /* One cloud, not two — and it belongs in the sidebar.
     The mascot lives in both the drawer and the empty state, but on mobile you
     never see both at once because the drawer is an overlay. Side by side on
     desktop they read as a duplicated asset. The desktop design settles which
     one survives: the cloud sits at the top of the sidebar and the main area
     has no mascot at all, just the greeting over the composer. */
  #app.desk .empty-cloud { display: none; }

  /* Without a mascot beside it the greeting has to carry the space on its own,
     at the display size the desktop design gives it. */
  #app.desk .empty-greeting { font-size: 49.4px; }

  /* An empty chat centres the greeting and the composer as one group instead of
     pinning the composer to the floor. Stranded at the bottom of 800px of empty
     background, a white bar on #F4F9FC is genuinely hard to find — which is
     what "I can't see the text bar" meant. The two auto margins split the free
     space above and below the pair, so the topbar stays put at the top.
     Once there are messages the transcript takes over and the composer returns
     to the bottom, which is correct there. */
  #app.desk .dt-main:has(.empty) .empty {
    flex: 0 0 auto;
    margin-top: auto;
    padding-bottom: 34px; /* 97px is a phone value; this is the gap to the composer */
  }
  #app.desk .dt-main:has(.empty) .composer-wrap { margin-bottom: auto; }

  /* Enough edge to find against a near-white page. On the phone the composer
     sits on a gradient with a shadow and never needs this. */
  #app.desk .composer { border: 1px solid var(--separator); }
}

/* ===== Desktop settings dialog =====
   Two panes over a dimmed app, per the desktop design (shots/18-settings-*).
   The right pane hosts the unmodified mobile screens, which are absolutely
   positioned `.screen` roots — hence position:relative + overflow:hidden here,
   so they fill this box instead of the viewport. */
@media (min-width: 1024px) {
  #app.desk .dt-modal {
    position: absolute;
    z-index: 30;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: min(1120px, calc((100vw - 72px) / var(--ui-scale)));
    height: min(820px, calc((100dvh - 72px) / var(--ui-scale)));
    display: flex;
    background: var(--surface);
    border-radius: 16px;
    box-shadow: 0 30px 80px var(--shadow-strong);
    overflow: hidden;
    animation: fade 0.18s ease;
  }
  /* The suppression has to be id-scoped too, or it never lands. .dt-modal.no-anim
     up in the base rules is (0,2,0) and the rule directly above is (1,2,0), so
     the id won every time and the class did nothing — render() has been marking
     the dialog no-anim on every pane switch and the whole 1120x820 panel re-ran
     its fade regardless. That replay is the flash: click Memory, and the entire
     dialog blinks rather than just its right-hand pane changing. */
  #app.desk .dt-modal.no-anim { animation: none; }

  /* --- Memory pane ------------------------------------------------------- */
  /* 19px between cards and a 305px measure on the note are both sized for the
     402px phone frame. In a 900px pane the cards drift apart and the paragraph
     wraps at a third of the width it has, so it reads as a narrow column
     stranded beside empty space. */
  #app.desk .mem-link { margin-top: 10px; }
  #app.desk .mem-note { max-width: 62ch; }
  #app.desk .dt-modal-nav {
    width: 264px;
    flex: 0 0 264px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 26px 14px;
    border-right: 1px solid var(--separator);
    /* White nav, tinted content — the same way round as the phone, where cards
       sit on the light background rather than the other way about. */
    background: var(--surface);
  }
  #app.desk .dt-modal-title {
    font-family: var(--serif);
    font-size: 26px;
    font-weight: 600;
    color: var(--ink);
    padding: 0 10px 16px;
  }
  #app.desk .dt-nav-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 10px 12px;
    border-radius: var(--r);
    font-size: 15px;
    color: var(--ink);
    text-align: left;
    text-decoration: none;
  }
  #app.desk .dt-nav-row .ico { display: flex; color: var(--ink-2); flex: 0 0 auto; }
  #app.desk .dt-nav-row:not(.is-soon):hover { background: var(--raised); }
  #app.desk .dt-nav-row.is-soon { color: var(--ink-3); cursor: default; pointer-events: none; }
  #app.desk .dt-nav-row.is-soon .ico { color: var(--ink-3); }
  #app.desk .dt-nav-row.active { background: var(--accent-wash); color: var(--accent-ink); }
  #app.desk .dt-nav-row.active .ico { color: var(--accent); }
  /* Pushes the trailing Privacy link to the bottom of the column. */
  #app.desk .dt-nav-gap { flex: 1; min-height: 12px; }

  #app.desk .dt-modal-body { position: relative; flex: 1; min-width: 0; overflow: hidden; background: var(--bg-top); }
  /* The reused mobile screens carry a phone's top inset and their own back
     button; neither belongs in a dialog that has its own close button. The
     inset is replaced rather than zeroed — the inner scrollers (.prof-scroll,
     .settings-scroll) have no top padding of their own, so padding-top: 0 put
     the header hard against the dialog edge. */
  #app.desk .dt-modal-body .screen { padding: 28px 0 20px; animation: none; }
  #app.desk .dt-modal-body .prof-back,
  #app.desk .dt-modal-body .set-header .circle-btn,
  #app.desk .dt-modal-body [data-act="back-to-chat"].circle-btn { display: none; }
  /* With the back button gone the title is no longer optically centred against
     it; centre it in the pane instead. */
  #app.desk .dt-modal-body .prof-header { justify-content: center; }

  #app.desk .dt-modal-x {
    position: absolute;
    top: 14px;
    right: 14px;
    width: 32px;
    height: 32px;
    border-radius: 999px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--ink-2);
    z-index: 2;
  }
  #app.desk .dt-modal-x:hover { background: var(--raised); }

}

/* ===== Desktop sidebar chrome ===== */
@media (min-width: 1024px) {
  #app.desk .dt-side-top {
    display: flex;
    align-items: center;
    /* Search and collapse read as one pair of window controls, so they sit
       together at the right rather than being spread apart. */
    justify-content: flex-end;
    /* Air between the two: at 2px they read as one welded control. */
    gap: 14px;
    padding: 14px 16px 0;
    flex: 0 0 auto;
  }
  #app.desk .dt-side-btn {
    width: 34px; height: 34px; border-radius: var(--r);
    display: flex; align-items: center; justify-content: center;
    /* --ink, like New Project and Memory below it. At --ink-2 it was the only
       lit control in the column drawn in the caption colour, which reads as
       disabled rather than as secondary. */
    color: var(--ink);
    /* The inherited button padding (1px 6px) left only 22px of content box, so
       any glyph wider than that was flex-shrunk horizontally while keeping its
       height -- the panel icon rendered 22 wide by 27 tall, visibly squashed
       and reading smaller than the nav icons below. Zero the padding and stop
       the svg from shrinking; the 34px box centres the glyph on its own. */
    padding: 0;
  }
  #app.desk .dt-side-btn svg { flex: 0 0 auto; }
  #app.desk .dt-side-btn:hover { background: var(--raised); color: var(--ink); }

  /* Collapsed rail — icons only. Labels are hidden rather than removed so the
     DOM (and every handler bound to it) is identical in both states. */
  #app.desk .dt-side.rail { width: 76px; flex: 0 0 76px; }
  #app.desk .dt-side.rail .drawer-cloud,
  #app.desk .dt-side.rail .drawer-sec,
  #app.desk .dt-side.rail .proj-row,
  #app.desk .dt-side.rail .drawer-rule,
  #app.desk .dt-side.rail .who,
  /* Renamed with the compact footer; the old selector matched nothing, which is
     why the name, chevron and bug button still crammed into the 76px rail. */
  #app.desk .dt-side.rail .who-line,
  #app.desk .dt-side.rail .foot-chev,
  #app.desk .dt-side.rail .foot-bug { display: none; }
  #app.desk .dt-side.rail .drawer-foot { padding: 0; justify-content: center; }
  #app.desk .dt-side.rail .drawer-foot-account.compact { max-width: none; flex: 0 0 auto; padding: 6px; gap: 0; }
  #app.desk .dt-side.rail .dt-side-top { justify-content: center; }
  #app.desk .dt-side.rail .dt-side-top [data-act="open-search"] { display: none; }
  #app.desk .dt-side.rail .drawer-item {
    justify-content: center; gap: 0; padding: 0; font-size: 0;
  }
  #app.desk .dt-side.rail .drawer-item .ico { font-size: initial; }
  /* The "Soon" pill survived the collapse. font-size:0 hides the LABEL, but the
     tag sets its own 11px, so it kept its size — and with margin-left:auto it
     claimed the free space and shoved the Integrations icon off the left edge
     of a 76px rail while the three above it stayed centred. */
  #app.desk .dt-side.rail .soon-tag { display: none; }
  /* The tips hang outside the 84px rail, and .drawer-scroll clips horizontally
     — it has to, to keep long project names from bleeding out when the sidebar
     is open. Collapsed, everything it scrolls (recents, the rule, the section
     heading) is hidden and only three icons remain, so there is nothing left to
     scroll and nothing left to clip. */
  #app.desk .dt-side.rail .drawer-scroll { overflow: visible; }
  /* Evens out the column: the toggle-to-first-icon gap measured 68.2px
     against 59.4px between the nav icons, because this margin was sized for
     the cloud logo that rail mode hides. 10px puts all three gaps at ~59px. */
  #app.desk .dt-side.rail .drawer-nav { margin-top: 10px; }
  #app.desk .dt-side.rail .drawer-foot { padding: 0; justify-content: center; gap: 0; }
  #app.desk .dt-side.rail .foot-gear { display: none; }
  #app.desk .dt-side.rail .drawer-foot-account { flex: 0 0 auto; justify-content: center; }
}

@media (min-width: 1024px) {
}

/* Account menu hanging off the sidebar footer (desktop), per the design:
   the email as a quiet header, the active row as a filled pill, hairline
   rules between groups, over an undimmed app. */
@media (min-width: 1024px) {
  #app.desk .foot-menu { padding: 6px; }
  #app.desk .foot-menu-email {
    padding: 8px 12px 10px;
    font-size: 14px;
    color: var(--ink-3);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  #app.desk .foot-menu-rule { height: 1px; background: var(--separator); margin: 6px 8px; }
  #app.desk .foot-menu .popover-row {
    font-size: 15px; padding: 9px 12px; border-radius: var(--r);
    text-decoration: none;
  }
  #app.desk .foot-menu .popover-row:not(.is-soon):hover { background: var(--raised); }
  /* The design fills the current row rather than only marking it on hover. */
  #app.desk .foot-menu .popover-row.active { background: var(--accent-wash); color: var(--accent-ink); }
  #app.desk .foot-menu .popover-row.active .ico { color: var(--accent); }
  #app.desk .foot-menu .ico { color: var(--ink-2); }
  /* Chevron replaces the gear: the whole footer row is the trigger now. */
  #app.desk .dt-side .foot-gear { color: var(--ink-2); }
}

@media (min-width: 1024px) {
  /* One target: avatar, name and chevron. */
  #app.desk .drawer-foot-account.with-chevron { gap: 12px; padding: 0 14px 0 0; border-radius: var(--r); }
  #app.desk .drawer-foot-account.with-chevron:hover { background: var(--raised); }
  #app.desk .foot-chev { display: flex; color: var(--ink-2); flex: 0 0 auto; }
  #app.desk .drawer-foot { padding-right: 14px; }
}

@media (min-width: 1024px) {
  /* Rows the design draws but the app has not built. Visible so the menu keeps
     its shape, inert so nothing pretends to work — the same call as the "Soon"
     labels on the integrations list. */
  #app.desk .popover-row.is-soon {
    display: flex; align-items: center; gap: 14px;
    padding: 9px 12px; font-size: 15px;
    color: var(--ink-3); cursor: default;
  }
  #app.desk .popover-row.is-soon .ico { color: var(--ink-3); }
  #app.desk .soon-tag {
    margin-left: auto; font-size: 11px; letter-spacing: 0.3px;
    color: var(--ink-3); background: var(--raised);
    padding: 2px 7px; border-radius: 999px;
  }
  /* Help's disclosure arrow, pointing right rather than down. */
  #app.desk .row-chev { margin-left: auto; display: flex; color: var(--ink-3); transform: rotate(-90deg); }
  #app.desk .foot-submenu { padding: 6px; z-index: 22; }
  #app.desk .foot-submenu .popover-row { font-size: 15px; padding: 9px 12px; border-radius: var(--r); text-decoration: none; }
  #app.desk .foot-submenu .popover-row:not(.is-soon):hover { background: var(--raised); }
}

@media (min-width: 1024px) {
  /* Compact account row: one line, avatar + "Name · Tier" + chevron, with the
     settings gear on the far right as its own target. The two-line 20px/16px
     version was the phone drawer's, and at desktop scale it read as the heaviest
     thing in the sidebar rather than a quiet footer. */
  #app.desk .drawer-foot { height: 76px; padding: 0 12px 0 10px; gap: 6px; }
  #app.desk .drawer-foot-account.compact {
    display: flex; align-items: center; gap: 9px;
    flex: 0 1 auto; max-width: calc(100% - 44px); min-width: 0;
    padding: 6px 8px; border-radius: var(--r);
  }
  #app.desk .drawer-foot-account.compact:hover { background: var(--raised); }
  #app.desk .drawer-foot-account.compact .avatar-initials {
    width: 26px; height: 26px; font-size: 11px;
    background: var(--raised); color: var(--ink);
  }
  #app.desk .who-line {
    flex: 0 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 15px; color: var(--ink);
  }
  #app.desk .who-dot { margin: 0 6px; color: var(--ink-3); }
  #app.desk .who-tier { color: var(--ink-2); }
  #app.desk .drawer-foot-account.compact .foot-chev { flex: 0 0 auto; color: var(--ink-2); display: flex; }
  #app.desk .dt-side .foot-gear { flex: 0 0 auto; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: var(--r); color: var(--ink-2); }
  #app.desk .dt-side .foot-gear:hover { background: var(--raised); }
  /* The scroll area clears the shorter footer now. */
  #app.desk .dt-side .drawer-scroll { padding-bottom: 92px; }
}

@media (min-width: 1024px) {
  /* Bug button sits hard right; the account button no longer stretches, so the
     chevron stays beside the name instead of being pushed across the row. */
  #app.desk .foot-bug {
    margin-left: auto; flex: 0 0 auto;
    width: 32px; height: 32px; border-radius: var(--r);
    display: flex; align-items: center; justify-content: center;
    color: var(--ink-2);
  }
  #app.desk .foot-bug:hover { background: var(--raised); color: var(--ink); }
  #app.desk .drawer-foot-account.compact .avatar-initials { width: 28px; height: 28px; font-size: 12px; }
  #app.desk .who-line { font-size: 16px; }
}

/* Feedback composer. Small centred dialog — this one dims, unlike the account
   menu, because it takes focus and holds text the user is part-way through. */
.fb-dialog {
  position: absolute; z-index: 32;
  /* Centred with auto margins, NOT translate(-50%,-50%). The pop animation sets
     transform to scale() alone, and transform is a single property — so the
     translate was dropped for the animation duration and the panel painted
     offset down-right before snapping to the middle. Same trap the drawer cloud
     keyframes call out. */
  inset: 0;
  margin: auto;
  height: max-content;
  width: min(520px, calc((100vw - 40px) / var(--ui-scale)));
  background: var(--surface);
  border-radius: 16px;
  box-shadow: 0 24px 60px var(--shadow-strong);
  padding: 22px 22px 18px;
  animation: pop 0.16s ease;
}
.fb-title { margin: 0 0 14px; font-size: 18px; font-weight: 600; color: var(--ink); }
.fb-area {
  width: 100%; resize: vertical; min-height: 132px;
  padding: 12px 14px; border-radius: 12px;
  border: 1px solid var(--separator); background: var(--bg-top);
  font-family: var(--sans); font-size: 15px; line-height: 1.5; color: var(--ink);
}
.fb-area:focus { outline: none; border-color: var(--accent-soft); }
.fb-note { margin: 12px 0 0; font-size: 13px; line-height: 1.5; color: var(--ink-2); }
.fb-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; }
.fb-btn {
  padding: 9px 18px; border-radius: 999px; font-size: 15px;
  background: var(--raised); color: var(--ink);
}
.fb-btn:hover { background: var(--soft-sky); }
.fb-btn.primary { background: var(--accent); color: #fff; }
.fb-btn.primary:hover { background: var(--deep-sky); }

@media (min-width: 1024px) {
  /* The disclaimer belongs to the screen, not to the composer. With the empty
     state centred, sitting it under the composer parks it in the middle of the
     window; pinned to the foot of the column it reads as a footnote, which is
     what it is. */
  #app.desk .dt-main { position: relative; }
  #app.desk .dt-main .ai-note {
    position: absolute;
    left: 0; right: 0; bottom: 12px;
    margin: 0;
  }
}

@media (min-width: 1024px) {
  /* Beats `.drawer-foot-account.compact .foot-chev { display: flex }`, which ties
     on specificity and, being declared later, was winning. */
  #app.desk .dt-side.rail .drawer-foot-account.compact .foot-chev { display: none; }
}

/* Project ⋮ — hidden until the row is hovered, so the sidebar stays quiet while
   the menu is still reachable with a pointer. Touch keeps long-press; there is
   no hover there, so the button simply stays visible on coarse pointers. */
.proj-dots {
  flex: 0 0 auto;
  width: 28px; height: 28px; border-radius: var(--r);
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-2);
  opacity: 0;
  transition: opacity 0.12s ease;
}
.proj-row:hover .proj-dots,
.proj-dots:focus-visible { opacity: 1; }
.proj-dots:hover { background: var(--surface); color: var(--ink); }
@media (hover: none) {
  .proj-dots { opacity: 1; }
}

@media (min-width: 1024px) {
  /* The header ⋮ is a bare icon here — the white disc is a phone affordance,
     drawn so the button reads against a photo or a busy transcript. On a wide
     quiet header it just looks like a stray chip. */
  #app.desk .dt-topbar .circle-btn {
    background: transparent;
    box-shadow: none;
    width: 40px; height: 40px;
    color: var(--ink);
  }
  #app.desk .dt-topbar .circle-btn:hover { background: var(--raised); color: var(--ink); }

}

@media (min-width: 1024px) {
  /* The project menu lives inside the sidebar in the DOM, but it is a popover:
     at left:195px + 160px wide it reaches 355px and the 340px sidebar was
     clipping it. Let the column overflow so the menu floats over the chat, the
     way the design draws it. The scroll area still clips its own content. */
  #app.desk .dt-side { overflow: visible; }
  #app.desk .dt-side .drawer-scroll { overflow-y: auto; overflow-x: hidden; }
  /* Only the mascot relied on the old clip — it is inset -24px and would hang
     past the window edge. */
  #app.desk .dt-side .drawer-cloud { margin-left: 0; }
  #app.desk .proj-menu { z-index: 24; }
}

@media (min-width: 1024px) {
  /* Expanding the rail replays the drawer's entrance, the same beat a phone
     gets when the drawer slides in. Added by syncSideCloudEntrance on that
     transition only — the base rule keeps `animation: none` so an ordinary
     re-render never replays it. */
  #app.desk .dt-side .drawer-cloud.cloud-enter {
    animation: drawer-cloud-in 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) both;
  }
}

@media (min-width: 1024px) {
  /* The header kebab now carries the project actions too, so it needs the same
     rule and danger colour the sidebar's own menu uses. */
  #app.desk .kebab-menu { padding: 6px; }
  #app.desk .kebab-menu .popover-row { border-radius: var(--r); padding: 10px 12px; font-size: 15px; }
  #app.desk .kebab-menu .popover-row:not(.is-soon):hover { background: var(--raised); }
  #app.desk .kebab-menu .popover-row.danger,
  #app.desk .kebab-menu .popover-row.danger .ico { color: var(--danger); }
}

/* ===== Desktop scale =====
   The sidebar and chat inherited the phone's type sizes, which are set for a
   402px frame held at arm's length. On a 1600px display at desk distance the
   same numbers read small and the column looks under-filled. Everything below
   is one step up, kept in proportion so the alignments hold: icon slots grow
   with their labels, row heights with their text, and the 8px rhythm survives.
   Colours are untouched — every value here is a size, and the palette tokens
   stay exactly as they are. */
@media (min-width: 1024px) {
  /* --- sidebar --------------------------------------------------------- */
  #app.desk .dt-side .drawer-item {
    height: 54px;
    /* 19px, one step down from 21px. This block -- not the base .drawer-item
       rule -- is what wins here: it is later in the file AND more specific. */
    font-size: 19px;
    padding: 0 20px;
    gap: 20px;
    border-radius: var(--r);
  }
  /* The icon slot has to grow with the label or the text shifts left as the
     glyph stays put, and the nav stops aligning with the project rows. */
  #app.desk .dt-side .drawer-item .ico { width: 26px; }
  #app.desk .dt-side .drawer-sec { font-size: 16px; padding: 30px 0 14px 21px; }
  #app.desk .dt-side .proj-row {
    height: 44px;
    font-size: 19px;
    padding: 0 6px 0 16px;
    margin-left: 20px;
    width: calc(100% - 40px);
  }
  #app.desk .dt-side .proj-dots { width: 32px; height: 32px; }
  #app.desk .dt-side .drawer-rule { margin: 16px 20px 0; }

  /* --- sidebar footer -------------------------------------------------- */
  #app.desk .drawer-foot { height: 84px; padding: 0 14px 0 12px; }
  #app.desk .drawer-foot-account.compact { padding: 8px 10px; gap: 11px; }
  #app.desk .drawer-foot-account.compact .avatar-initials { width: 32px; height: 32px; font-size: 13px; }
  #app.desk .who-line { font-size: 17px; }
  #app.desk .foot-bug { width: 36px; height: 36px; }
  #app.desk .dt-side .drawer-scroll { padding-bottom: 100px; }

  /* --- header ---------------------------------------------------------- */
  #app.desk .dt-topbar { padding: 22px 28px 8px; }
  #app.desk .dt-proj { font-size: 21px; padding: 8px 10px; }
  #app.desk .dt-topbar .circle-btn { width: 44px; height: 44px; }

  /* --- composer -------------------------------------------------------- */
  /* Taller bar, larger text, and a radius that keeps pace — 10px on a 66px bar
     reads as a rectangle with the corners knocked off. */
  #app.desk .composer { min-height: 76px; border-radius: 16px; padding: 0 16px 0 18px; }
  #app.desk .composer #composerInput { font-size: 21.8px; }
  /* The real class names: .plus, .mic and the blue .orb. The icons inside are
     sized by their SVG width attribute, so only the orb takes a box size —
     the other two grow via the glyph scale below. */
  #app.desk .composer .orb { width: 46px; height: 46px; }
  #app.desk .composer .plus svg,
  #app.desk .composer .mic svg { width: 28px; height: 28px; }
  /* 4px was mine, from the scale-up pass, and it jammed the mic against the orb
     and both against the edge. The phone uses 14; a wider bar with bigger glyphs
     needs at least that. */
  /* :not(.stacked) again — this padding has zero top/bottom, which is right
     for the single-line bar (min-height centres it) but strips .composer.stacked
     of the vertical room its two rows need. The id here outweighs that class,
     so the wrapped composer was left with text jammed against the top edge and
     buttons against the bottom. */
  #app.desk .composer:not(.stacked) { gap: 16px; padding: 0 14px 0 20px; }
  #app.desk .composer.stacked { gap: 16px; padding: 18px 16px 14px 20px; }
  #app.desk .composer.stacked textarea { padding: 0 0 16px; line-height: 1.5; }
  /* Both exclusions matter. The mic carries margin-left:auto, and the mic is
     hidden as soon as there is something to send — so the orb re-adds the auto
     margin to keep Send at the right edge. This rule has an id in it and
     outranks that, collapsing Send back against the +. It caught .stacked
     first; .has-attach is the same layout reached a different way, and staging
     a picture put Send next to the + again. */
  #app.desk .composer:not(.stacked):not(.has-attach) .orb { margin-left: 0; }
  #app.desk .ai-note { font-size: 13px; }

  /* --- transcript ------------------------------------------------------- */
  #app.desk .msg-ai .ai-text, #app.desk .msg-user { font-size: 22px; line-height: 1.6; }
  #app.desk .actions button { width: 36px; height: 36px; }
  /* Not the 36px of an AI row: these hang in the gap between messages, and at
     36 they reach the message below. */
  #app.desk .actions.user-actions button { width: 30px; height: 30px; }
}

@media (min-width: 1024px) {
  /* Project names line up with the nav labels above them. The nav puts its text
     at 20 (padding) + 26 (icon) + 20 (gap) = 66; the row is inset 20 for its
     pill, so the button carries the remaining 46. Without this the two lists
     start at different x and the sidebar reads as two unrelated columns —
     which is what the "slot stays so names align" comment always intended and
     never actually achieved. */
  #app.desk .dt-side .proj-row { padding-left: 0; }
  #app.desk .dt-side .proj-open { padding-left: 16px; }
}

@media (min-width: 1024px) {
  /* Project names group with the "Recents" label above them rather than with
     the nav icons, so both sit at 36. Aligning them to the nav labels at 66 was
     technically consistent but pushed the list far enough right that it read as
     indented under nothing. */
  #app.desk .dt-side .drawer-sec { padding-left: 36px; }

  /* --- taller footer, larger text --- */
  #app.desk .drawer-foot { height: 96px; padding: 0 16px 0 14px; }
  #app.desk .drawer-foot-account.compact { padding: 10px 12px; gap: 13px; }
  #app.desk .drawer-foot-account.compact .avatar-initials { width: 36px; height: 36px; font-size: 14px; }
  #app.desk .who-line { font-size: 19px; }
  #app.desk .foot-bug { width: 40px; height: 40px; }
  #app.desk .dt-side .drawer-scroll { padding-bottom: 112px; }

  /* The disclaimer sits in a band the same height as the sidebar footer and is
     centred in it, so the two line up across the divider instead of the note
     floating a few pixels lower than the account row. */
  #app.desk .dt-main .ai-note {
    bottom: 0;
    height: 96px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}

@media (min-width: 1024px) {
  /* Low sidebar section, one more step up. */
  #app.desk .drawer-foot { height: 108px; padding: 0 18px 0 16px; }
  #app.desk .drawer-foot-account.compact { padding: 12px 14px; gap: 15px; }
  #app.desk .drawer-foot-account.compact .avatar-initials { width: 42px; height: 42px; font-size: 16px; }
  #app.desk .who-line { font-size: 21px; }
  #app.desk .foot-chev svg { width: 22px; height: 22px; }
  #app.desk .foot-bug { width: 46px; height: 46px; }
  #app.desk .foot-bug svg { width: 24px; height: 24px; }
  #app.desk .dt-side .drawer-scroll { padding-bottom: 124px; }

  /* The disclaimer's band tracks the footer's height so the two stay level. */
  #app.desk .dt-main .ai-note { height: 108px; }
}

@media (min-width: 1024px) {
  /* The menu hangs off the ⋮ on the right, so it anchors to the right edge and
     needs no sidebar arithmetic at all — the two rules that used to live here
     offset it by the sidebar's width (and by the rail's when collapsed) to sit
     under the project title, which is no longer what opens it. `left: auto`
     matters: without it a stale `left` would fight the inline `right` and win,
     which is exactly what pinned the menu to the far side of the pane. */
  #app.desk .kebab-menu { left: auto; }
}

@media (min-width: 1024px) {
  /* The dialog's own type was set before the panes were dropped in, so its nav
     ended up the smallest text in the window while the content beside it ran at
     17-18px. Bring the two into one family: nav rows match the pane labels, and
     the "Settings" heading matches .prof-title rather than sitting 1px under it. */
  #app.desk .dt-nav-row { font-size: 17px; padding: 11px 12px; }
  #app.desk .dt-modal-title { font-size: 27px; }
}

/* ===== Search across chats ===== */
.srch {
  position: absolute; z-index: 32;
  /* Same reason as .fb-dialog: pop would drop translateX and the panel would
     slide in from the left edge on open. */
  top: calc(12vh / var(--ui-scale)); left: 0; right: 0; margin: 0 auto;
  width: min(680px, calc((100vw - 40px) / var(--ui-scale)));
  max-height: calc(70vh / var(--ui-scale));
  display: flex; flex-direction: column;
  background: var(--surface);
  border-radius: 16px;
  box-shadow: 0 24px 60px var(--shadow-strong);
  overflow: hidden;
  animation: pop 0.16s ease;
}
.srch-top {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--separator);
  flex: 0 0 auto;
}
.srch-ico { display: flex; color: var(--ink-2); flex: 0 0 auto; }
.srch-input {
  flex: 1; min-width: 0; border: none; background: none; outline: none;
  font-family: var(--sans); font-size: 18px; color: var(--ink);
}
.srch-input::placeholder { color: var(--ink-3); }
.srch-x { display: flex; color: var(--ink-2); flex: 0 0 auto; }
.srch-results { overflow-y: auto; padding: 6px; }
.srch-hint { margin: 0; padding: 22px 16px; font-size: 15px; color: var(--ink-2); text-align: center; }
.srch-hit {
  display: flex; flex-direction: column; gap: 4px; width: 100%;
  padding: 11px 12px; border-radius: var(--r); text-align: left;
}
.srch-hit:hover { background: var(--raised); }
.srch-hit-top { display: flex; align-items: center; gap: 8px; }
.srch-proj { font-size: 13px; font-weight: 500; color: var(--accent); }
.srch-who { font-size: 12px; color: var(--ink-3); }
.srch-snip { font-size: 15px; line-height: 1.45; color: var(--ink); }

@media (min-width: 1024px) {
  /* The sidebar's icons and the chat's project title are the same bar, split by
     the divider. Their centres were 13px apart (31 vs 44), which read as two
     unrelated headers rather than one row. The buttons are 34px and the title
     44, so the shorter one takes the difference in padding: 22 + (44-34)/2 = 27. */
  #app.desk .dt-side-top { padding-top: 27px; }
}

/* Feedback composer extras: the error line, and the quoted reply shown when the
   report was filed against one specific answer. */
.fb-err { margin: 10px 0 0; font-size: 13px; color: var(--danger); }
.fb-quote {
  margin-top: 12px; padding: 10px 12px;
  border-left: 3px solid var(--separator);
  background: var(--bg-top); border-radius: 0 8px 8px 0;
  font-size: 13px; line-height: 1.5; color: var(--ink-2);
  max-height: 96px; overflow: hidden;
}
.fb-quote-lbl { display: block; font-size: 11px; letter-spacing: 0.3px; color: var(--ink-3); margin-bottom: 4px; }

@media (min-width: 1024px) {
  /* 280px is a phone measure — at 18px type that is four or five words a line,
     and against a 1060px column it reads as a narrow ribbon pinned to the
     right. Pictures are outside the bubble now, so widening it no longer drags
     their size along with it. */
  #app.desk .msg-user-row { max-width: 66%; }
  #app.desk .msg-user-row.editing { max-width: 66%; }
  #app.desk .msg-user {
    max-width: 100%;
    padding: 14px 18px;
    border-radius: 14px 4px 14px 14px;
  }
}

/* Attachments staged inside the composer, above the text. The row has to force
   a wrap on its own: growComposer only adds .stacked from the textarea's own
   height, so a short caption with a picture attached would otherwise leave a
   full-width row inside a nowrap flex line. */
.composer.has-attach { flex-wrap: wrap; align-items: center; padding-top: 12px; }
/* The top/right padding is what stops the remove button being shaved off.
   overflow-x:auto computes overflow-y to auto too, which makes this a scroll
   container that crops anything outside its PADDING box — and .attach-x sits at
   top:-5px right:-5px, outside every chip. The chip's own overflow:visible was
   never the problem. */
.composer .attach-row { flex: 1 0 100%; order: -2; padding: 9px 9px 10px 2px; margin: 0; }
/* The textarea already claims order:-1 when stacked; this keeps the caption
   under the picture in the unstacked case too. */
.composer.has-attach textarea { flex: 1 0 100%; order: -1; padding: 0 0 8px; }

@media (min-width: 1024px) {
  /* Roomier chips to match the larger desktop composer. */
  #app.desk .attach-chip { width: 72px; height: 72px; }
  #app.desk .composer.has-attach { padding: 16px 16px 14px 20px; }

  /* Sent pictures: a real thumbnail, no bubble behind it — the image is the
     message. One size whether or not there are words with it. */
  #app.desk .msg-user-row .msg-atts:not(.multi) .msg-img {
    width: 340px;
    border-radius: 14px;
    box-shadow: 0 1px 3px rgba(14, 30, 44, 0.12);
  }
}

/* ===== Memory import dialog ==============================================
   A modal over the app, opened from the badge under an empty chat or from
   Settings -> Memory. It used to be a full screen, and a full screen borrowed
   onboarding's shape — a big title, a Skip button — so opening it from the
   badge read as being sent back into sign-up.

   Cloud White, not white: everything inside — the grey prompt box, the white
   paste field, the white note cards — was drawn against the page colour, and on
   a white panel the field and the cards lose their edge. */
/* Above the desktop Settings dialog (z 30) and its scrim, because import also
   opens from inside Settings. */
.imp-scrim { z-index: 35; }
/* pointer-events off, so a click in the space beside the box falls through to
   the scrim underneath. */
.imp-layer {
  position: absolute; inset: 0; z-index: 36;
  display: flex; align-items: center; justify-content: center;
  padding: 24px 16px;
  pointer-events: none;
}
.imp-dialog {
  pointer-events: auto;
  display: flex; flex-direction: column;
  width: min(580px, 100%);
  max-height: 100%;
  background: var(--bg-top);
  border-radius: 18px;
  box-shadow: 0 30px 80px var(--shadow-strong), 0 0 0 1px rgba(var(--ink-2-rgb), 0.08);
  outline: none;
  animation: pop 0.18s ease;
}
/* no-anim before closing, so a closing dialog still plays its exit. */
.imp-dialog.no-anim { animation: none; }
.imp-dialog.closing { animation: popout 0.16s ease forwards; }

.imp-head { display: flex; align-items: flex-start; gap: 12px; padding: 22px 16px 8px 24px; }
.imp-head-text { flex: 1; min-width: 0; }
.imp-title {
  margin: 0;
  font-family: var(--serif); font-size: 28px; font-weight: 700; line-height: 1.15;
  color: var(--ink);
}
.imp-sub { margin: 6px 0 0; font-size: 14px; line-height: 1.4; color: var(--ink-2); }
/* The way out. A rounded square, like every other icon button in the app. */
.imp-close {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; margin-top: -2px; padding: 0;
  border-radius: var(--r);
  color: var(--ink-2);
  transition: background 0.12s ease, color 0.12s ease;
}
.imp-close:active { background: rgba(var(--ink-2-rgb), 0.16); color: var(--ink); }
@media (hover: hover) {
  .imp-close:hover { background: rgba(var(--ink-2-rgb), 0.10); color: var(--ink); }
}
/* Only the middle scrolls. The title, the × and the button stay put however
   long the paste or the list of results gets. */
.imp-body {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 0 24px 4px;
}
/* The body is a flex column, and flex items shrink by default — so a long
   result list or a short window SQUASHED the children to fit instead of letting
   the body scroll. The paste field shrank, and the prompt box, being
   overflow:hidden, is allowed to shrink all the way to nothing. They keep their
   own size; the body scrolls. */
.imp-body > * { flex-shrink: 0; }
.imp-body > .imp-row:first-child { margin-top: 6px; }
.imp-foot {
  flex: 0 0 auto;
  display: flex; justify-content: flex-end; gap: 10px;
  padding: 16px 24px 20px;
}
.imp-empty { margin: 4px 0 0; font-size: 15px; line-height: 1.5; color: var(--ink-2); }

/* A phone: the same dialog, a little tighter, so the steps are not squeezed
   under an oversized title. */
@media (max-width: 480px) {
  .imp-layer { padding: 16px 12px; }
  .imp-head { padding: 18px 10px 6px 18px; }
  .imp-title { font-size: 24px; }
  .imp-body { padding: 0 18px 4px; }
  .imp-foot { padding: 14px 18px 16px; }
}

/* Step rows: a numbered badge and its instruction on one baseline. */
.imp-row {
  display: flex; align-items: center; gap: 10px;
  align-self: stretch; margin: 16px 0 8px;
}
.imp-num {
  flex: 0 0 auto;
  width: 22px; height: 22px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  /* NOT --raised: that is Cloud White, the same value as the onboarding
     background, so the circle was invisible and the digits floated loose. A
     tint of Blue Gray reads on any near-white ground in the palette. */
  background: rgba(var(--ink-2-rgb), 0.13); color: var(--ink-2);
  font-size: 12px; font-weight: 600; line-height: 1;
}
/* Blue Gray, not ink: these number the task, they are not the thing being
   read. At full ink they competed with the title above them. */
.imp-label { font-size: 14px; font-weight: 400; color: var(--ink); line-height: 1.35; }

/* The prompt to take away.
   Sized to be COPIED, not read: three lines is enough to show what it is, and
   at 132px the block was mostly dead space with a half-line of "Categories, in
   this order:" stranded under the fade. Shorter also lifts step 2 above the
   fold on a 402px screen, which is the step people actually have to act on. */
.imp-prompt {
  align-self: stretch; position: relative;
  /* A soft Blue Gray wash, NOT --raised. --raised is #F4F9FC, the same value
     as the page, so it read as an outlined hole rather than a block; and fully
     transparent was no better. A tint sits it slightly BELOW the page, which
     is right for something quoted and read-only, and keeps it distinct from
     the white input below that the user actually types into. */
  background: rgba(var(--ink-2-rgb), 0.07);
  border: 1px solid rgba(var(--ink-2-rgb), 0.22);
  border-radius: var(--r);
  overflow: hidden;
}
.imp-prompt-scroll {
  /* 44px of bottom padding is the button (30px) plus its 9px offset plus a
     hair: without it the last visible line ran under the Copy button, which the
     narrower text column made worse. */
  max-height: 108px; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 13px 13px 44px;
  /* The default Windows scrollbar is 17px of grey furniture on a 13px block.
     A hairline says "scrollable" without becoming the loudest thing in it. */
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--ink-2-rgb), 0.3) transparent;
}
.imp-prompt-scroll::-webkit-scrollbar { width: 6px; }
.imp-prompt-scroll::-webkit-scrollbar-track { background: transparent; }
.imp-prompt-scroll::-webkit-scrollbar-thumb {
  background: rgba(var(--ink-2-rgb), 0.28); border-radius: 999px;
}
.imp-prompt p {
  margin: 0; font-size: 13px; line-height: 1.5;
  color: var(--ink-2); white-space: pre-wrap; user-select: text;
}
/* Fades the clipped line, so a cut block reads as "there is more" rather than
   as a sentence that stops. */
.imp-prompt-fade {
  position: absolute; left: 0; right: 0; bottom: 0; height: 38px;
  /* Reaches full opacity late. At 68% over 46px the third line dropped to
     about a quarter visible, which reads as a rendering fault rather than as
     text continuing below. */
  /* Fades to the block's own fill — the tint over Cloud White resolves to
     this, so the last line disappears into the block rather than into the page. */
  background: linear-gradient(to bottom, rgba(233, 239, 243, 0), rgba(233, 239, 243, 1) 88%);
  pointer-events: none;
}
/* Pinned bottom-right over the fade, where a copy affordance is looked for.
   It is the one thing on this step the user must actually do, so it carries
   the accent rather than sitting as another grey outline among grey outlines. */
.imp-copy {
  position: absolute; right: 9px; bottom: 9px;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 30px; padding: 0 13px; border-radius: 999px;
  background: var(--surface);
  border: 1px solid rgba(10, 102, 194, 0.35);
  color: var(--accent); font-size: 13px; font-weight: 600;
}
/* The copy glyph draws its front square to y=19.75 in a 20-unit viewBox and
   strokes it at 1.5, so the bottom half-stroke needs 20.5 and was being
   clipped by the svg viewport. Same cause as the New Project icon. */
.imp-copy svg { display: block; overflow: visible; }
.imp-copy-ico { display: flex; align-items: center; }
.imp-copy:active { background: var(--accent-wash); }
/* Confirmed state: the label already changes to "Copied", but on a screen the
   user is about to leave, the colour is what carries at a glance. */
.imp-copy.done { background: var(--accent-wash); }

.imp-hint {

  align-self: stretch; margin-top: 8px;
  font-size: 13px; line-height: 1.45; color: var(--ink-2);
}

.imp-paste {
  align-self: stretch; width: 100%; height: 132px; resize: none;
  padding: 14px; border: none; outline: none;
  background: var(--surface); border-radius: var(--r);
  box-shadow: 0 0 4px var(--shadow);
  font-family: var(--sans); font-size: 15px; font-weight: 300;
  line-height: 1.45; color: var(--ink);
}
.imp-paste::placeholder { color: var(--ink-2); font-weight: 300; }
.imp-paste:focus { box-shadow: 0 0 0 2px rgba(10, 102, 194, 0.35); }

/* The promise and the counter share a line. Separately they were two centred
   rows of small grey text stacked above the buttons; together they read as one
   caption, and the reassurance rises to where the eye already is. */
/* Only the counter lives here now — the promise moved to the results screen,
   where it answers a question the user is actually asking. */
.imp-meta {
  display: flex; align-items: baseline; justify-content: flex-end;
  align-self: stretch; margin-top: 8px;
}
.imp-count {
  flex: 0 0 auto;
  font-size: 12px; color: var(--ink-3); white-space: nowrap;
}
/* Over the cap is not an error — the paste still works, it is just clipped. */
.imp-count.over { color: var(--ink-2); white-space: normal; text-align: right; }

.imp-err {
  align-self: stretch; margin-top: 12px;
  font-size: 13px; line-height: 1.45; color: var(--danger);
}


/* Box text lines up with the NUMBER column, not with the label beside it.
   Indented to the label edge the two boxes read as hanging off it; pulled back
   they sit under the 1 and the 2, and the badges read as marking the whole
   step rather than just its heading.
   12 vs 13 because .imp-prompt carries a 1px border and .imp-paste does not —
   both land their text on the same x. */
.imp-prompt-scroll { padding-left: 12px; }
.imp-paste { padding-left: 13px; }
.imp-meta { padding-left: 13px; }

/* The dialog's one action: right-aligned, only as wide as its label. */
.imp-btn {
  flex: 0 0 auto; min-height: 40px; padding: 0 22px;
  border-radius: var(--r);
  font-size: 15px; font-weight: 600;
  display: flex; align-items: center; justify-content: center;
}
.imp-btn.filled { background: var(--accent); color: #fff; }
.imp-btn.filled:active { background: var(--accent-press, var(--accent)); }
/* A tinted accent at 45% still reads as a live button — people tap it and
   nothing happens. Disabled drops the accent entirely. */
.imp-btn.filled:disabled {
  background: rgba(var(--ink-2-rgb), 0.13);
  color: var(--ink-3);
  opacity: 1;
}

/* The payoff list. Reuses .settings-card / .note-line from the Memory screen
   verbatim, so the notes look the way they will look once they are saved. */
.imp-scroll { align-self: stretch; width: 100%; margin-top: 4px; }
.imp-dialog .settings-section { padding-left: 0; padding-right: 0; }
.imp-dialog .note-line { cursor: default; }

/* "More remains" — the other AI said its export was partial, so offer a second
   paste rather than letting the user believe they got everything. */
.imp-more {
  align-self: stretch; margin-top: 14px; padding: 12px 14px;
  background: var(--accent-wash);
  border-radius: var(--r);
  font-size: 13px; line-height: 1.45; color: var(--ink-2); text-align: center;
}
.imp-more-btn {
  padding: 0 4px; font-size: 13px; font-weight: 600;
  color: var(--accent); align-self: auto;
}
/* Project rows carry a name in bold plus its description, so they wrap where a
   one-line note does not. */
.imp-dialog .note-line b { font-weight: 600; }

/* ===== Import nudge — one-time offer under the composer on an empty project ==
   An offer, not a banner: no fill, no border, no accent, and the same weight as
   the disclaimer below it. Anyone who wants it will find it; anyone who does not
   can ignore it without having been interrupted. */
/* A slot that is ALWAYS in the layout, holding the nudge or nothing.

   The composer is bottom-anchored on a phone and centred by auto margins on
   desktop, so anything that changes this wrapper's height moves it — in flow
   the nudge dropped the composer 46px the moment it was dismissed. Absolute
   positioning fixed that but required .composer-wrap to be relative, which
   re-parented the desktop disclaimer (absolute, bottom-pinned to .dt-main) on
   top of the composer. Reserving the height instead costs one always-empty
   band and breaks nothing. */
/* The slot is the same height whether or not the badge is in it, so the
   composer never moves when the offer is dismissed. It tracks the badge's own
   height — 32px — rather than the old line of text's 22px. */
.imp-nudge-slot {
  height: 32px; margin-top: 20px;
  display: flex; align-items: center; justify-content: center;
}
/* A real badge: white pill, hairline, a lift of shadow. The accent is kept to
   the memory mark and the arrow, so it reads as an offer rather than a button
   competing with the composer above it. */
.imp-nudge {
  display: inline-flex; align-items: center;
  height: 32px; max-width: 100%;
  padding: 0 4px 0 0;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid rgba(var(--ink-2-rgb), 0.16);
  box-shadow: 0 1px 3px rgba(14, 30, 44, 0.06);
  transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease, transform 0.12s ease;
}
.imp-nudge-cta {
  display: inline-flex; align-items: center; gap: 7px;
  height: 100%; min-width: 0;
  padding: 0 8px 0 12px;
  border-radius: 999px 0 0 999px;
  font-size: 13px; line-height: 1; font-weight: 500;
  color: var(--ink-2);
  white-space: nowrap;
  transition: color 0.15s ease;
}
.imp-nudge-ico { display: flex; flex: 0 0 auto; color: var(--accent); }
.imp-nudge-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.imp-nudge-arrow { display: inline-block; color: var(--accent); transition: transform 0.15s ease; }
/* Separates accepting the offer from dismissing it, inside one pill. */
.imp-nudge-sep {
  flex: 0 0 auto; width: 1px; height: 16px;
  background: rgba(var(--ink-2-rgb), 0.16);
}
.imp-nudge-x {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; margin-left: 4px; padding: 0;
  border-radius: 8px;
  color: var(--ink-3);
  transition: background 0.12s ease, color 0.12s ease;
}
/* Drawn 24px to sit inside a 32px pill, which is half the 44px a finger needs,
   on the one control someone reaches for having already decided against the
   offer. An invisible box carries the tap; it grows right, past the pill's
   edge, and barely left, so it cannot take clicks meant for the CTA. */
.imp-nudge-x::after {
  content: ''; position: absolute;
  top: 50%; transform: translateY(-50%);
  left: -4px; right: -12px;
  height: 44px;
}
/* Press — outside the hover query, since it is the only feedback a phone gets.
   The whole pill gives under a finger on the CTA; the cross has its own. */
.imp-nudge:has(.imp-nudge-cta:active) {
  transform: scale(0.98);
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
}
.imp-nudge-x:active { background: rgba(var(--ink-2-rgb), 0.16); color: var(--ink); }
/* Narrow phones: the full sentence does not fit beside the dismiss, and an
   ellipsis mid-name ("ChatGPT or Cla…") reads as broken. The short form keeps
   the verb and the noun. */
/* 400, not 380: the full badge needs a 357px gutter, and at 390 (iPhone 12-15)
   the gutter is 358 — a 1px fit that any font-rendering difference turns into
   an ellipsis. 402 and wider keep the whole sentence. */
@media (max-width: 400px) {
  .imp-nudge-from { display: none; }
}


/* The way back into import once onboarding is over.
   Plain text, bottom of the Memory screen, no chevron and no row treatment: it
   is a once-ever action, so it should be findable by someone looking for it and
   invisible to everyone else. As a .mem-link it would sit level with Global
   memory, which people open all the time. */
.mem-import {
  display: block; margin: 34px 0 4px; padding: 6px 9px 6px 9px;
  font-size: 15px; font-weight: 300; line-height: 1.45;
  color: var(--ink-2); text-align: left;
}
.mem-import:active { color: var(--ink); }

/* "Already have an account? Sign in" — on every onboarding question screen.
   Onboarding runs before sign-in, so without this a returning user has to
   answer the questions again just to reach a login screen.

   Quieter than Skip and set apart from it: Skip is a step in the flow they are
   in, this leaves the flow entirely. Only the words "Sign in" carry weight,
   which is what makes it findable without competing with the primary button. */
.textbtn.have-acct {
  /* .textbtn centres itself with align-self, which does nothing on the
     integrations screen — there the button sits inside a plain .pad block, so
     the link shrank to its text and aligned left under a full-width button.
     Own width and own text-align, so it centres under the button in either
     parent. */
  display: block; width: 100%; text-align: center;
  margin-top: 16px;
  font-size: 14px; font-weight: 400; color: var(--ink-3);
}
.textbtn.have-acct b { font-weight: 600; color: var(--accent); }
.textbtn.have-acct:active { opacity: 0.6; }

/* ===== Sign-in as the last step of onboarding =======================
   Sign-in used to be the FIRST thing after the welcome screen, where a bare
   card needed no explanation — nothing had happened yet. It is now the last
   step of a flow the user has just worked through, and without the logo, the
   reason, or the app's own background it read as a dead end they had been
   dropped on rather than the end of something they did. */
.login-why {
  margin: 10px 0 0;
  font-size: 15px; line-height: 1.45; font-weight: 400;
  color: var(--ink-2); text-align: center;
}

@media (min-width: 1024px) {
  /* 62px under the mark carries the welcome screen's headline. Here it would
     push the card off a 900px window. */
  .login-desk .wd-mark-sm { margin-bottom: 22px; }
  .login-desk .login-why { font-size: 17px; margin-top: 12px; }
  /* The card answers the line above it, so it sits closer to it than the
     40px that separated it from a lone heading. */
  .login-desk .wd-card { margin-top: 26px; }
}

/* ===== Hover =========================================================
   The app was built phone-first, so most controls had a :active press state
   and nothing for a pointer — on desktop, rows and icon buttons gave no sign
   they were clickable until you clicked them.

   All of it behind @media (hover: hover). A touch browser resolves :hover on
   tap and then leaves it stuck until you touch something else, so a row you
   tapped would stay lit as though it were selected.

   The wash is a translucent ink rather than --raised, and deliberately: --raised
   is the same value as the page background, so it shows up on the white drawer
   and vanishes on a settings screen. An alpha wash works on both. */
@media (hover: hover) {
  /* --- rows: the sidebar, settings, and the menus that share their shape --- */
  /* 3% of --hover-base, landing on #F9FBFC.
     Faint by necessity, not by taste: the selected row is Cloud White
     (#F4F9FC), only about seven points off the drawer's white, so the whole
     hover range has to fit inside that gap or it outranks selection. The tint
     is what took the tuning — 6% grey read neutral, 8% blue came out darker
     than selected, 3% pure blue still read blue. */
  .drawer-item:not(.is-soon):hover,
  .set-row:not(.is-soon):hover,
  .about-row:not(.about-version):hover,
  .mode-row:not(.sel):hover,
  .pm-row:hover {
    background: color-mix(in srgb, var(--hover-base) 3%, transparent);
  }
  /* Already rounded and inset, and .active already owns --raised — hovering an
     inactive row must read as lighter than the selected one, not equal to it. */
  .proj-row:not(.active):hover { background: color-mix(in srgb, var(--hover-base) 3%, transparent); }

  /* --- icon buttons: square with rounded corners, not a disc --- */
  /* Hovering the offer lifts the whole badge and nudges the arrow toward where
     it leads; hovering the cross highlights only the cross. */
  .imp-nudge:has(.imp-nudge-cta:hover) {
    border-color: color-mix(in srgb, var(--accent) 35%, transparent);
    box-shadow: 0 3px 10px rgba(14, 30, 44, 0.08);
  }
  .imp-nudge-cta:hover { color: var(--ink); }
  .imp-nudge-cta:hover .imp-nudge-arrow { transform: translateX(3px); }
  .imp-nudge-x:hover { background: rgba(var(--ink-2-rgb), 0.10); color: var(--ink); }

  /* --- the blue buttons, everywhere they appear --- */
  /* Deep Sky, the palette's own darker blue, and already what .fb-btn.primary
     uses on hover — so blue buttons now agree with each other instead of this
     one place inventing a shade. It also flips with the theme (#053666 light,
     #2A6FB8 dark), which a hardcoded darkening does not: mixing the accent
     toward black is backwards on a dark ground, where hover has to get lighter.
     The shadow grows with it so the button lifts under the pointer, then
     settles on :active, which already scales it down. */
  .btn:not([disabled]):hover,
  .prof-btn:not([disabled]):hover,
  .imp-btn.filled:not([disabled]):hover {
    background: color-mix(in srgb, var(--accent) 75%, var(--deep-sky));
    box-shadow: 0 6px 12px rgba(14, 30, 44, 0.24);
  }
  .btn, .prof-btn, .imp-btn.filled { transition: transform 0.12s ease, opacity 0.12s ease, background 0.12s ease, box-shadow 0.12s ease; }

  /* Secondary and text controls, so the whole flow responds consistently. */
  .textbtn:hover { color: var(--ink); }
  .textbtn.have-acct:hover b { text-decoration: underline; }
  .imp-copy:hover { background: var(--accent-wash); }
  .mem-import:hover { background: color-mix(in srgb, var(--hover-base) 3%, transparent); border-radius: var(--r); }
}

/* The press.
   Full Deep Sky is too much for a pointer merely resting on a button — it read
   as a state change rather than a highlight. It is right for the press, where
   the button is being held and the question is whether it registered, so hover
   takes a quarter of the way there and holding it down goes the whole way.

   Outside the hover query on purpose: a press is the one state a touch screen
   does have, and it is the only feedback a phone gets. */
.btn:not([disabled]):active,
.prof-btn:not([disabled]):active,
.imp-btn.filled:not([disabled]):active {
  background: var(--deep-sky);
  box-shadow: 0 2px 5px rgba(14, 30, 44, 0.22);
}
/* .btn already scales; the others did not move at all under a finger. */
.prof-btn:not([disabled]):active,
.imp-btn.filled:not([disabled]):active { transform: scale(0.98); }
.prof-btn, .imp-btn.filled { transition: transform 0.12s ease, background 0.12s ease, box-shadow 0.12s ease; }

/* ===== Message actions ==============================================
   Hover for the six buttons under a message — copy / share / edit on your own,
   copy / share / more on SkyOS's. They had a press state and nothing for a
   pointer, so on desktop there was no sign which glyph you were about to hit.
   Rounded square, matching every other icon button. */
@media (hover: hover) {
  .actions button:hover {
    background: rgba(var(--ink-2-rgb), 0.10);
    color: var(--ink);
  }
}
/* The assistant's row has no padding of its own, so its buttons need the same
   box the user's already have or the highlight would hug the glyph. */
.actions button { border-radius: 6px; transition: background 0.12s ease, color 0.12s ease; }
.msg-ai .actions button { width: 30px; height: 30px; padding: 5px; align-items: center; justify-content: center; }
/* The row is 20px-gapped for reading; with a visible highlight the boxes need
   to not touch, but the glyph rhythm should not change either — the padding
   above already carries it. */

/* Wrapping is never right for this strip: three icons on two lines under a
   one-word message read as two separate controls. */
.actions.user-actions { flex-wrap: nowrap; }

/* ...and when they genuinely do not fit, they move beside the bubble instead
   of under it. placeUserActions() measures and sets this. */
.msg-user-row.acts-side .user-actions {
  top: 50%;
  bottom: auto;
  left: auto;
  right: calc(100% + 8px);
  transform: translateY(-50%);
  width: max-content;
  margin-top: 0;
}
/* The tooltip hangs below each button; beside a bubble there is room for it,
   but it must not push the transcript sideways. */
.msg-user-row.acts-side .user-actions button::after { left: auto; right: 0; transform: none; }

/* ===== Topbar and composer icon buttons =============================
   The topbar pair already had a hover — var(--raised) — which is the same
   value as the chat background it sits on, so it painted nothing. The composer
   trio had no hover at all. Same wash as the message actions, so every icon
   button in the app answers the pointer the same way. */
@media (hover: hover) {
  #app.desk .dt-topbar .circle-btn:hover {
    background: rgba(var(--ink-2-rgb), 0.10);
    color: var(--ink);
  }
  /* + and the mic are bare glyphs with no box of their own. Widening them would
     move the composer's layout, so the highlight is a pseudo-element centred
     behind the icon — ::before, because ::after is already carrying the 44px
     tap target, and because content paints above it without needing z-index. */
  .composer .plus:hover::before,
  .composer .mic:hover::before { background: rgba(var(--ink-2-rgb), 0.10); }
  /* The orb is a filled accent button, so it follows the blue buttons rather
     than the icon buttons: a quarter toward Deep Sky on hover. */
  .composer .orb:hover { background: color-mix(in srgb, var(--accent) 75%, var(--deep-sky)); }
}
#app.desk .dt-topbar .circle-btn { border-radius: var(--r); transition: background 0.12s ease, color 0.12s ease; }
.composer .plus::before,
.composer .mic::before {
  content: ''; position: absolute;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  /* 44px, the same box as the tap target underneath. 34 cleared a 24px glyph
     by five pixels a side and read as an outline; 40 was still tight. Purely
     decorative — pointer-events off so the mic's box, which reaches under the
     orb sitting 3px beside it, cannot take the orb's clicks. */
  width: 44px; height: 44px; border-radius: var(--r);
  background: transparent;
  pointer-events: none;
  transition: background 0.12s ease;
}
/* The press, on touch as well as pointer. */
.composer .plus:active::before,
.composer .mic:active::before { background: rgba(var(--ink-2-rgb), 0.16); }
.composer .orb { transition: background 0.12s ease, transform 0.12s ease; }
.composer .orb:active { background: var(--deep-sky); transform: scale(0.96); }
#app.desk .dt-topbar .circle-btn:active { background: rgba(var(--ink-2-rgb), 0.16); }

/* The mic glyph is not centred in its own artwork: FIG.mic's viewBox is
   0 0 24 24.4364, and the ink runs x=4 to x=22.01, so its centre sits at 13
   against a viewBox centre of 12. Measured, not guessed — one unit right, which
   is 0.92px at the 22px it renders at. Everything else about the icon is
   centred, so only x moves. */
.composer .mic::before { left: calc(50% + 0.92px); }
