/* ────────────────────────────────────────────────────────────────────
   chat.posta.no — base design tokens
   Lifted from posta/web/static/style.css. Marketing hero and paper
   grain are intentionally omitted; chat-specific layouts will be
   added on top of these tokens in later slices.

   palette: paper cream, sky blue, forest, terracotta
   type:    Crimson Pro (display) · IBM Plex Sans (body) · IBM Plex Mono
   ──────────────────────────────────────────────────────────────────── */

:root {
  /* paper + ink */
  --paper:        #f7f1e3;
  --paper-warm:   #fbf6e8;
  --paper-deep:   #ece3cc;
  --ink:          #2d2a24;
  --ink-soft:     #524c3f;
  --ink-faint:    #8a826f;

  /* sky */
  --sky:          #c8dde9;
  --sky-deep:     #88b1cd;
  --sky-haze:     #e6eef3;

  /* forest */
  --forest:       #6e8b5b;
  --forest-deep:  #4d6a3e;
  --moss:         #b8c79a;

  /* sun + lantern */
  --terracotta:   #c87b5d;
  --sunset:       #e3a47a;
  --gold:         #e6c073;

  /* surface */
  --line:         rgba(45, 42, 36, 0.10);
  --line-soft:    rgba(45, 42, 36, 0.06);
  --shadow:       0 1px 2px rgba(45, 42, 36, 0.04),
                  0 8px 24px rgba(45, 42, 36, 0.06);
  --shadow-deep:  0 4px 12px rgba(45, 42, 36, 0.08),
                  0 24px 48px rgba(45, 42, 36, 0.10);

  --radius:       14px;
  --radius-lg:    22px;

  --measure: 64rem;

  color-scheme: light dark;
}

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: 'IBM Plex Sans', system-ui, sans-serif;
  font-size: 17px;
  line-height: 1.6;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: var(--forest-deep); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
a:hover { color: var(--terracotta); }

code, pre {
  font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, monospace;
  font-size: 0.92em;
}
p code, li code {
  background: var(--paper-deep);
  padding: 0.1em 0.4em;
  border-radius: 6px;
  font-size: 0.86em;
}

em { font-style: italic; color: var(--forest-deep); }

h1, h2, h3 {
  font-family: 'Crimson Pro', Georgia, serif;
  font-weight: 500;
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 0 0 16px;
}
h1 {
  font-weight: 400;
  font-size: clamp(2.4rem, 5vw, 3.6rem);
  line-height: 1.06;
  letter-spacing: -0.02em;
}
h1 em { font-style: italic; color: var(--forest-deep); }
h2 { font-size: 1.7rem; }
h3 { font-size: 1.25rem; }

p { margin: 0 0 18px; text-wrap: pretty; }

.kicker {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.8rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--terracotta);
  margin: 0 0 28px;
}

/* ─── welcome page ────────────────────────────────────────────────── */
.welcome {
  max-width: 38rem;
  margin: 0 auto;
  padding: 96px 32px 64px;
}
.welcome .lede,
.main-empty .lede {
  font-size: 1.18rem;
  line-height: 1.65;
  color: var(--ink-soft);
  margin: 0 0 32px;
}
.welcome .status,
.main-empty .status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  background: var(--paper-warm);
}
.welcome .status::before,
.main-empty .status::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--forest);
  box-shadow: 0 0 0 3px rgba(110, 139, 91, 0.18);
}

/* ─── inbox shell ─────────────────────────────────────────────────── */
/* The app-shell is the two-column layout used by every page after
   login: a fixed-width sidebar on the left, the main pane filling
   the rest. At desktop widths (≥768px) both panes are visible; below
   the 768px breakpoint we collapse to a single column and the CSS at
   the bottom of this file (the @media (max-width: 767.98px) block)
   hides whichever pane isn't relevant for body[data-view]. */
body.app {
  /* The shell itself is full-viewport-height; scrollable regions
     live inside it. Removes the default body margin that would
     leak the paper colour around the edges.
     `100dvh` (dynamic viewport height) adapts to the iOS Safari
     show/hide of the address bar and to the on-screen keyboard
     appearing — `100vh` would let the keyboard cover the composer.
     Fallback to 100vh for browsers that don't grok `dvh`. */
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
}
.app-shell {
  display: grid;
  grid-template-columns: 320px 1fr;
  min-height: 100vh;
  min-height: 100dvh;
}

