/* introby.me — signed-in app shell + design tokens.
   Loaded only by layouts/app.html.erb, so the landing page is untouched.
   Purple budget: brand mark, links, and the active nav state only. Everything
   else (primary buttons, avatar, icon tiles) is neutral. */

:root {
  --font: ui-sans-serif, system-ui, -apple-system, "SF Pro Text", "Segoe UI", sans-serif;

  --bg: #fbfbfd;
  --surface: #ffffff;
  --ink: #1d1d1f;
  --ink-hover: #3a3a3d;      /* ~22% lift from --ink — the filled-button hover (lighten, not darken). Stronger lift than the original ~10% so the delta reads cleanly against the near-black base. */
  --ink-2: #6e6e73;
  --ink-3: #a1a1a6;
  --line: rgba(0, 0, 0, 0.08);
  --line-2: rgba(0, 0, 0, 0.05);
  --fill: #f1f1f3;          /* static neutral fill (tiles, inputs) */
  --hover: rgba(0, 0, 0, 0.07); /* neutral control hover/active state */

  --accent-rgb: 108 92 231;
  --accent: rgb(var(--accent-rgb));
  --accent-soft: rgb(var(--accent-rgb) / 0.10);

  --danger-rgb: 192 57 43;                      /* #c0392b — Flat UI pomegranate, as a triple */
  --danger: rgb(var(--danger-rgb));             /* validation errors & destructive actions */
  --danger-soft: rgb(var(--danger-rgb) / 0.10); /* soft pill behind a destructive action on hover */

  --radius-sm: 12px;
  --radius: 18px;
  --radius-lg: 24px;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 10px 30px rgba(0, 0, 0, 0.06);
  --shadow-pop: 0 8px 28px rgba(0, 0, 0, 0.12);

  /* Icon-button size ramp. Pick from these; don't invent in-between sizes. */
  --control: 40px;          /* touch targets & floating-over-content controls */
  --control-sm: 32px;       /* compact icon buttons inside the chrome */

  --sidebar-w: 248px;
  --sidebar-collapsed-w: 64px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---------- App shell ---------- */
.app {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  min-height: 100vh;
  transition: grid-template-columns 0.22s ease;
}
.app__main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  /* Query container for the timeline header's avatar-clearance gutter. The collision
     is driven by THIS column's width (viewport minus the variable sidebar), not the
     viewport — so a viewport media query can't be right for both the 248px and 64px
     sidebar states. Keying the gutter off app__main's own width is correct for every
     sidebar state (expanded, collapsed, drawer) for free. */
  container: app-main / inline-size;
}
/* Mobile-only chrome (floating menu button, drawer scrim, drawer close button);
   hidden until the phone breakpoint, so the desktop shell is untouched. */
.app__menu,
.app__scrim,
.sidebar__close { display: none; }
/* left sidebar collapsed — narrows to an icon strip (ChatGPT-style). The icons and
   the separation line stay; labels, the intro list, and the wordmark hide. */
.app--sidebar-collapsed { grid-template-columns: var(--sidebar-collapsed-w) 1fr; }
.app--sidebar-collapsed .sidebar-toggle { display: none; }
.app--sidebar-collapsed .nav-item { width: var(--control-sm); }
.app--sidebar-collapsed .nav-item span,
.app--sidebar-collapsed .sidebar__section { display: none; opacity: 0; }
.app--sidebar-collapsed .sidebar__logo { opacity: 0; pointer-events: none; }

/* fade labels back in as the sidebar expands, instead of popping at full opacity.
   Gated on app--sidebar-animate (added a frame after connect) so a fresh page
   render paints labels at full opacity rather than re-running the fade. */
@starting-style {
  .app--sidebar-animate:not(.app--sidebar-collapsed) .sidebar__logo,
  .app--sidebar-animate:not(.app--sidebar-collapsed) .nav-item span,
  .app--sidebar-animate:not(.app--sidebar-collapsed) .sidebar__section { opacity: 0; }
}

/* ---------- Sidebar ---------- */
.sidebar {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  padding: 1.4rem 0.8rem 1rem;
  background: var(--surface);
  border-right: 1px solid var(--line);
}
.sidebar__brand {
  position: relative;
  display: flex; align-items: center; gap: 0.5rem;
  height: calc(2rem + 1.55rem);
  padding: 0.25rem 0 1.3rem;
}
.sidebar__logo {
  position: absolute;
  /* nudge so the wordmark's first glyph centers on the 32px icon column (where the
     collapsed dot and the nav icons sit) — keeps the brand from sliding across the fade */
  left: 3.6px; top: 0.25rem;
  display: inline-flex; align-items: center;
  font-size: 1.1rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--ink);
  white-space: nowrap;
  padding: 0.3rem 0.65rem;
  border-radius: 8px;
  transition: background 0.15s ease;
}
.app--sidebar-animate .sidebar__logo { transition: background 0.15s ease, opacity 0.2s ease; }
.sidebar__logo:hover { text-decoration: none; background: var(--hover); }
.sidebar__logo span { color: var(--accent); }