.sidebar {
  border-right: 1px solid var(--line);
  background: var(--paper-warm);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* Height matches the shell row so the contacts list scrolls
     independently of the main pane. */
  max-height: 100vh;
  max-height: 100dvh;
}
.sidebar-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 24px 24px 12px;
  border-bottom: 1px solid var(--line-soft);
}
.sidebar-header .kicker { margin: 0; }
.sidebar-logout { margin: 0; }
.link-button {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--ink-soft);
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.link-button:hover { color: var(--terracotta); }

.sidebar-empty,
.sidebar-error {
  padding: 24px;
  color: var(--ink-soft);
  font-size: 0.95rem;
}
.sidebar-error {
  color: var(--terracotta);
}
/* Sidebar empty-state polish (Slice 12). Two lines: a "No conversations
   yet" headline and a smaller hint pointing at the "+" toggle in the
   sidebar header. Tokens-only — flips through to dark mode via the
   --ink-soft / --ink-faint shifts in the @media block. */
.sidebar-empty {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.sidebar-empty p {
  margin: 0;
}
.sidebar-empty-title {
  color: var(--ink);
  font-size: 1rem;
}
.sidebar-empty-hint {
  color: var(--ink-faint);
  font-size: 0.88rem;
}
.sidebar-empty-hint strong {
  /* The "+" glyph reference picks up the terracotta accent so it
     visually echoes the button it's pointing at. */
  color: var(--terracotta);
  font-weight: 700;
}

/* ─── sidebar header + new-conversation inline form ────────────── */
/* The sidebar-header-actions row sits in the sidebar header to the
   right of the kicker, holding both the "+" toggle and the sign-out
   form on the same baseline. */
.sidebar-header-actions {
  display: flex;
  align-items: center;
  gap: 14px;
}
.new-conversation-toggle {
  font-size: 1.4rem;
  line-height: 1;
  text-decoration: none;
  color: var(--ink);
  padding: 0 4px;
  /* Finger-sized tap target on mobile (PRD §62). The "+" glyph
     alone is only ~22px wide; the min-width/height pushes the hit
     region out to the iOS HIG floor. */
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.new-conversation-toggle:hover {
  color: var(--terracotta);
}
/* The expanded inline form. Hidden by default (collapsed); the "+"
   toggle adds `.open` to the wrapper to reveal it. The form pushes
   the contacts list down — no overlay / popover ceremony, matches
   the rest of the inbox shell that resizes organically. */
.new-conversation {
  border-bottom: 1px solid var(--line-soft);
}
.new-conversation > .new-conversation-form,
.new-conversation > .new-conversation-preview {
  display: none;
}
.new-conversation.open > .new-conversation-form,
.new-conversation.open > .new-conversation-preview {
  display: block;
}
.new-conversation-form {
  padding: 12px 24px 8px;
}
.new-conversation-label {
  display: block;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 4px;
}
.new-conversation-input {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--paper);
  font: inherit;
  font-size: 0.95rem;
  color: var(--ink);
}
.new-conversation-input:focus {
  outline: 2px solid var(--sky-deep);
  outline-offset: 1px;
}
.new-conversation-preview {
  padding: 4px 24px 14px;
  min-height: 0;
}
.new-conversation-error {
  color: var(--terracotta);
  font-size: 0.9rem;
  margin: 0;
}
.new-conversation-result {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.new-conversation-identity {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
}
.new-conversation-summary {
  font-size: 0.92rem;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.new-conversation-summary strong {
  color: var(--ink);
}
.new-conversation-host {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.82rem;
}
.new-conversation-start {
  appearance: none;
  background: var(--forest);
  color: var(--paper);
  border: 0;
  border-radius: 6px;
  padding: 6px 14px;
  font: inherit;
  font-size: 0.9rem;
  text-decoration: none;
  cursor: pointer;
}
.new-conversation-start:hover {
  background: var(--terracotta);
}
/* The /new full-page main pane uses a similar form, scaled up for
   focused entry. Visual treatment matches the welcome paint so the
   page slot in cleanly. */
.new-conversation-main-form {
  margin: 0 0 16px;
  max-width: 24rem;
}
.new-conversation-main-form input[type="url"] {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  font: inherit;
  font-size: 1rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--paper);
  color: var(--ink);
}
.new-conversation-main-form input[type="url"]:focus {
  outline: 2px solid var(--sky-deep);
  outline-offset: 1px;
}

.contacts {
  list-style: none;
  margin: 0;
  padding: 8px 0;
  overflow-y: auto;
  flex: 1;
}
.contact {
  margin: 0;
}
.contact-link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 24px;
  color: var(--ink);
  text-decoration: none;
  transition: background 0.12s ease;
  /* 44px tap target — the avatar's 36px + 10px vertical padding
     already lands here on desktop; the explicit floor guarantees
     finger-sized rows on mobile (PRD §62). */
  min-height: 44px;
}
.contact-link:hover {
  background: var(--paper-deep);
  color: var(--ink);
}
.contact-link:focus-visible {
  outline: 2px solid var(--sky-deep);
  outline-offset: -2px;
}
.contact-link.is-active {
  background: var(--sky-haze);
}
.contact-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.98rem;
}

/* Avatar: 36px round element used in both <img> form (peer-set
   avatar URL) and <span> form (initials fallback). Sharing the
   `.avatar` class keeps the layout boxes identical regardless of
   which rendering branch the template took. */
.avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  object-fit: cover;
  font-family: 'IBM Plex Sans', system-ui, sans-serif;
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: #fff;
  text-transform: uppercase;
  /* Subtle ring to anchor the avatar against pale backgrounds in
     dark mode where the paper colours can be very close to the
     accent values. */
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
}

.unread-badge {
  flex-shrink: 0;
  min-width: 22px;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--terracotta);
  color: #fff;
  font-family: 'IBM Plex Sans', system-ui, sans-serif;
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-align: center;
  line-height: 1.4;
}

.main {
  background: var(--paper);
  min-height: 100vh;
  min-height: 100dvh;
  overflow-y: auto;
}
.main-empty {
  max-width: 38rem;
  margin: 0 auto;
  padding: 96px 32px 64px;
  text-align: center;
}
/* First-login CTA on the empty main pane (Slice 12). The button opens
   the sidebar's inline new-conversation form via view.js's
   data-action="open-new-conversation" handler. The visual treatment
   mirrors `.new-conversation-start` (forest fill, paper text) but
   sized up for the centred main-pane composition. */
.main-empty-cta {
  appearance: none;
  border: 0;
  background: var(--forest);
  color: var(--paper);
  font: inherit;
  font-size: 1rem;
  padding: 12px 22px;
  border-radius: var(--radius);
  cursor: pointer;
  min-height: 44px;
}
.main-empty-cta:hover,
.main-empty-cta:focus-visible {
  background: var(--forest-deep);
}
.main-empty-cta:focus-visible {
  outline: 2px solid var(--sky-deep);
  outline-offset: 2px;
}
.main-empty-hint {
  margin-top: 8px;
  color: var(--ink-faint);
  font-size: 0.88rem;
}

/* ─── thread view ─────────────────────────────────────────────────── */
/* The thread fills the main pane vertically. Bubbles flow oldest →
   newest from top to bottom, which means the composer (landing in
   Slice 7) sits at the bottom near the newest message — the same
   reading model every chat UI uses.

   The 2-min clustering rule is enforced server-side; the CSS just
   tightens the spacing between bubbles inside a cluster and gives
   each cluster a single avatar+timestamp meta row at the top. */
.thread {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  padding: 0;
}
.thread-header {
  position: sticky;
  top: 0;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  padding: 18px 28px 14px;
  z-index: 1;
  /* Flex row so the back chevron (mobile only) can sit left of the
     titles on small screens. On desktop the chevron is display:none
     and the titles fall back to a normal block layout via the
     flex-direction interaction (the chevron's absence collapses the
     flex container to a single child). */
  display: flex;
  align-items: center;
  gap: 12px;
}
.thread-header-titles {
  /* Wrapper so the title + URL stack vertically inside the flex
     header next to the back chevron. flex-grow:1 lets the titles
     consume the remaining row width. */
  flex: 1;
  min-width: 0;
}
.thread-title {
  margin: 0;
  font-size: 1.4rem;
  font-family: 'Crimson Pro', Georgia, serif;
  font-weight: 500;
}
.thread-peer-url {
  margin: 2px 0 0;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.78rem;
  color: var(--ink-faint);
  word-break: break-all;
}

/* Back chevron — server-rendered on every thread page, hidden by
   default (desktop). The mobile media query at the bottom of this
   file flips it to inline-flex below 768px. The 44×44 tap target
   matches the PRD's mobile finger-sized affordance requirement. */