/* persistent brand mark: a pulsing dot anchored at the icon column in both
   states (it morphs to the expand icon on hover while collapsed). Stays put as
   the wordmark fades, so the brand never jumps. */
.sidebar__mark {
  position: absolute;
  left: 0; top: 0.25rem;
  width: var(--control-sm); height: var(--control-sm);
  display: grid; place-items: center;
  border: none; background: none; cursor: pointer;
  color: var(--ink-3);
  border-radius: var(--radius-sm);
  opacity: 0; pointer-events: none;
  transition: opacity 0.2s ease, background 0.15s ease, color 0.15s ease;
}
.app--sidebar-collapsed .sidebar__mark { opacity: 1; pointer-events: auto; }
.sidebar__mark:hover { background: var(--hover); color: var(--ink-2); }
.sidebar__mark-glyph {
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0.55);
  animation: brandPulse 2.4s infinite;
  transition: opacity 0.12s ease;
}
/* promote the dot and the panel icon to their own compositor layers (translateZ)
   so each rasterizes once and the dot→panel morph only composites opacity. without
   this the panel svg is re-rasterized at fractional sub-pixel offsets while its
   opacity ramps, so it visibly wobbles ~1px right after it appears. */
.sidebar__mark svg {
  position: absolute; inset: 0; margin: auto;
  width: 20px; height: 20px;
  opacity: 0; transition: opacity 0.12s ease;
  transform: translateZ(0);
}
.app--sidebar-collapsed .sidebar__mark:hover .sidebar__mark-glyph { opacity: 0; animation: none; }
.app--sidebar-collapsed .sidebar__mark:hover svg { opacity: 1; }

.sidebar__nav { display: flex; flex-direction: column; gap: 0.15rem; }

.nav-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0;
  border-radius: var(--radius-sm);
  color: var(--ink-2);
  font-size: 0.95rem;
  font-weight: 500;
  transition: background 0.15s ease, color 0.15s ease;
}
.nav-item:hover { background: var(--hover); color: var(--ink); text-decoration: none; }
/* promote each nav icon to its own compositor layer so it rasterizes once and
   composites at the same offset in both sidebar states — otherwise the plus
   icon's dead-centre hairline snaps ~1px between collapsed/expanded (the
   distributed "about" strokes don't, which is why only the plus drifts). */
.nav-item svg { width: var(--control-sm); height: var(--control-sm); padding: 6px; flex: none; transform: translateZ(0); }
.nav-item span { white-space: nowrap; }
.app--sidebar-animate .nav-item span { transition: opacity 0.2s ease, display 0.2s allow-discrete; }
/* Active item marked by accent colour + soft fill only — no weight bump, so navigating
   doesn't thicken the just-clicked label (and bold's wider metrics can't nudge the row). */
.nav-item[aria-current="page"] { background: var(--accent-soft); color: var(--accent); }

/* the list of made intros, below the nav (ChatGPT-style history) */
.sidebar__section {
  margin-top: 1.3rem;
  min-height: 0;
  display: flex; flex-direction: column;
}
.app--sidebar-animate .sidebar__section { transition: opacity 0.2s ease, display 0.2s allow-discrete; }
.sidebar__label {
  padding: 0 0.65rem 0.35rem;
  font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--ink-3);
  white-space: nowrap;
  margin: 0;
}
.sidebar__list { overflow-y: auto; display: flex; flex-direction: column; gap: 0.05rem; }
.introduction-link {
  display: block;
  padding: 0.5rem 0.65rem;
  border-radius: var(--radius-sm);
  color: var(--ink-2);
  font-size: 0.9rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.introduction-link:hover { background: var(--hover); color: var(--ink); text-decoration: none; }

/* ---------- Collapse controls ---------- */
.sidebar-toggle {
  display: grid; place-items: center;
  border: none; background: none; cursor: pointer;
  color: var(--ink-3);
  border-radius: var(--radius-sm);
  transition: background 0.15s ease, color 0.15s ease;
  width: var(--control-sm); height: var(--control-sm); flex: none; margin-left: auto;
}
.sidebar-toggle svg { width: 20px; height: 20px; }
.sidebar-toggle:hover { background: var(--hover); color: var(--ink-2); }

@keyframes brandPulse {
  0%   { box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0.5); }
  70%  { box-shadow: 0 0 0 9px rgb(var(--accent-rgb) / 0); }
  100% { box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0); }
}

/* ---------- Tooltip hints (ChatGPT-style) ----------
   A single hinted toggle today; the target is a vertical strip of hinted icon
   buttons (see docs/architecture/3-ui-design.md). */
/* :where() keeps this at zero specificity so a positioned component (e.g. the
   absolutely-placed .sidebar__mark) always wins without a specificity war. */