.back-chevron {
  display: none;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  margin-left: -10px;
  padding: 0;
  color: var(--ink);
  text-decoration: none;
  border-radius: 999px;
  flex-shrink: 0;
}
.back-chevron:hover,
.back-chevron:focus-visible {
  background: var(--paper-deep);
  color: var(--ink);
}
.back-chevron:focus-visible {
  outline: 2px solid var(--sky-deep);
  outline-offset: 2px;
}
.thread-error {
  margin: 24px 28px;
  padding: 14px 18px;
  border-radius: var(--radius);
  background: var(--paper-deep);
  color: var(--terracotta);
}
/* "Say hi" empty state in a fresh thread (Slice 12). The original
   slice 6 hint was a single line; the polish pass splits into a
   stronger title + a softer prompt so a brand-new thread has a
   visible call to action above the composer. */
.thread-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  color: var(--ink-soft);
  font-size: 1.05rem;
  text-align: center;
  padding: 0 20px;
}
.thread-empty p {
  margin: 0;
}
.thread-empty-title {
  color: var(--ink);
  font-size: 1.15rem;
}
.thread-empty-title strong {
  color: var(--forest-deep);
}
.thread-empty-hint {
  color: var(--ink-faint);
  font-size: 0.95rem;
}
.thread-messages {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 20px 28px 32px;
}
.thread-load-anchor {
  display: flex;
  justify-content: center;
  padding: 12px 28px 0;
}
.load-more {
  appearance: none;
  border: 1px solid var(--line);
  background: var(--paper-warm);
  color: var(--ink-soft);
  font: inherit;
  font-size: 0.85rem;
  padding: 8px 18px;
  border-radius: 999px;
  cursor: pointer;
  /* Mobile-friendly tap target without forcing the desktop layout
     to look chunky. */
  min-height: 36px;
}
.load-more:hover {
  background: var(--paper-deep);
  color: var(--ink);
}
.thread-history-exhausted {
  margin: 0;
  color: var(--ink-faint);
  font-size: 0.85rem;
  font-style: italic;
}

/* Date dividers — centered horizontal rule with a small label.
   Inserted client-side by thread.js between clusters whose *local*
   day differs (server never emits these, since UTC day boundaries
   don't match the viewer's local day for non-UTC timezones). */
.date-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 18px 0 4px;
  color: var(--ink-faint);
  font-size: 0.78rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.date-divider::before,
.date-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* Clusters: a vertical stack with optional avatar+time meta row at
   the top. Outbound clusters mirror the meta row to the right side
   and skip the avatar entirely. */
.cluster {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-width: 100%;
}
.cluster-in {
  align-items: flex-start;
}
.cluster-out {
  align-items: flex-end;
}
.cluster-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.78rem;
  color: var(--ink-faint);
  margin-bottom: 2px;
}
.cluster-out .cluster-meta {
  flex-direction: row-reverse;
}
.cluster-avatar {
  width: 22px;
  height: 22px;
  font-size: 0.62rem;
}
.cluster-time {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
}
.cluster-bubbles {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-width: 60%;
}

/* Bubbles: rounded rectangle with directional fill. Inside a
   cluster, consecutive bubbles share the same colour and sit at
   2px gap; the bubble + bubble selector tightens further if needed. */
.bubble {
  padding: 8px 14px;
  border-radius: var(--radius);
  word-wrap: break-word;
  line-height: 1.5;
  /* Generous min height so touch targets are usable on mobile.
     The 44px target lands in Slice 11; here we lean on padding +
     line-height to land in the same ballpark. */
  min-height: 36px;
}
.cluster-in .bubble {
  background: var(--paper-deep);
  color: var(--ink);
  border-bottom-left-radius: 6px;
}
.cluster-out .bubble {
  background: var(--sky-deep);
  color: var(--paper);
  border-bottom-right-radius: 6px;
}
.bubble + .bubble {
  /* tighter spacing within a cluster (override gap with margin
     would still respect the parent's gap; this is a no-op but kept
     so a designer can find the hook). */
}
.bubble .body {
  /* Newlines in plain-text bodies preserved per PRD §35 / acceptance
     criterion "Newlines in text bodies preserved". URL linkification
     (Slice 12) emits inline <a> anchors via `linkifyBody`; their
     visual treatment is below. */
  white-space: pre-wrap;
  margin: 0;
}
/* Linkified URLs inside text bubbles. Inbound bubbles sit on the
   paper-deep fill and use the global link colour (forest-deep). Outbound
   bubbles sit on the sky-deep fill — the global link colour fades out
   against it, so we re-paint the anchor to use the paper tone for the
   bubble's foreground colour and underline for affordance. */
.bubble .body a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  /* Make the underline visible-but-quiet across both bubble fills.
     The currentColor inheritance plus underline keeps the link
     legible without painting it a different colour. */
  text-decoration-thickness: 1px;
  /* URLs can be long; break them so a wide pasted URL doesn't blow
     past the bubble width. */
  word-break: break-word;
  overflow-wrap: anywhere;
}
.bubble .body a:hover {
  text-decoration-thickness: 2px;
}
.cluster-out .bubble .body a:hover {
  /* Outbound bubble: hover paints the link warmer so the cursor's
     position is unambiguous against the sky-deep fill. */
  color: var(--gold);
}

/* Outbound bubble colours: light + dark share the same
   --sky-deep / --paper pair (the tokens themselves shift between
   schemes), so no `prefers-color-scheme: dark` override needed.
   Slice 12 polish can revisit if dark-mode contrast wants tuning. */

/* ─── status indicators ──────────────────────────────────────────── */
/* Per PRD §32 outbound bubbles surface their send state next to the
   LAST bubble in each 2-minute cluster. The icon inherits its colour
   from the bubble text (so the clock + check sit muted against the
   sky-deep fill) and shifts to terracotta / gold for the two failure
   states. Inline SVGs scale with text size. */
.bubble .status {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 6px;
  margin-top: 2px;
  vertical-align: baseline;
  opacity: 0.85;
  /* Inherit the text colour by default — the failure-state classes
     override below. */
  color: currentColor;
}
.cluster-out .bubble .body {
  /* Inline the status next to the last word rather than pushing it
     onto its own line. Setting the parent to inline-flex would break
     `white-space: pre-wrap`, so we keep the body block-level and
     position the status as a trailing inline-block. */
  display: inline;
}
.cluster-out .bubble {
  /* The status icon trails the body inside the bubble; the bubble
     itself stays a block so the cluster gap is preserved. */
  display: flex;
  align-items: flex-end;
  gap: 4px;
  flex-wrap: wrap;
}
.bubble .status-pending,
.bubble .status-sending {
  /* Muted: a send in flight is mostly "uneventful, just wait". */
  color: var(--paper-deep);
}
.bubble .status-delivered {
  color: var(--paper-warm);
}
.bubble .status-failed-permanent {
  /* Hard failure → red triangle. We re-use --terracotta for the warm
     red so the failure state lives in the palette. */
  color: var(--terracotta);
}
.bubble .status-failed-pending-user {
  /* Soft failure the user can act on → orange triangle (gold + warm
     mix). The palette doesn't carry a dedicated orange so we pick
     --sunset which sits between gold and terracotta. */
  color: var(--sunset);
}

/* ─── composer ───────────────────────────────────────────────────── */
/* The composer is pinned to the bottom of the thread main pane. It
   participates in the thread's flexbox column (.thread is
   flex-column with min-height: 100vh), so we use position: sticky
   instead of position: fixed — the textarea grows the form's height
   organically and the sidebar's chrome stays where it is on every
   render. */
.composer {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: flex-end;
  gap: 10px;
  padding: 14px 28px 18px;
  /* Honour the iOS safe-area at the bottom (home-bar gesture area)
     so the Send button isn't tucked under the hardware indicator
     on notched devices. env() returns 0 on platforms without an
     inset; the `max()` keeps the existing 18px floor on desktop. */
  padding-bottom: max(18px, env(safe-area-inset-bottom));
  background: var(--paper-warm);
  border-top: 1px solid var(--line);
  /* z-index keeps the composer over the bubble cluster behind it
     when the scroll-area is short enough that the composer would
     otherwise be drawn behind a sticky bubble row. */
  z-index: 2;
}
.composer-input {
  flex: 1;
  resize: none;
  font: inherit;
  font-size: 1rem;
  line-height: 1.5;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  /* Auto-grow caps the height at ~6 lines client-side via composer.js;
     the textarea starts at one line tall via rows=1 + line-height. */
  min-height: 40px;
  max-height: 240px;
  overflow-y: hidden;
}
.composer-input:focus-visible {
  outline: 2px solid var(--sky-deep);
  outline-offset: -1px;
  border-color: var(--sky-deep);
}
.composer-send {
  appearance: none;
  border: 0;
  background: var(--forest);
  color: var(--paper);
  font: inherit;
  font-size: 0.95rem;
  padding: 10px 18px;
  border-radius: var(--radius);
  cursor: pointer;
  /* 44px min keeps the Send button finger-friendly on mobile per
     PRD §62 — the desktop layout already lands here via padding +
     line-height, so this floor is a guarantee not a redesign. */
  min-height: 44px;
  min-width: 44px;
}
.composer-send:hover {
  background: var(--forest-deep);
}
.composer-send:focus-visible {
  outline: 2px solid var(--sky-deep);
  outline-offset: 2px;
}