:where([data-tip]) { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute; left: calc(100% + 8px); top: 50%; transform: translateY(-50%);
  background: var(--ink); color: #fff;
  font-size: 0.8rem; font-weight: 500;
  padding: 0.35rem 0.6rem; border-radius: 8px; white-space: nowrap;
  opacity: 0; pointer-events: none; transition: opacity 0.12s ease;
  box-shadow: var(--shadow-pop);
  z-index: 70;
}
[data-tip]:hover::after { opacity: 1; }
.tip-left[data-tip]::after { left: auto; right: calc(100% + 8px); }

/* nav items carry their label as a tooltip, but only show it while collapsed
   (expanded, the label is already visible beside the icon). */
.nav-item[data-tip]:hover::after { opacity: 0; }
.app--sidebar-collapsed .nav-item[data-tip]:hover::after { opacity: 1; }

/* ---------- Top-right account (Google-style, native <details>) ---------- */
.usermenu { position: fixed; top: 1rem; right: 1.25rem; z-index: 50; }
.usermenu__avatar {
  width: var(--control); height: var(--control);
  display: grid; place-items: center;
  border-radius: var(--radius-sm);
  cursor: pointer;
  list-style: none;
  user-select: none;
  transition: background 0.15s ease, transform 0.06s ease;
}
.usermenu__avatar::-webkit-details-marker { display: none; }
.usermenu__avatar:hover { background: var(--hover); }
.usermenu__avatar:active { transform: scale(0.96); }
.usermenu__disc {
  width: 26px; height: 26px;
  border-radius: 50%;
  background: #3a3a3c;
  color: #fff;
  display: grid; place-items: center;
  font-size: 0.78rem; font-weight: 600;
  box-shadow: var(--shadow-sm);
}
.usermenu__panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  min-width: 232px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
  padding: 0.4rem;
  overflow: hidden;
}
.usermenu__email {
  padding: 0.6rem 0.7rem;
  font-size: 0.85rem;
  color: var(--ink-2);
  border-bottom: 1px solid var(--line-2);
  margin-bottom: 0.3rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.usermenu__signout {
  display: flex; align-items: center; gap: 0.6rem;
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: none; background: none; cursor: pointer;
  border-radius: var(--radius-sm);
  color: var(--ink);
  font: inherit; font-size: 0.9rem;
}
.usermenu__signout:hover { background: var(--hover); }
.usermenu__signout svg { width: 18px; height: 18px; color: var(--ink-2); }

/* ---------- Buttons ---------- */
/* Base color is --ink so a link-as-button (link_to … class: "btn") stops inheriting the
   global `a { color: var(--accent) }` — this is the Cancel-is-purple fix (§12.1). Filled
   and destructive variants override from here. */
.btn {
  display: inline-flex; align-items: center; gap: 0.45rem;
  font: inherit; font-size: 0.92rem; font-weight: 600;
  padding: 0.6rem 1.05rem;
  border-radius: 999px;
  border: 1px solid transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.btn:hover { text-decoration: none; }
/* Press feedback is non-positional (§11.1 stability rule): a background shift, never a
   transform that moves the button or reflows neighbours. */
.btn:active { background: var(--hover); }
/* translateZ(0) keeps the glyph on its own compositor layer. No longer load-bearing
   after §11.1 removed the :active translate (there's no move for the glyph to slip
   against), but kept as cheap insurance against sub-pixel jitter from layout neighbours. */
.btn svg { width: 18px; height: 18px; transform: translateZ(0); }

/* Three hover families (§12.1):
   1. Filled primary — lighten on hover (the near-black base hides a darken). */
.btn--primary { background: var(--ink); color: #fff; }
.btn--primary:hover { background: var(--ink-hover); color: #fff; }
.btn--primary:active { background: var(--ink-hover); }
/* 2. Ghost neutral (Cancel, Edit, the file-attach label) — soft pill on hover. */
.btn:not(.btn--primary):not(.btn--danger):hover { background: var(--hover); }
/* 3. Destructive (Delete) — quiet at rest, soft red pill + red text on hover. */
.btn--danger { color: var(--ink-3); }
.btn--danger:hover { color: var(--danger); background: var(--danger-soft); }
.btn--lg { padding: 0.8rem 1.5rem; font-size: 1rem; }
/* Empty-state Save (§12.4.C): aria-disabled (NOT the disabled attribute, so hover/focus
   still fire the tooltip). Looks disabled; ingest#onSaveClick intercepts the click. */
/* Mute via a solid token, NOT opacity: opacity dims the element's ::after too, and the
   empty-state tip is rendered there — an opacity-dimmed tip goes translucent and the
   buttons behind it bleed through. A muted fill keeps the tip fully opaque. */
.btn[aria-disabled="true"] { cursor: not-allowed; }
.btn--primary[aria-disabled="true"],
.btn--primary[aria-disabled="true"]:hover { background: var(--ink-3); }

/* ---------- Fields (shared single-line text / date input primitive) ----------
   The reusable counterpart to .prompt__input (which is the borderless compose
   textarea). Sized off the --control ramp; focus mirrors .prompt:focus-within. */
.field {
  height: var(--control);
  padding: 0 0.7rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink);
  font: inherit;
  font-size: 0.92rem;
  transition: border-color 0.15s ease;
}
.field::placeholder { color: var(--ink-3); }
.field:focus { outline: none; border-color: rgba(0, 0, 0, 0.18); }

/* Turbo frames are inline custom elements by default; make them block so the
   per-entry/Add frames lay out like the cards they wrap. */
turbo-frame { display: block; }

/* ---------- Generic page (about you, etc.) ---------- */
.page {
  flex: 1;
  width: 100%;
  max-width: 920px;
  margin: 0 auto;
  padding: 2.6rem 2rem;
}
.page__head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 1.8rem;
}
.page__title { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; margin: 0; }

/* ---------- Empty state ---------- */
.empty {
  display: flex; flex-direction: column; align-items: center;
  text-align: center;
  padding: 4rem 1.5rem;
}
.empty__art {
  width: 60px; height: 60px;
  display: grid; place-items: center;
  border-radius: 16px;
  background: var(--fill);
  color: var(--ink-2);
  margin-bottom: 1.3rem;
}
.empty__art svg { width: 28px; height: 28px; }
.empty__title { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.01em; margin: 0 0 0.5rem; }
.empty__sub { max-width: 30rem; color: var(--ink-2); line-height: 1.55; margin: 0 0 1.6rem; }

/* ---------- Compose (New intro hero — the signed-in home) ---------- */
.compose {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1.5rem 10vh;
}
.compose__inner { width: 100%; max-width: 680px; text-align: center; }
.compose__title {
  font-size: clamp(1.7rem, 3.4vw, 2.3rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0 0 0.7rem;
}
.compose__sub {
  font-size: 1.02rem;
  line-height: 1.55;
  color: var(--ink-2);
  max-width: 32rem;
  margin: 0 auto 2rem;
}

.compose__form { margin: 0; }

.prompt {
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  padding: 0.6rem 0.6rem 0.6rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.prompt:focus-within {
  border-color: rgba(0, 0, 0, 0.18);
  box-shadow: var(--shadow);
}
.prompt__input {
  flex: 1;
  border: none;
  outline: none;
  resize: none;
  background: none;
  font: inherit;
  font-size: 1.05rem;
  line-height: 1.5;
  color: var(--ink);
  max-height: 200px;
  padding: 0.45rem 0;
}
.prompt__input::placeholder { color: var(--ink-3); }
.prompt__send {
  flex: none;
  width: 38px; height: 38px;
  border: none;
  border-radius: 50%;
  background: var(--ink);
  color: #fff;
  cursor: pointer;
  display: grid; place-items: center;
  transition: background 0.15s ease;
}
.prompt__send svg { width: 20px; height: 20px; }
.prompt__send:disabled { background: #d2d2d7; cursor: default; }
.prompt__send:not(:disabled):hover { background: var(--accent); }

/* pre-selection chips — quick-fill the input */
.compose__chips {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 0.5rem;
  margin-top: 1.1rem;
}
.chip {
  padding: 0.5rem 0.95rem;
  border: 1px solid rgb(var(--accent-rgb) / 0.45);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-2);
  font: inherit; font-size: 0.9rem;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.chip:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }

.compose__hint {
  margin-top: 1.7rem;
  display: flex; align-items: center; justify-content: center; gap: 0.4rem;
  font-size: 0.85rem;
  color: var(--ink-3);
}
.compose__hint svg { width: 15px; height: 15px; flex: none; }

/* Rejected create: only the input signals error (red text + red frame) and locks; the chips and
   send button take the native disabled attribute. The alert dialog carries the full message, so
   nothing here moves — colour + attribute changes only, zero reflow. */
.compose--error .prompt { border-color: rgb(var(--danger-rgb) / 0.5); }
.compose--error .prompt__input,
.compose--error .prompt__input::placeholder {
  color: var(--danger);
  -webkit-text-fill-color: var(--danger);
}
/* Locked: every control reads as unavailable — pick a different model to recover. */
.compose--error .prompt,
.compose--error .prompt__input,
.compose--error .chip:disabled,
.compose--error .prompt__send:disabled { cursor: not-allowed; }
/* The short hint rides on .prompt (::after doesn't render on the textarea) and sits centred above
   the box — the default right-edge tip would overflow toward the send button. */
.compose--error .prompt[data-tip]::after {
  left: 50%; right: auto; top: auto; bottom: calc(100% + 8px);
  transform: translateX(-50%);
}
.chip:disabled {
  cursor: default;
  color: var(--ink-3);
  border-color: var(--line);
  background: var(--surface);
}

/* Rejected-create alert: a small, centred overlay — never shifts the page beneath it. */
.alert {
  border: none;
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--ink);
  width: min(360px, 92vw);
  padding: 1.7rem 1.5rem 1.4rem;
  text-align: center;
  box-shadow: var(--shadow-pop);
}
.alert::backdrop { background: rgba(0, 0, 0, 0.32); backdrop-filter: blur(2px); }
.alert__msg { margin: 0 0 1.3rem; font-size: 1.05rem; line-height: 1.5; }
.alert__ok {
  font: inherit; font-weight: 600; font-size: 0.95rem;
  padding: 0.6rem 1.5rem;
  border: none; border-radius: 999px;
  background: var(--ink); color: #fff;
  cursor: pointer;
  transition: background 0.15s ease;
}
.alert__ok:hover { background: var(--accent); }

/* ---------- Model picker (native <select>, styled) — pinned to the top of the page ---------- */
.compose { position: relative; }
/* Centred at the top of the page. Same top + control height as the floating account avatar
   (.usermenu), so the shorter pill's vertical centre lines up with the avatar's. */
.compose__tools {
  position: absolute;
  top: 1rem; left: 0; right: 0;
  height: var(--control);
  display: flex; align-items: center; justify-content: center;
  z-index: 5;
}
.picker { position: relative; display: inline-flex; align-items: center; }
.picker__select {
  appearance: none;
  font: inherit; font-size: 0.9rem; font-weight: 500;
  color: var(--accent);
  background: var(--surface);
  border: 1px solid rgb(var(--accent-rgb) / 0.45);
  border-radius: 999px;
  padding: 0.45rem 2.1rem 0.45rem 0.95rem;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.picker__select:hover { background: var(--accent-soft); border-color: var(--accent); }
/* appearance:none strips native focus — restore a visible focus-visible ring (house a11y) */
.picker__select:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.25);
}
.picker__chev {
  position: absolute; right: 0.75rem; top: 50%;
  transform: translateY(-50%);
  width: 15px; height: 15px;
  color: var(--accent);
  pointer-events: none;
}

/* ---------- Conversation (post-submit streaming view) ---------- */
.convo {
  flex: 1;
  position: relative;            /* anchors the top-pinned .compose__tools (picker) */
  display: flex; flex-direction: column;
  width: 100%; max-width: 720px;
  margin: 0 auto;
  /* Top clears the absolute picker (top:1rem + --control) and the fixed avatar so the first
     message never starts underneath them; the picker is out of flow and reserves no space. */
  padding: calc(1rem + var(--control) + 1.25rem) 1.5rem 2rem;
}
.convo__thread {
  flex: 1;
  display: flex; flex-direction: column;
  gap: 1rem;
  padding-bottom: 1.5rem;
}
.msg { display: flex; flex-direction: column; }
.msg--user { align-items: flex-end; }
.msg--assistant { align-items: flex-start; }
.msg .bubble {
  max-width: 85%;
  padding: 0.7rem 1rem;
  border-radius: var(--radius);
  line-height: 1.55;
  overflow-wrap: anywhere;
}
/* pre-wrap lives on the text span (not the bubble) so the model's own newlines are
   preserved while template whitespace around the text/dot spans collapses — otherwise the
   dot wraps below the text and ERB indentation leaks in as a leading indent. */
.msg__text { white-space: pre-wrap; }
.msg--user .bubble { background: var(--accent); color: #fff; border-bottom-right-radius: 6px; }
.msg--assistant .bubble {
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  border-bottom-left-radius: 6px;
}
/* Dot-as-cursor: inline sibling after the text span, so appends into the text push it to
   the end through line wraps (§6). Reuses the brand pulse — no new keyframe. */
.stream-dot {
  display: inline-block;
  width: 8px; height: 8px;
  margin-left: 3px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0.55);
  animation: brandPulse 2.4s infinite;
  vertical-align: middle;
}
.msg__hint { font-size: 0.82rem; color: var(--accent); margin: 0 0 0.35rem; }
.msg__error { font-size: 0.9rem; color: var(--danger); margin: 0.4rem 0 0; }

/* Locked composer after submit (V1 one-turn lock) — shown, not removed */
.convo__composer { position: sticky; bottom: 0; padding-top: 0.6rem; }
.prompt--locked { background: var(--fill); box-shadow: none; }
.prompt__input--locked {
  flex: 1;
  color: var(--ink-3);
  padding: 0.45rem 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---------- About you: life timeline + year scale ---------- */
.tl {
  flex: 1;
  width: 100%;
  max-width: 880px;
  margin: 0 auto;
  padding: 2.6rem 2rem;
}
.tl__head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.6rem;
}
.tl__lead { color: var(--ink-2); line-height: 1.55; margin: 0.4rem 0 0; max-width: 34rem; }
.tl__actions { flex: none; }

.tl__stream { display: block; }

/* minmax(0, 1fr): a bare 1fr track has an implicit min of `auto` = the max-content
   width of its widest child, so a long unbroken string (an SSH key, a no-space word)
   would stretch the track past .tl's max-width and overflow the page. minmax(0, …)
   lets the track shrink so the clamps below can take effect: the title still uses
   text-overflow:ellipsis, but the detail moved to -webkit-line-clamp (§12.2), whose
   break heuristics differ — it won't split an unbroken token on its own, so
   .entry__detail carries an explicit `overflow-wrap: anywhere` as the new guard. */
.era { display: grid; grid-template-columns: 88px minmax(0, 1fr); }
.era__marker { position: relative; }
.era__year {
  position: sticky; top: 1.2rem;
  font-size: 1.05rem; font-weight: 600; color: var(--ink-2);
  margin: 0; padding-top: 0.55rem;
  letter-spacing: -0.01em;
}
/* spine between the year markers and the entries */
.era__items {
  position: relative;
  min-width: 0; /* let the flex column shrink below its content's min-width (long unbroken strings) */
  /* padding-bottom matches the intra-era gap (1.6rem) so entry-to-entry distance is
     uniform across era boundaries — the year label carries the break, not extra air.
     §14.2 equalized at 0.7rem (read as cramped); §14.5 is the authoritative value. */
  padding: 0 0 1.6rem 1.5rem;
  border-left: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 1.6rem;
}

.entry {
  position: relative;
  display: flex; align-items: center; gap: 0.85rem;
  padding: 0.85rem 1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  /* The whole read card is click-to-edit (entry-open controller) — afford it. */
  cursor: pointer;
  transition: border-color 0.15s ease;
}
.entry:hover { border-color: var(--ink-3); }
/* node on the spine */
.entry::before {
  content: ""; position: absolute;
  left: calc(-1.5rem - 1px); top: 50%;
  width: 9px; height: 9px; margin-top: -4.5px;
  border-radius: 50%;
  background: var(--surface);
  border: 1.5px solid var(--ink-3);
}
/* Content tile, not an interactive control — sized to content, off the --control ramp. */
.entry__icon {
  flex: none;
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: var(--radius-sm);
  background: var(--fill);
  color: var(--ink-2);
}
.entry__icon svg { width: 20px; height: 20px; }
.entry__body { min-width: 0; flex: 1; }
/* One line, ellipsised (§11.4) — a long title can't wrap and grow the card height. */
.entry__title {
  font-size: 0.97rem; font-weight: 600; margin: 0; letter-spacing: -0.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Detail flexes 1–3 lines (§12.2, supersedes §11.2a uniform height). overflow-wrap:
   anywhere is load-bearing, not cosmetic: it replaces the old text-overflow:ellipsis as
   the column-overflow guard (see .era comment) — line-clamp won't break an unbroken token
   on its own, so without this a long no-space string re-stretches the grid track. */
.entry__detail {
  font-size: 0.88rem; color: var(--ink-2); margin: 0.15rem 0 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.entry__source {
  flex: none;
  font-size: 0.72rem; font-weight: 500; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: 0.04em;
}
/* Quiet read-card edit affordance — a neutral text link, not a pill. */
.entry__edit {
  flex: none;
  font-size: 0.82rem; font-weight: 500; color: var(--ink-3);
}
.entry__edit:hover { color: var(--ink); text-decoration: none; }

/* ---------- Editor card (shared Add/edit) ---------- */
/* Same .entry shell, laid out as a column. No spine node while editing (the card
   is tall; a mid-height disc reads as detached from the rail rhythm). */
.entry--editor {
  flex-direction: column;
  align-items: stretch;
  gap: 0.7rem;
  /* The editor isn't click-to-edit — cancel the read card's pointer + hover affordance
     so the open form doesn't read as a clickable tile. */
  cursor: default;
}
.entry--editor:hover { border-color: var(--line); }
/* Add form sits outside .era__items, so it gets no era gap below it. Scope the
   breathing room to #new_entry (the Add frame) so it never lands on an open Edit
   card, which already has .era__items' gap beneath it (§14.1). */
#new_entry .entry--editor { margin-bottom: 1.8rem; }
.entry--editor::before { content: none; }
.editor { display: flex; flex-direction: column; gap: 0.7rem; }
/* Reuse the .prompt box for the paste surface; floor its height so autosize never
   shrinks it below ~3 lines on connect (which would reflow the fields beneath). */
.editor__paste { padding: 0.55rem 0.8rem; }
/* Drag-over feedback (§11.7): an accent tint + border so a dragged file reads as
   droppable. Border/shadow/background only — no size or position change. */
.editor__paste--drag {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: var(--shadow);
}
/* Floor keeps autosize from shrinking below ~3 lines; the 18rem ceiling (§11.2) stops a
   long paste from taking over the viewport — past it the textarea scrolls internally. */
.editor__paste-input { font-size: 1rem; min-height: 4.5rem; max-height: 18rem; overflow-y: auto; }
/* §13.2 — attach row. Notes + file are independent (§13.1), so the row is always
   present. The label is a filled gray pill at rest (overrides .btn's ghost
   rest-state) to match the original reference sketch; click means "attach" with
   no file, "replace" with one. Filename (purple) + X (gray, proportional) show
   only when a file is attached — toggled client-side via [hidden]. */
.editor__attach {
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  margin-top: 0.6rem;
}
.editor__attach-label {
  padding: 0.45rem 0.85rem; font-size: 0.86rem;
  background: var(--fill);
}
/* Darken to --ink-3 — var(--hover) (7% black) on the surface is nearly identical to
   var(--fill), so the hover delta would be imperceptible. Jumping straight to the
   muted-gray token gives a clearly readable bump (same lesson as §12.1's
   --ink-hover: subtle ≠ unmeasurable). */
.editor__attach-label:hover { background: var(--ink-3); }

.editor__attach-filename {
  color: var(--accent);
  font-size: 0.92rem;
  font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; max-width: 28ch;
}

/* Detach chip: the whole X + filename group is ONE button. Hover anywhere on the
   chip highlights the X (per user request — "highlight the delete icon"); click
   anywhere triggers ingest#onRemoveFile. Single tab stop, announced as "Remove
   file" via aria-label. The full filename is on the wrapper's title attr for the
   truncation-disclosure tooltip. */
.editor__attach-detach {
  flex: none;
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.25rem 0.6rem 0.25rem 0.45rem;
  border: none; background: none;
  border-radius: 999px;
  cursor: pointer;
  color: inherit;
  transition: background 0.15s ease;
  margin-left: auto;
}
.editor__attach-detach[hidden] { display: none; }

.editor__attach-remove {
  flex: none;
  display: inline-grid; place-items: center;
  width: 1.3em; height: 1.3em;
  border-radius: 50%;
  color: var(--ink-3);
  transition: color 0.15s ease, background 0.15s ease;
}
.editor__attach-remove svg { width: 0.85em; height: 0.85em; }
/* Highlight X on parent (chip) hover — the visual cue that the whole chip is the
   delete target, not just the X. */
.editor__attach-detach:hover .editor__attach-remove {
  color: var(--ink);
  background: rgb(0 0 0 / 0.08);
}
.editor__fields { display: flex; gap: 0.6rem; }
.field--title { flex: 1; min-width: 0; }
.field--date { flex: none; }
.editor__error { margin: 0; font-size: 0.85rem; color: var(--danger); }
.editor__actions { display: flex; align-items: center; gap: 0.6rem; }
.editor__actions .btn { padding: 0.5rem 0.95rem; font-size: 0.88rem; }
/* Footer row holds primary actions (Save + Cancel) on the left and Delete on the right.
   Lives OUTSIDE the entry form so Delete's own button_to form can share the row — see
   _form.html.erb for the structural rationale. flex-wrap is the narrow-viewport safety net:
   when the items don't fit, Delete wraps below, still right-aligned via margin-left. */
.editor__footer { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
/* Delete is wrapped in its own button_to <form>, so that form (not the button) is the
   footer's flex child — push it to the far right from there. */
.editor__footer .button_to { margin-left: auto; }
/* Delete parked at the far right of the footer; layout only — colour + hover come from
   .btn--danger, the single source of truth for the destructive hover state. */
.editor__delete.btn { padding: 0.5rem 0.95rem; font-size: 0.88rem; }

/* ---------- First-run sample (dimmed, inert preview) ---------- */
.tl__sample {
  opacity: 0.5;
  pointer-events: none;
  filter: saturate(0.85);
}

/* ---------- Sparse tail (faded rail + "your story starts here" cap) ---------- */
.tl__tail-items {
  /* Fade the rail out toward the bottom so the timeline tapers rather than stops. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
  padding-bottom: 2.6rem;
  min-height: 3rem;
}
.tl__tail-cap {
  font-size: 0.85rem; color: var(--ink-3);
  margin: 0; padding-top: 0.4rem;
}

/* Narrow main column: .tl shrinks toward its max-width edge while the fixed .usermenu
   (right: 1.25rem desktop, right: 1rem at ≤768px) holds top-right position, so + Add
   slides under the avatar. Pad .tl__head's right to keep Add clear. The avatar's footprint
   is ~60px from the viewport edge (40px tap target + 20px offset); Add's clearance is
   (app-main − 880)/2 + 32px, which falls below that once app-main < ~960px. Queried on the
   app-main column (not the viewport) so it's correct for every sidebar width — expanded
   (248px), collapsed (64px), and the ≤768px drawer band where it must keep applying. */
@container app-main (max-width: 960px) {
  .tl__head { padding-right: 3rem; }
}

/* ---------- Responsive: phone layout (drawer nav + bottom-anchored compose) ---------- */
@media (max-width: 768px) {
  /* Single column: the rail becomes an off-canvas drawer, so any persisted
     desktop-collapsed width is ignored. No top bar — main fills the viewport
     (100dvh accounts for the mobile browser chrome) and the menu/account buttons
     float over the content. */
  .app,
  .app--sidebar-collapsed {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    min-height: 100dvh;
  }

  /* Floating menu button (top-left, over the content). Sits below the scrim so it
     dims out while the drawer is open; the drawer's own close button takes over. */
  .app__menu {
    display: grid; place-items: center;
    position: fixed;
    top: calc(env(safe-area-inset-top) + 8px);
    left: 8px;
    z-index: 55;
    width: var(--control); height: var(--control);
    border: none; background: none; cursor: pointer;
    color: var(--ink);
    border-radius: var(--radius-sm);
    transition: background 0.15s ease;
  }
  .app__menu svg { width: 22px; height: 22px; }
  .app__menu:hover,
  .app__menu:active { background: var(--hover); }

  /* The sidebar, slid off-canvas; the menu button toggles app--drawer-open.
     Transform-only (not layout) so the slide can't reflow content behind it. */
  .sidebar {
    position: fixed;
    top: 0; left: 0; bottom: 0;
    width: min(82vw, 320px);
    height: 100dvh;
    z-index: 70;
    transform: translateX(-100%);
    transition: transform 0.24s ease;
    padding: calc(1rem + env(safe-area-inset-top)) 0.9rem 1rem;
    border-right: 1px solid var(--line);
    border-radius: 0 var(--radius) var(--radius) 0;
    /* Separation cue in place of a page dim: a soft shadow off the drawer's right edge.
       Kept light/narrow so it doesn't darken the screen-edge strips iOS Safari samples. */
    box-shadow: 6px 0 24px rgba(0, 0, 0, 0.12);
  }
  .app--drawer-open .sidebar { transform: translateX(0); }

  /* The drawer always shows the expanded contents (the desktop fold controls are
     meaningless here), overriding any persisted desktop-collapsed state. The brand
     wordmark sits top-left, the close button top-right. */
  .sidebar__mark,
  .sidebar-toggle { display: none; }
  .sidebar__brand {
    height: auto;
    padding: 0 0 1rem;
    justify-content: space-between;
  }
  .sidebar__logo,
  .app--sidebar-collapsed .sidebar__logo {
    position: static;
    opacity: 1;
    pointer-events: auto;
  }
  .app--sidebar-collapsed .nav-item { width: auto; }
  .app--sidebar-collapsed .nav-item span,
  .app--sidebar-collapsed .sidebar__section { display: block; opacity: 1; }

  /* Explicit close affordance for the drawer, with the same grey rounded highlight
     the desktop fold control uses. */
  .sidebar__close {
    display: grid; place-items: center;
    margin-left: auto;
    width: var(--control); height: var(--control);
    border: none; background: none; cursor: pointer;
    color: var(--ink-2);
    border-radius: var(--radius-sm);
    transition: background 0.15s ease, color 0.15s ease;
  }
  .sidebar__close svg { width: 20px; height: 20px; }
  .sidebar__close:hover,
  .sidebar__close:active { background: var(--fill); color: var(--ink); }

  /* Invisible tap-catcher behind the open drawer: lets a tap outside close it, but
     paints nothing. We deliberately do NOT dim the page — a dark overlay makes iOS
     Safari sample dark pixels at the screen edges and tint its status bar/toolbar grey,
     which then latches. No dark pixels = the chrome stays the resting light theme-color.
     The drawer's own shadow provides the separation a dim would otherwise give. */
  .app__scrim {
    display: block;
    position: fixed; inset: 0;
    z-index: 60;
    background: none;
    pointer-events: none;
  }
  .app--drawer-open .app__scrim { pointer-events: auto; }

  /* Account avatar clears the status bar / notch. Its --control size already
     matches the hamburger, so no size override is needed here. */
  .usermenu { top: calc(env(safe-area-inset-top) + 8px); right: 1rem; }

  /* Compose: headline centred in the upper space, chips + input pinned to the
     bottom. The input rides above the keyboard via --kb (set per-frame by the
     viewport controller). Top padding clears the floating menu/account buttons. */
  .compose {
    align-items: stretch;
    padding: calc(env(safe-area-inset-top) + 3.2rem) 1.4rem 0;
    padding-bottom: calc(0.8rem + env(safe-area-inset-bottom) + var(--kb, 0px));
  }
  .compose__inner {
    display: flex;
    flex-direction: column;
    max-width: none;
    text-align: center;
  }
  /* Picker stays pinned at the top centre, clearing the notch. */
  .compose__tools { top: calc(env(safe-area-inset-top) + 8px); }
  .compose__title { order: 1; margin: auto 0 0.7rem; }
  .compose__sub   { order: 2; margin: 0 auto; }
  .compose__chips { order: 3; justify-content: center; margin-top: auto; margin-bottom: 0.8rem; }
  .compose__form  { order: 4; }
  .compose__hint  { display: none; }

  /* Generic page + timeline density; extra top padding clears the floating menu. */
  .page { padding: calc(env(safe-area-inset-top) + 3.4rem) 1.2rem 1.8rem; }
  .tl { padding: calc(env(safe-area-inset-top) + 3.4rem) 1.2rem 1.8rem; }
  .era { grid-template-columns: 1fr; }
  .era__marker { padding-bottom: 0.4rem; }
  .era__year { position: static; padding-top: 0; }
  .era__items { padding-left: 1.2rem; min-width: 0; }

  /* Editor goes full-width with ≥44px touch targets; date stacks above title so
     each field gets the full width, and the attach controls keep a comfortable tap. */
  .editor__fields { flex-direction: column; }
  .field { height: 44px; }
  .editor__actions .btn,
  .editor__attach-label,
  .editor__delete.btn { padding: 0.7rem 1rem; }
}