/* Error slot under the composer. Empty by default — composer.js
   clears it on each submit, and the server's error fragment swaps
   into it via hx-target-4xx/hx-target-5xx. */
.composer-error {
  padding: 0 28px;
  background: var(--paper-warm);
  min-height: 0;
}
.composer-error:empty {
  display: none;
}
.composer-error-message {
  margin: 0 0 12px;
  padding: 10px 14px;
  border-radius: var(--radius);
  background: var(--paper-deep);
  color: var(--terracotta);
  font-size: 0.92rem;
}

/* Sentinel marker — not visible. Sits at the bottom of the thread
   message list so new outbound bubbles `hx-swap="beforebegin"` into
   the slot just above it. */
.thread-bubbles-end {
  height: 0;
  width: 0;
}

/* ─── image bubbles (Slice 10) ───────────────────────────────────── */
/* posta.link/v1 image payloads render as an inline <img> wrapped in
   an <a> for click-to-open. Bubble-width sizing per PRD 46; 60vh
   height cap keeps a portrait shot from blowing the layout. The
   <a> reset (no underline/colour) keeps the affordance to the cursor
   change rather than a styled link border. */
.bubble-image-link {
  display: block;
  text-decoration: none;
  color: inherit;
}
.bubble img {
  display: block;
  max-width: 100%;
  max-height: 60vh;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: calc(var(--radius) - 4px);
}
.bubble.bubble-image {
  /* The image bubble drops the body's padding so the <img> fills
     edge-to-edge. The radius still rounds the corners; the inner
     image gets its own slightly smaller radius so it doesn't fight
     the bubble corner. */
  padding: 4px;
}
/* Broken-image fallback. The <img>'s onerror handler flips .broken
   on the parent .bubble; the CSS swaps the <img> out for a small
   "Image from <host>" caption. The caption is hidden by default and
   only revealed in the broken state. */
.bubble-image-broken-fallback {
  display: none;
  padding: 18px 16px;
  background: var(--paper-deep);
  color: var(--terracotta);
  border-radius: calc(var(--radius) - 4px);
  font-size: 0.9rem;
  text-align: center;
}
.bubble.broken .bubble-image-link {
  display: none;
}
.bubble.broken .bubble-image-broken-fallback {
  display: block;
}

/* ─── composer attach + image-mode (Slice 10) ────────────────────── */
.composer-attach {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  padding: 8px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 44px tap target — see .composer-send for the rationale. */
  min-width: 44px;
  min-height: 44px;
  /* Keep the affordance subtle until interacted with — the paperclip
     just hints "you can attach". */
  opacity: 0.65;
  transition: opacity 0.12s ease, background-color 0.12s ease;
}
.composer-attach:hover,
.composer-attach:focus-visible {
  opacity: 1;
  background: var(--paper-deep);
}
.composer-attach:focus-visible {
  outline: 2px solid var(--sky-deep);
  outline-offset: 2px;
}

/* Image preview row. Hidden by default; .composer.image-mode reveals
   it and concurrently hides the textarea. The layout is a small
   thumbnail + the file name/size + a Discard button. */
.composer-image-preview {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.composer-image-thumb {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: calc(var(--radius) - 4px);
  background: var(--paper-deep);
  flex-shrink: 0;
}
.composer-image-meta {
  display: flex;
  flex-direction: column;
  font-size: 0.88rem;
  color: var(--ink);
  min-width: 0;
  flex: 1;
}
.composer-image-name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.composer-image-size {
  color: var(--ink-muted, var(--ink));
  opacity: 0.7;
  font-size: 0.8rem;
}
.composer-image-discard {
  appearance: none;
  border: 1px solid var(--line);
  background: var(--paper-warm);
  color: var(--terracotta);
  padding: 6px 12px;
  border-radius: var(--radius);
  cursor: pointer;
  font-size: 0.88rem;
}
.composer-image-discard:hover {
  background: var(--paper-deep);
}

/* Image-mode swap: hide the textarea, show the preview. The send
   button's label changes JS-side ("Send image"). */
.composer.image-mode .composer-input {
  display: none;
}
.composer:not(.image-mode) .composer-image-preview {
  display: none;
}

/* Drag-over affordance: lift the composer's background so the user
   can tell where the drop will land. */
.composer.drag-over {
  background: var(--paper-deep);
  outline: 2px dashed var(--sky-deep);
  outline-offset: -8px;
}


/* ────────────────────────────────────────────────────────────────────
   mobile responsive (Slice 11)

   Below 768px the desktop two-column layout collapses to a single
   column. Both panes are still server-rendered every time (so a
   direct visit to /c/<peer> always brings the sidebar markup along
   for the JS-less / back-button path); the body's data-view
   attribute drives which pane is visible via CSS.

   - body[data-view="inbox"]  on /          → show sidebar, hide main
   - body[data-view="thread"] on /c/<peer>  → hide sidebar, show main

   The `.back-chevron` <a> on the thread page only paints below
   768px; tapping it loads / (which flips data-view to "inbox" via
   server-side render or view.js's history hook).

   .67.98px is the standard pre-768 anti-overlap edge. The matching
   @media (min-width: 768px) block below keys the desktop chevron
   off-screen and re-asserts the grid template (a no-op against the
   default `.app-shell` but explicit reads better).
   ──────────────────────────────────────────────────────────────────── */

@media (max-width: 767.98px) {
  /* Single-column shell: stack the two panes vertically as a fallback
     for browsers that ignore the display:none rules below (none do,
     but the cascade is cheaper than a no-op). The visible pane fills
     the whole viewport because the inactive one is `display: none`. */
  .app-shell {
    grid-template-columns: 1fr;
  }

  /* Hide the inactive pane based on data-view. The :is() shape keeps
     selector specificity matched between the two view states so a
     later rule doesn't accidentally trump one but not the other. */
  body[data-view="inbox"] .main {
    display: none;
  }
  body[data-view="thread"] .sidebar {
    display: none;
  }

  /* The visible pane spans the full row. The sidebar stops capping
     its width at 320px (the desktop value comes from the
     `.app-shell` grid template) — explicit width: auto plus 100%
     keeps it bounded by the viewport. Mobile contact list scrolls
     vertically within the viewport. */
  .sidebar,
  .main {
    width: 100%;
    max-height: 100vh;
    max-height: 100dvh;
  }

  /* Mobile chevron: reveal the back affordance only here. The
     server always renders it; CSS owns the visibility. */
  .back-chevron {
    display: inline-flex;
  }

  /* Bubble width cap goes up on mobile: 60% looks pinched on a
     narrow phone, 90% feels right while still hinting at the
     in/out alignment via the gutter. */
  .cluster-bubbles {
    max-width: 90%;
  }

  /* Tighter thread paddings on mobile so the bubbles don't feel
     squeezed against the gutter. */
  .thread-header {
    padding: 14px 16px 12px;
  }
  .thread-messages {
    padding: 16px 16px 24px;
  }
  .composer {
    padding-left: 14px;
    padding-right: 14px;
  }
  .composer-error {
    padding-left: 14px;
    padding-right: 14px;
  }

  /* Welcome / empty-pane gets a smaller top padding on narrow
     viewports so the call-to-action lives above the fold. */
  .welcome,
  .main-empty {
    padding: 48px 20px 32px;
  }
}

/* The matching desktop block. The default styles ALREADY cover the
   ≥768px case (the grid template at the top of this file paints
   both panes); this block exists so the chevron stays hidden when
   the viewport crosses back over the breakpoint via a window-resize
   or device-rotate — without an explicit `display: none` at desktop
   the `display: inline-flex` from the mobile block would persist
   when a stale style sheet hadn't yet been replaced. */
@media (min-width: 768px) {
  .back-chevron {
    display: none;
  }
}

/* ────────────────────────────────────────────────────────────────────
   dark mode — OS-driven via prefers-color-scheme. Lifted verbatim
   from the marketing site so the chat surface follows the same mood.

   Slice 12 adds component-level overrides for the chat-specific
   surfaces (image bubbles, broken-image fallback, error fragments,
   outbound text colour, sidebar avatar ring). Most things flow
   automatically through the `var(--name)` indirection; the overrides
   below handle the cases where a hardcoded contrast assumption
   would otherwise break (e.g. outbound bubbles painting --paper text
   on --sky-deep — that combo flips fine in dark mode but the broken-
   image red caption needs a darker container against dark paper).
   ──────────────────────────────────────────────────────────────────── */
@media (prefers-color-scheme: dark) {
  :root {
    --paper:        #14171c;
    --paper-warm:   #1c2026;
    --paper-deep:   #252a32;
    --ink:          #ece3cc;
    --ink-soft:     #b8b09a;
    --ink-faint:    #8a826f;

    --sky:          #2a3a4f;
    --sky-deep:     #6e91b4;
    --sky-haze:     rgba(110, 145, 180, 0.22);

    --forest:       #92b27d;
    --forest-deep:  #b8c79a;
    --moss:         #b8c79a;

    --terracotta:   #e09478;
    --sunset:       #e3a47a;
    --gold:         #e6c073;

    --line:         rgba(236, 227, 204, 0.10);
    --line-soft:    rgba(236, 227, 204, 0.06);
    --shadow:       0 1px 2px rgba(0, 0, 0, 0.40),
                    0 8px 24px rgba(0, 0, 0, 0.45);
    --shadow-deep:  0 4px 12px rgba(0, 0, 0, 0.45),
                    0 24px 48px rgba(0, 0, 0, 0.50);
  }

  /* Outbound bubbles render light text (--paper) on --sky-deep in
     light mode. In dark mode --paper is near-black, which would
     make the outbound text invisible. Pin the outbound text to ink
     (cream in dark mode, near-black in light mode) so the contrast
     stays right in both schemes.
     Light-mode outbound was paper-on-sky-deep deliberately — the
     bubble reads as the user's voice via the warm cream — but the
     dark-mode equivalent (cream-on-sky-deep) reads cleanly too. */
  .cluster-out .bubble {
    color: var(--ink);
  }
  /* Status icon "muted" states inside outbound bubbles previously
     used --paper-deep (a dark sand) and --paper-warm (cream) for the
     pending/sending/delivered states — fine on a light bubble with
     near-white text, but the dark-mode paper-deep is a charcoal that
     vanishes against the sky-deep fill. Re-pin the muted states to
     a higher-contrast cream-with-alpha so the indicator stays
     visible against the dark sky-deep bubble. */
  .bubble .status-pending,
  .bubble .status-sending {
    color: rgba(236, 227, 204, 0.55);
  }
  .bubble .status-delivered {
    color: rgba(236, 227, 204, 0.85);
  }

  /* Broken-image fallback caption: light-mode used paper-deep as the
     background and terracotta for the text. Dark-mode paper-deep is
     a much darker charcoal; the terracotta read against it still
     works but the caption sits inside the bubble's 4px padding so we
     re-paint the fallback with a slightly different fill so the
     "broken" affordance is clearly distinct from a normal image
     bubble's load state. */
  .bubble-image-broken-fallback {
    background: rgba(224, 148, 120, 0.12);
  }

  /* Composer error fragment: paper-deep + terracotta in light mode.
     In dark mode the same tokens flip cleanly but the terracotta on
     dark paper-deep can read a touch hot — pull the background to a
     terracotta-tinted overlay so the error reads as a distinct
     surface from the rest of the composer. */
  .composer-error-message {
    background: rgba(224, 148, 120, 0.12);
  }
  /* Drag-over composer state: light mode lifts to paper-deep. In dark
     mode the dashed sky-deep outline already does most of the work;
     a slightly lighter background reinforces the drop-target. */
  .composer.drag-over {
    background: var(--paper-deep);
  }

  /* Sidebar avatar ring: the inset white ring polish-pass added in
     Slice 5 was tuned for cream paper. Against the darker sidebar
     in dark mode it reads as a faint highlight — keep the ring but
     shift it to a cream-on-shadow tint so the avatar anchor still
     reads. The token-only rule means the existing accent shifts
     (forest → softer green, sky → deeper blue, terracotta → warmer
     orange) flow through to the avatar fills automatically. */
  .avatar {
    box-shadow: inset 0 0 0 1px rgba(236, 227, 204, 0.10);
  }
}
