/* Lina Pocket, the workshop in your pocket.
   Atelier design language, lifted 1:1 from the lina4.ai site (web/app/static/style.css,
   D-058): warm ember on near-black, Hanken Grotesk, fluid type via clamp() so it stays
   calm at 100% and unbroken at 125% (the iPhone default). No fixed pixel text sizes,
   rem/ch everywhere, controlled wrapping. The signature ember "lamp" dot carries the
   workshop light through. No terminal green. */

@import url("https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800&display=swap");

:root {
  --bg: #0C0B0A;
  --surface: #1E1813;
  --surface2: #2A2219;
  --border: #3A3026;
  --border2: #39301f33;
  --text: #F1ECE3;
  --muted: #B6AD9F;
  --faint: #8C8273;
  --ember: #EFA24A;
  --ember-deep: #E07B3C;
  --info: #7CC4F2;
  --done: #8FB07A;   /* a calm sage for "all wrapped up": settled, not a terminal green (T4) */
  --grad: linear-gradient(120deg, #F3B05C, #E07B3C);

  /* fluid type scale, clamped to the viewport (D-058) */
  --step--1: clamp(0.84rem, 0.80rem + 0.20vw, 0.95rem);
  --step-0: clamp(1rem, 0.95rem + 0.28vw, 1.12rem);
  --step-1: clamp(1.15rem, 1.05rem + 0.6vw, 1.4rem);
  --step-2: clamp(1.5rem, 1.2rem + 1.6vw, 2.1rem);
  --step-3: clamp(1.9rem, 1.4rem + 2.6vw, 2.9rem);
  --gutter: clamp(1.1rem, 0.6rem + 2.6vw, 1.6rem);

  --radius: 1rem;
  --radius-sm: 0.6rem;

  /* The centred reading column (operator 2026-07-01, step 4): one source for how wide the
     conversation reads on a big screen (~66 characters). The thread already uses it; the top bar,
     the composer and the jump-to-latest button bind to it too, so on desktop everything lines up in
     one calm column instead of stretching edge to edge. */
  --reading-max: 46rem;

  /* Warm translucent darks for overlays/chips, one source (the lightbox + zoom-hint chip). */
  --scrim: #0b0906f2;      /* full-screen backdrop behind a picture */
  --warm-glass: #1a140de6; /* floating controls: close button, action bar */
  --chip: #14100acc;       /* small on-image chip (the zoom hint) */
}

* { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Hanken Grotesk", system-ui, -apple-system, sans-serif;
  font-size: var(--step-0);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* App-shell, SESSION conversation page ONLY (operator 2026-06-26, A1 iOS-anchor). That page is the
   one place text bled above the top bar and below the composer on iPhone Safari (toolbar at the
   bottom, 100% zoom). Root cause: a document that scrolls, with a sticky bar and a fixed composer
   anchored to the layout viewport, lets iOS leave a band at the visible top/bottom (two opaque-fill
   attempts did not help, the band is a render effect of the document scroll, not transparency).
   Fix: make THIS page an app-shell, the document does not scroll (body is a fixed-height flex
   column, 100dvh tracks Safari's dynamic toolbars), only #view scrolls inside it. Scoped to
   .session-shell so the overview/login/project pages keep their normal document scroll, untouched.
   Keyboard (the one thing 100dvh misses on older iOS) is handled in app.js by shrinking this height
   to the visible area while a field is focused; interactive-widget=resizes-content covers iOS 17.4+. */
body.session-shell {
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* The bar becomes a top tile of the column (not sticky over a scrolling document). position:relative
   (not static) so its z-index stays effective: it must paint OVER the 2px the scroller tucks under
   it (see body.session-shell .wrap margin-top), otherwise the scrolling content would paint on top
   of the bar's bottom edge (static z-index is ignored). */
body.session-shell .bar { position: relative; z-index: 6; flex: none; }
/* #view is the ONE scroll container: it fills the gap between bar and composer and scrolls inside;
   min-height:0 lets it actually shrink so overflow-y takes effect inside the flex column */
body.session-shell .wrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  /* Full content width, independent of how much is in view (operator 2026-06-26: the status line
     looked narrower in Quiet than in Clean/Full). As a flex item, .wrap's "margin:0 auto" (for the
     46rem max-width centering) cancels the default stretch and makes it shrink-wrap to its content,
     so Quiet (one short message) made #view, and with it the status line, narrower. width:100% gives
     it a definite cross size again (capped by max-width, still centered by the auto margins), so the
     width no longer depends on the content. Only needed here because #view is a flex item now; the
     other pages keep .wrap as a normal block that already fills the width. */
  width: 100%;
  /* Tuck the scroller 2px UNDER the bar (operator 2026-06-26, iPhone): the bar is opaque and on top
     (z-index 6), the sticky status line inside #view sits at top:0 = the bar's bottom edge. At iOS
     sub-pixel rounding a 1-3px hairline opened between the two where scrolling thread text flashed
     through. A 2px overlap puts that seam behind the opaque bar; the status line still reads flush
     under the bar (its hidden top 2px are only border/corner, the pill has its own inner padding). */
  margin-top: -2px;
}

/* faint warm grain, the one real workshop-material detail (kept premium/subtle) */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  opacity: 0.04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
}

h1, h2, h3 {
  margin: 0;
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.04;
  text-wrap: balance;
}
p { margin: 0; text-wrap: pretty; }
a { color: inherit; text-decoration: none; }

/* ---- top bar -------------------------------------------------------------------------- */
.bar {
  /* Overview/project/login keep a normal sticky bar over a scrolling document. The session page
     overrides this to a static flex tile (see body.session-shell .bar). --vv-offset is unset off
     the session page, so this resolves to top:0 there. */
  position: sticky;
  top: var(--vv-offset, 0px);
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 0.3rem 0.3rem;
  flex-wrap: wrap;
  padding: 0.85rem var(--gutter);
  padding-top: max(0.85rem, env(safe-area-inset-top));
  /* Solid, not translucent (operator 2026-06-26, iPhone Safari bar at the bottom): bar.top measured
     0 (the bar IS flush), so "text above the menu" was conversation text SHOWING THROUGH the 0.86
     glass + blur. An opaque fill stops the show-through; the bar still reads clean and warm. */
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

/* the brand mark: ember lamp dot + wordmark, carries the workshop light */
.mark {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 800;
  font-size: var(--step-1);
  letter-spacing: -0.02em;
}
/* The wordmark's Werkstatt-Lampe: the canonical lit work-lamp mark, inlined from the one source
   brand/lamp.svg (templates/_lamp.html), replacing the earlier plain ember dot so Pocket carries
   the SAME pendant lamp as the website (D-062/D-260). Size keeps the 22:28 viewBox aspect so the
   lamp is not letterboxed; colours are mixed from the --ember/--text tokens, no raw hex (mirrors
   web .logo .lamp). The old .lamp::before cord is gone -- the lamp now draws its own cord. */
.lamp {
  position: relative;
  display: inline-flex;
  align-self: center;
  width: 0.92rem;
  height: 1.17rem;
  flex: none;
}
.lamp .lampsvg { width: 100%; height: 100%; overflow: visible; }
.lamp .lamp-cord { stroke: var(--faint); stroke-width: 1.1; stroke-linecap: round; }
.lamp .lamp-shade { fill: color-mix(in srgb, var(--ember) 58%, var(--text)); }
.lamp .lamp-rim { stroke: color-mix(in srgb, var(--text) 72%, var(--ember)); stroke-width: 0.9; stroke-linecap: round; }
.lamp .lamp-beam { fill: var(--ember); opacity: 0.14; }
.lamp .lamp-bulb { fill: color-mix(in srgb, var(--text) 68%, var(--ember)); }
.lamp .lamp-glow { fill: var(--ember); opacity: 0.3; filter: blur(1.4px); }
.mark .tld { color: var(--muted); font-weight: 500; }
a.mark { color: var(--text); text-decoration: none; }
/* The lina4.ai wordmark (home + overview), matching the website: "lina" in text, the "4" in ember,
   ".ai" quiet and a touch smaller (operator 2026-06-26). One inline-flex unit so the bar's gap sits
   between the lamp and the word, never between the letters. */
/* Wordmark, matched to the website's brand-spec rendering (web .logo, brand/README "Wortmarke/Logo"):
   "lina" leads bold; the 4 keeps its ember accent but at a SUBORDINATE size so it no longer reads as
   a big version number (operator 2026-07-15: the full-size "4" was the "alte Schrift mit der grossen 4");
   ".ai" steps back as a quiet domain suffix. Lowercase stays (brand/README: bewusst Kleinschreibung). */
.wordmark { display: inline-flex; align-items: baseline; letter-spacing: -0.005em; }
.wordmark .four { color: var(--ember); font-weight: 600; font-size: 0.72em; margin-left: 0.03em; }
.wordmark .tld { color: var(--muted); font-weight: 500; font-size: 0.72em; margin-left: 0.02em; opacity: 0.9; }
/* the project name sits on its OWN second line under the menu row (operator 2026-06-26): at 125%
   zoom the wordmark + name + gear + step out were too wide for one row and the menu wrapped; a
   full-width second line keeps row 1 compact and the name clearly readable, one clipped line. */
.proj-name {
  flex-basis: 100%;
  min-width: 0;
  margin-top: 0.2rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 700;
  font-size: var(--step-0);
  letter-spacing: -0.01em;
  color: var(--text);
}

.bar-spacer { margin-left: auto; }

/* the conversation theme: a small line under the bar so you always know where you are.
   It's a button: tap it to read the full name big (it's clipped here). */
/* The conversation's name, shown only on tap of the tag symbol (operator 2026-06-25: reading
   it permanently is overkill and ate the top space). Full name, allowed to wrap since it's
   on-demand. */
/* The conversation's name: small, quiet, permanently under the bar, ONE clipped line. There if
   you glance for it, not in the way; tap it for the full name. Toggle off in the gear. */
.convo-name {
  flex-basis: 100%;
  min-width: 0;
  margin-top: 0.3rem;
  padding: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: clamp(0.74rem, 0.70rem + 0.18vw, 0.82rem);
  color: var(--faint);
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
@media (hover: hover) { .convo-name:hover { color: var(--muted); } }
.convo-name[hidden] { display: none; }
/* The header name is now the work area (worktree) with the fill figure in brackets, e.g.
   "lina4-webchat (142k)" (operator 2026-07-09). One line, never wrapped, never cut (operator
   2026-07-10, "nie auf 2 zeilen umbrechen bei 125 zoom"): when the row is tight the whole name
   (area + bracket) scales DOWN a touch via --cn-scale (set by fitConvoName in app.js) so both stay
   whole, instead of clipping the area or breaking onto a second line. Worktree names are short, so
   the shrink is barely visible and almost never needed; the ellipsis below is only a last net for a
   pathologically long name past the scale floor. */
.convo-name { display: flex; align-items: baseline; gap: 0.3rem; white-space: normal;
  --cn-fs: clamp(0.74rem, 0.70rem + 0.18vw, 0.82rem);
  font-size: calc(var(--cn-fs) * var(--cn-scale, 1)); }
.cn-area { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cn-fill { flex: none; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--faint); }
.cn-fill[hidden] { display: none; }
/* Warm bands that deepen as the conversation fills, mirroring the bench gauge (never a traffic
   light): calm below firm, ember when getting full, deep ember when full. */
.cn-fill[data-stage="2"] { color: var(--ember); }
.cn-fill[data-stage="3"] { color: var(--ember-deep); }
/* A small "i" beside the conversation name (operator 2026-06-26): the visible cue that the name
   is tappable. Same quiet faint colour as the name, sized to the name's line (not a big round tap
   target), so it sits snug under the bar. Tapping it opens the SAME conversation-info box as
   tapping the name. */
.convo-info {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--faint);
  border-radius: 50%;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) { .convo-info:hover { color: var(--muted); } }
.convo-info[hidden] { display: none; }
/* The name rides the one calm header row now (operator 2026-07-01): the i + name travel as one
   group that grows to fill the space between the lamp and the menu, the name clipped to one line.
   Overrides the name's stand-alone flex-basis:100% (which used to force it onto its own row).
   flex-BASIS 0 (not auto): with auto, flex-wrap sized this group at the name's FULL width when
   deciding line breaks, so a long name shoved the icon group onto a second row even though the name
   could clip. Basis 0 makes the group count as zero for that decision, so it grows into the free
   space and clips instead, and the icons stay on the one row (operator 2026-07-02, Max). The bar
   keeps flex-wrap only as the extreme-zoom fallback, so nothing is ever cut off. */
.convo-head { flex: 1 1 0; min-width: 0; display: flex; align-items: center; gap: 0.15rem; }
.convo-head .convo-name { flex: 1 1 0; min-width: 0; margin-top: 0; }
/* the neutral "more" menu symbol, same round footprint as the gear */
.more-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
@media (hover: hover) { .more-btn:hover { color: var(--ember); border-color: var(--ember-deep); } }
.more-btn.nudge { color: var(--ember); border-color: var(--ember); }
.more-btn[aria-expanded="true"] { color: var(--ember); }

/* the brand lamp as a home link, sitting right after the back chevron on every sub-page,
   so Lina's mark is always present and always leads home (operator 2026-06-23). */
/* padding-left:0 so the lamp sits as close to the back chevron as it does on the overview, where the
   wordmark's .mark has no left padding (operator 2026-06-26: in a conversation the lamp sat farther
   from the chevron than in the project's conversation list). */
/* min 44x44 tap target like .back: the old 0.2rem padding left a tiny lamp hitbox that missed
   first taps (operator 2026-07-02). No justify-content, so the lamp stays flush-left by the
   chevron (padding-left:0); the box only grows rightward/down to fill the tap target. */
.brand { flex: none; display: inline-flex; align-items: center; min-width: 44px; min-height: 44px; color: var(--text); padding: 0.2rem; padding-left: 0; }
@media (hover: hover) { .brand:hover { color: var(--ember); } }

/* slim back/home chevron (icon only); the page name leads right after it (operator 2026-06-23) */
.back {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  /* A full 44x44 tap target (the visible chevron stays 24px, centred): the old ~30px box missed
     first taps. And NO negative left margin: it pulled the box into the iOS left-edge back-swipe
     zone, where the system ate the first tap (operator 2026-06-29 "first tap feels dead"). The
     wider box also closes the dead gap to the lamp. Fits the bar height, so the bar does not grow. */
  min-width: 44px;
  min-height: 44px;
  padding: 0.2rem;
}
.back:hover { color: var(--text); }
/* "step out" (logout) on home + overview: now a round icon button matching the gear (a door-with-
   arrow-out glyph), with clear breathing room from the gear so the two never crowd (operator
   2026-06-26). */
.step-out {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  margin-left: 0.7rem;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
}
@media (hover: hover) { .step-out:hover { color: var(--ember); border-color: var(--ember-deep); } }
/* The exit glyph (door on the left, arrow out to the right) is GEOMETRICALLY centred but its ink
   weighs to the left (the solid door frame vs the thin arrow), so it reads as sitting too far left
   (operator 2026-06-27). Measured ink centroid was 2.03 viewBox units left of centre; a 1.5px nudge
   right brings it to optical centre, on par with the other three icons (~0). Glyph-only, the button
   stays put. */
.step-out svg { transform: translateX(1.5px); }
/* the quiet "project:" label leading the project name on its second line (operator 2026-06-26) */
.proj-key { color: var(--faint); font-weight: 600; }

/* stage pills, allowed to wrap as a group below the title rather than overflow */
/* Quiet/Glance/Full as ONE compact segmented switch (operator 2026-06-25): narrower than three
   separate pills, so the whole menu fits beside the lamp instead of wrapping the gear away. */
.stages {
  display: inline-flex;
  margin-left: 0;
  border: 1px solid var(--border);
  border-radius: 2rem;
  overflow: hidden;
}
.stage-btn {
  background: transparent;
  color: var(--muted);
  border: 0;
  border-radius: 0;
  /* Larger again than the status-line fixes had left them, but ~7% under step-0 so the three pills
     + lamp + icons still fit ONE row at iPhone 125% zoom, where full step-0 just wrapped (operator
     2026-06-26). Own clamp sitting between step--1 and step-0. */
  /* Trimmed side padding so all three can be EQUAL width (see min-width) WITHOUT widening the row
     and risking a wrap; the text is centred (operator 2026-06-26: "Full" looked too small because
     its word is shorter). */
  padding: 0.4rem 0.3rem;
  font-family: inherit;
  font-size: clamp(0.92rem, 0.88rem + 0.24vw, 1.02rem);
  font-weight: 600;
  cursor: pointer;
  line-height: 1.2;
  white-space: nowrap;
  text-align: center;
  min-width: 3.15rem;   /* fits the widest label ("Clean"/"Quiet"); "Full" grows to match, centred */
}
.stage-btn + .stage-btn { border-left: 1px solid var(--border); }
.stage-btn:hover { color: var(--text); }
.stage-btn.on { color: #1b1206; background: var(--grad); }

/* The small symbols travel as ONE group: beside the length switch when there's room, else they
   drop to a clean second row together (never a lone gear, operator 2026-06-25). margin-left:auto
   keeps them at the right edge of whichever row they land on, and lets the switch sit next to the
   lamp filling the previously empty top-row space. */
.bar-icons { display: flex; align-items: center; gap: 0.4rem; margin-left: auto; }
.gear {
  flex: none;
  /* same iOS native-button nudge + same fix as .bar-iconbtn: the svg fills the whole button (below),
     padding 0, so the nudge has no room. Size is set by the gear svg's own padded viewBox, which is a
     touch wider than the bar icons' so the busier gear motif reads optically on par (reworked 07-15). */
  -webkit-appearance: none;
  appearance: none;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.gear:hover { color: var(--text); border-color: var(--border2); }

/* settings panel behind the gear: a small popover, never covers the chat permanently */
.settings-scrim { position: fixed; inset: 0; z-index: 18; background: rgba(0, 0, 0, 0.45); }
.settings-scrim[hidden] { display: none; }
.settings-panel {
  position: fixed;
  z-index: 19;
  top: calc(env(safe-area-inset-top) + 3.6rem);
  right: var(--gutter);
  width: min(22rem, calc(100% - 2 * var(--gutter)));
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem 1.2rem 1.2rem;
  box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.5);
}
.settings-panel[hidden] { display: none; }
.settings-h { font-size: var(--step-1); margin-bottom: 0.6rem; }
.setting {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 0;
  border-top: 1px solid var(--border);
}
.setting:first-of-type { border-top: 0; }
.setting-text { min-width: 0; }
.setting-label { font-weight: 700; }
.setting-sub { color: var(--muted); font-size: var(--step--1); margin-top: 0.15rem; text-wrap: pretty; }
/* C8 (operator 2026-06-27): the reach-mode picker. Full-width block (no switch on the right); the
   two modes as a segmented pair, the tabula-rasa confirm tucked under them. */
.reach-mode { display: block; }
.reach-mode[hidden] { display: none; }
.reach-seg {
  display: flex; gap: 0.4rem; margin-top: 0.5rem;
  border: 1px solid var(--border); border-radius: var(--radius); padding: 0.25rem;
}
.reach-btn {
  flex: 1 1 0; min-width: 0;
  background: transparent; color: var(--muted);
  border: 0; border-radius: calc(var(--radius) - 0.25rem);
  padding: 0.55rem 0.5rem; font: inherit; font-size: var(--step--1); font-weight: 600;
  cursor: pointer; text-align: center;
}
.reach-btn.on { background: var(--grad); color: #1b1206; }
.reach-confirm { margin-top: 0.6rem; }
.reach-confirm[hidden] { display: none; }
.reach-ask { margin: 0 0 0.5rem; color: var(--text); font-size: var(--step--1); text-wrap: pretty; }
.reach-confirm-actions { display: flex; gap: 0.5rem; }
.reach-go {
  flex: 1 1 auto; background: var(--grad); color: #1b1206; border: 0;
  border-radius: var(--radius-sm); padding: 0.6rem; font: inherit; font-weight: 700; cursor: pointer;
}
.reach-cancel {
  flex: 0 0 auto; background: transparent; color: var(--muted);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.6rem 0.9rem; font: inherit; cursor: pointer;
}
.setting-note { color: var(--ember); font-size: var(--step--1); margin-top: 0.45rem; text-wrap: pretty; }
.setting-note[hidden] { display: none; }
/* P10b (operator 2026-06-30): a compact indented sub-line that hangs under the setting above it
   (no top border, tighter padding, slightly inset), carrying a small switch on the right. Keeps the
   menu short (the trigger was a 125%-zoom space pinch on the iPhone). */
.setting-sub-row {
  border-top: 0; align-items: center;
  padding: 0.1rem 0 0.5rem 0.9rem; margin-top: -0.35rem;
}
.setting-sub-row[hidden] { display: none; }
.setting-sublabel { color: var(--muted); font-size: var(--step--1); font-weight: 600; text-wrap: pretty; }
.switch.switch-sm { width: 2.5rem; height: 1.5rem; }
.switch.switch-sm .switch-knob { width: 1.05rem; height: 1.05rem; }
.switch.switch-sm.on .switch-knob { left: calc(100% - 1.23rem); }

/* a setting that stacks its control under the text (e.g. the length chooser) */
.setting-col { flex-direction: column; align-items: stretch; gap: 0.7rem; }

/* end-this-conversation: a calm, deliberate control (not alarming red; warm ember-deep). */
.end-btn {
  align-self: flex-start;
  background: transparent;
  color: var(--ember-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  padding: 0.5rem 1rem;
  cursor: pointer;
}
@media (hover: hover) { .end-btn:hover { border-color: var(--ember-deep); } }
.end-btn[hidden] { display: none; }
.end-confirm { background: var(--ember-deep); color: #1b1206; border-color: var(--ember-deep); }
.end-confirm:disabled { opacity: 0.6; cursor: default; }
.seg { display: flex; gap: 0.4rem; }
.seg-btn {
  flex: 1 1 0;
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 2rem;
  padding: 0.45rem 0.6rem;
  font-family: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  cursor: pointer;
  line-height: 1.2;
}
.seg-btn.on { color: #1b1206; background: var(--grad); border-color: transparent; }
/* the earlier-conversation sort uses aria-pressed (set in project.js) for its active pill,
   same warm look as the .on pills. */
.seg-btn[aria-pressed="true"] { color: #1b1206; background: var(--grad); border-color: transparent; }

/* G4c: the search + sort bar above the earlier conversations. Calm, small, doesn't crowd the
   list; the search grows, the sort sits beside it and wraps under on narrow phones. */
.earlier-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: center;
  justify-content: space-between;   /* sort to the left, the magnifier to the right (like home) */
  margin: 0.6rem 0 1rem;
}
/* The search field itself now carries the home search's class (.home-search), so its look is
   identical and lives in ONE place; the reveal/clear behaviour matches home too (project.js). */
.earlier-sort { flex: 0 0 auto; }
.earlier-sort .seg-btn { flex: 0 0 auto; }

/* the on/off switch */
.switch {
  flex: none;
  position: relative;
  width: 3rem;
  height: 1.8rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 1rem;
  background: var(--bg);
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s;
}
.switch .switch-knob {
  position: absolute;
  top: 50%;
  left: 0.18rem;
  transform: translateY(-50%);
  width: 1.3rem;
  height: 1.3rem;
  border-radius: 50%;
  background: var(--faint);
  transition: left 0.2s, background 0.2s;
}
.switch.on { background: var(--grad); border-color: transparent; }
.switch.on .switch-knob { left: calc(100% - 1.48rem); background: #1b1206; }

/* ---- page shell ----------------------------------------------------------------------- */
.wrap {
  max-width: var(--reading-max);
  margin: 0 auto;
  padding: clamp(1.4rem, 0.8rem + 2vw, 2.2rem) var(--gutter)
           calc(env(safe-area-inset-bottom) + 2rem);
}
.wrap.has-composer {
  /* Session app-shell: the composer is now a sibling tile below #view, not a fixed overlay on top
     of it, so the old ~9.5rem clearance is gone; just a little breathing room under the last bubble.
     (has-composer is only ever set on the session page.) */
  padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem);
  /* The status line is the first thing in here and sticks flush under the bar (top: --bar-h). Its
     resting flow position must ALSO be flush, or it sits with a gap below the bar whenever the page
     is too short to scroll (Quiet view), while Full scrolls it sticky-flush, so the same strip
     looked placed differently per mode (operator 2026-06-26: measured gap 6.4px in Quiet, 0 in
     Full). padding-top 0 makes the resting position equal to the sticky position: flush in every
     mode, and no upward travel on the first scroll either. (The old 0.4rem dated from the removed
     ::before shelf; with the opaque pill flush under the bar there is nothing to leave room for.) */
  padding-top: 0;
}

.section {
  margin: clamp(2rem, 1rem + 3vw, 2.8rem) 0 0.9rem;
  font-size: 0.78rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ember);
  font-weight: 700;
}
/* The first section heading on a page sits close to the bar. CAUTION: ":first-of-type" is
   by ELEMENT TYPE, so a nested heading (e.g. the "Pinned" row's own h2) made the next
   heading count as "first" and lose its top gap (operator 2026-06-23, recurring spacing bug).
   Fix: when a VISIBLE block precedes a section, that section keeps the full top gap. */
.section:first-of-type { margin-top: 0.5rem; }
.frontrow:not([hidden]) + .section { margin-top: clamp(2rem, 1rem + 3vw, 2.8rem); }
/* the Pinned row itself: heading close to the top, generous gap to whatever follows */
.frontrow > .section { margin-top: 0.5rem; }
.frontrow { margin-bottom: clamp(2rem, 1rem + 3vw, 2.8rem); }

/* T8: search by the "What we're building" heading. The heading becomes a row with a quiet
   magnifier at the far right; tapping it reveals the field below. Keeps .section's own margins
   (still an h2.section), so the page spacing is untouched. */
.home-section { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
.section-search-btn {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; padding: 0;
  border: 0; background: transparent;
  color: var(--muted); border-radius: var(--radius-sm);
  cursor: pointer;
}
@media (hover: hover) { .section-search-btn:hover { color: var(--ember); } }
.section-search-btn[aria-expanded="true"] { color: var(--ember); }
.home-search-wrap { margin: 0 0 1.1rem; }
.home-search {
  width: 100%;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 2rem;
  padding: 0.6rem 1rem; font-family: inherit; font-size: var(--step-0);
}
.home-search::placeholder { color: var(--faint); }
.empty.soft { color: var(--faint); font-size: var(--step--1); padding: 0.6rem 0; }

.note { color: var(--muted); font-size: var(--step--1); margin-bottom: 0.6rem; }
.empty {
  color: var(--faint);
  font-size: var(--step-0);
  padding: 1.1rem 0;
}

/* The empty-conversation welcome (P8, operator 2026-06-29): a small warm workshop scene + an
   inviting line, in place of a bare "Nothing here yet". Centred, calm, and compact so it still
   fits at 125% zoom. body.cw-typing hides it the instant you tap into the field (the keyboard then
   has the room); it returns if you leave the field empty. */
.chat-welcome {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.55rem;
  padding: 1.6rem 1.1rem 0.8rem;
}
.cw-art {
  width: min(58%, 200px);
  height: auto;
  margin-bottom: 0.2rem;
}
.cw-title {
  margin: 0;
  font-size: var(--step-1);
  font-weight: 700;
  color: var(--text);
}
.cw-sub {
  margin: 0;
  font-size: var(--step--1);
  color: var(--muted);
  max-width: 22rem;
  line-height: 1.45;
}
body.cw-typing .chat-welcome { display: none; }

/* ---- work list (overview cards) ------------------------------------------------------- */
/* One calm column when space is tight (narrow / 125% zoom), two columns once there's room
   to fill, so a wide screen or tablet-landscape shows more without anything breaking.
   minmax(17rem, 1fr) guarantees each column stays wide enough; below ~34rem the track
   simply can't fit twice, so it collapses to one. No JS, no zoom detection, fully fluid. */
.cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.8rem;
  grid-template-columns: repeat(auto-fill, minmax(min(17rem, 100%), 1fr));
  /* stretch so two cards in the same row are the SAME height even when one carries an
     extra status line and the other doesn't (operator 2026-06-23, clearest on iPad) */
  align-items: stretch;
}
/* When a card is open, drop to one full-width column so the expanded details use the
   whole row instead of leaving a lonely second column beside it (operator 2026-06-23).
   Collapsing returns to the multi-column grid. :has() covers modern Safari; the
   .has-open class (set in index.js / project.js) is the reliable fallback. */
.cards:has(.pcard.open),
.cards.has-open {
  grid-template-columns: 1fr;
}

.card {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-width: 0;
  padding: 1.1rem 1.2rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  transition: border-color 0.2s, transform 0.2s;
}
.card:hover { border-color: var(--ember-deep); transform: translateY(-1px); }

.card-body { min-width: 0; flex: 1 1 auto; }
.card-title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-weight: 700;
  font-size: var(--step-1);
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}
.card-sub {
  display: block;
  margin-top: 0.2rem;
  color: var(--muted);
  font-size: var(--step--1);
}
/* worktree tag, only shown when a project spans more than one. A clearly distinct filled
   chip (own background, smaller, lighter, label-like), so it never reads as part of the
   status text next to it (operator: must be visually separable). */
.where {
  display: inline-block;
  margin-left: 0.5rem;
  padding: 0.08rem 0.5rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  color: var(--faint);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  vertical-align: 0.08em;
}

/* state dot on a live card, same ember-lamp vocabulary */
.dot {
  flex: none;
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  background: var(--faint);
  box-shadow: 0 0 0.5rem transparent;
}
.dot.working { background: var(--ember); box-shadow: 0 0 0.6rem var(--ember); }
/* Resting (idle) is a QUIET grey, not the active amber, so a finished/resting conversation no longer
   looks like the working ones (operator 2026-06-28: resting cards read as "orange/needs attention").
   Matches the status line, where idle is muted. Ember now means actually working. */
.dot.idle { background: var(--faint); box-shadow: none; }
.dot.waiting,
.dot.needs_you { background: #7CC4F2; box-shadow: 0 0 0.6rem #7cc4f2aa; }
.dot.error { background: var(--ember-deep); box-shadow: 0 0 0.6rem var(--ember-deep); }
.dot.done { background: var(--done); box-shadow: 0 0 0.6rem var(--done); }
/* T4: the status dot sits inline before the status word in a card's subline; small and aligned
   to the text, a quiet colour cue paired with the word right beside it. */
.psub .dot { display: inline-block; width: 0.5rem; height: 0.5rem; margin-right: 0.4rem; vertical-align: 0.04em; }
.card-chevron { flex: none; color: var(--faint); }

/* ---- project cards (overview, "Variante 2": tap shows info, arrow opens) -------------- */
.pcard {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  /* Slimmer boxes (operator 2026-06-26: the conversation boxes in a project were too tall): less
     vertical padding, so a collapsed card is a low, calm row. Same value for project + convo cards
     so they read from one mould. */
  padding: 0.55rem 0.6rem 0.55rem 0.7rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  flex-wrap: wrap;
  cursor: pointer;
}
/* live search hides non-matches via [hidden]; the class selector above would
   otherwise win over the UA [hidden] rule, so spell it out (same for convo cards) */
.pcard[hidden] { display: none; }
/* hover only on real pointers, so a tap on the phone doesn't leave a stuck border */
@media (hover: hover) {
  .pcard:hover { border-color: var(--border2); }
}
/* the ember border now MEANS open (matches the tap-to-expand mental model) */
.pcard.open { border-color: var(--ember-deep); }

/* pin, left, a proper tap size; faint until pinned, then ember */
.pin {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.4rem;
  height: 2.4rem;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--faint);
  cursor: pointer;
}
@media (hover: hover) {
  .pin:hover { color: var(--muted); }
}
.pcard.pinned .pin { color: var(--ember); }

/* the name (and, when running, a small second line); tap to expand the details */
.pcard-body {
  /* flex-basis 0 (not auto): the text column does NOT claim its content width, so the info + arrow
     always share the SAME row instead of being pushed onto a second one when the name is long
     (operator 2026-06-26: long-name cards were taller, the "i" dropped below). The 1-line clamp +
     min-width:0 ellipsize the name within whatever width is left. */
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.1rem 0;
  border: 0;
  background: transparent;
  color: var(--text);
  text-align: left;
  font-family: inherit;
  cursor: pointer;
}
.pname {
  font-weight: 700;
  font-size: var(--step-1);
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;          /* keep the list tidy; the full name is a tap away */
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The fill figure in a card name, e.g. "(142k)", and the start-time tiebreaker "14:30" when two
   conversations share one work area (operator 2026-07-10). The fill is a touch smaller than the
   name and deepens with the fill, mirroring the header badge (.cn-fill) and the bench gauge; the
   time is quiet and faint, there only to tell same-worktree conversations apart. */
.pn-fill { font-weight: 700; font-size: 0.86em; font-variant-numeric: tabular-nums; color: var(--faint); white-space: nowrap; }
.pn-fill[data-stage="2"] { color: var(--ember); }
.pn-fill[data-stage="3"] { color: var(--ember-deep); }
.pn-when { font-weight: 600; font-size: 0.82em; color: var(--faint); font-variant-numeric: tabular-nums; white-space: nowrap; }
.psub { font-size: var(--step--1); color: var(--ember); overflow-wrap: anywhere; }
.psub.muted { color: var(--muted); }
/* a state that is a call to you (waiting / needs you / stopped) stands out warmly */
.psub.alert { color: var(--ember); font-weight: 700; }
.pdetails .drow.alert { color: var(--ember); font-weight: 700; }

/* the pin moved out of the front row: a tiny ember star marks a pinned card, and the
   actual toggle lives in the expanded details, so nothing competes with the thumb. */
.pin-flag {
  display: none;
  margin-right: 0.35rem;
  color: var(--ember);
  vertical-align: -0.12em;
}
.pcard.pinned .pin-flag { display: inline-block; }
.pin-toggle {
  background: transparent;
  color: var(--ember);
  border: 1px solid var(--border2);
  border-radius: 2rem;
  padding: 0.4rem 0.95rem;
  font-family: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  cursor: pointer;
}
.pcard.pinned .pin-toggle { background: var(--surface2); border-color: var(--ember-deep); }

/* conversation cards reuse the project-card shell; the live dot sits at the front */
/* convo cards use the same dot spacing as the project cards (.psub .dot); no convo-specific override,
   so "Happening now" and the home project cards read identically (operator 2026-06-26). */

/* T4: the info symbol expands the details (a plain card tap now jumps into the project). Quiet
   at rest, ember when the details are open, sized as a comfortable tap target beside the arrow. */
.pcard-info {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.6rem;
  height: 2.6rem;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--faint);
  border-radius: 50%;
  cursor: pointer;
}
@media (hover: hover) { .pcard-info:hover { color: var(--muted); } }
.pcard-info[aria-expanded="true"] { color: var(--ember); }

/* the open arrow: big, its own clear tap target with a subtle well, opens the project */
.open-arrow {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.4rem;
  height: 3.4rem;
  border-radius: 50%;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--ember);
}
.open-arrow:active { background: var(--bg); }
@media (hover: hover) {
  .open-arrow:hover { border-color: var(--ember-deep); }
}
/* convo cards (Happening now + Earlier): the info + open arrow ride as ONE compact cluster on the
   right, beside the status, instead of the arrow wrapping to its own row (operator 2026-06-26, save
   height). The arrow is a touch smaller here than on the home project cards so the pair fits snug
   next to the status light + text; the worktree name moved into the details (only under info). */
.convo-actions { flex: none; display: inline-flex; align-items: center; gap: 0.25rem; margin-left: auto; }
/* convo cards stay ONE uniform height (operator 2026-06-26): a long name clamps to a single line
   with an ellipsis (the full name is in the details and a tap away), so a two-line title never grows
   the card nor pushes the info/arrow down out of line with the status. */
/* convo cards: name at the calmer step-0 size (not the bigger step-1). Up to TWO lines, like the
   project cards, so longer names aren't cut to one line (operator 2026-06-27, "aus einem Guss");
   the min-height below keeps short cards uniform. */
.pcard.convo .pname { -webkit-line-clamp: 2; font-size: var(--step-0); }
/* ALL convo cards the SAME height (operator 2026-06-26): a fixed min-height so a card without a
   status subline is exactly as tall as one with it, and the body centres within it. Combined with
   .pcard-body flex:1 1 0 (info/arrow never wrap) every collapsed convo card is one uniform row. */
.pcard.convo { min-height: 4.6rem; }
/* A just-started conversation whose transcript isn't on disk yet (operator 2026-06-29): a calm,
   non-tappable placeholder ("A fresh chat / Getting ready"), the same height as a real convo card so
   the list doesn't jump when the gentle refresh upgrades it. No info/arrow cluster, so it reads as
   "settling", not as something to act on. */
/* The pending placeholder is now enterable (P1, operator 2026-06-29): a real cursor, a readable
   (not greyed-out-dead) name, and its body is a link, so it reads as a card you can tap into
   instead of an inert stub you get stuck behind. */
.pcard.pending { min-height: 4.6rem; opacity: 0.95; }
.pcard.pending .pcard-body { text-decoration: none; color: var(--text); }
.pcard.pending .pname { font-size: var(--step-0); color: var(--muted); }
@media (hover: hover) { .pcard.pending:hover { border-color: var(--ember-deep); } }
.pcard.convo .pcard-info { width: 2.4rem; height: 2.4rem; }
.pcard.convo .open-arrow { width: 2.7rem; height: 2.7rem; }
.pcard.convo .open-arrow svg { width: 24px; height: 24px; }

/* the expanded details (after a tap on the body), full width under the row */
.pdetails {
  flex-basis: 100%;
  margin-top: 0.2rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--border);
}
.pdetails[hidden] { display: none; }
.pdetails .drow { font-size: var(--step--1); margin: 0.15rem 0; }
.pdetails .drow.muted { color: var(--muted); }
.pdetails .drow.state { color: var(--text); }
/* In the conversation-info SHEET the details reuse .pdetails ONLY for the shared colour rules, so the
   box reads exactly like the expanded overview card; the card-only top divider/spacing is dropped
   because the sheet header already separates the content (operator 2026-06-26). */
.filesheet-card .pdetails { border-top: 0; margin-top: 0; padding-top: 0; }
/* the at-a-glance state line atop the project-info sheet (the home card's subline, which lives
   outside .pdetails); a little air below it before the detail rows begin. */
.sheet-state { margin: 0.1rem 0 0.6rem; display: flex; align-items: center; }

/* a labelled field: the label on its own line, the value below (e.g. Latest / Full name),
   so a long title is clearly separated from its label and never runs together */
.field { margin: 0.6rem 0 0.9rem; }
.field .dk {
  display: block;
  font-size: 0.72rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--faint);
  font-weight: 700;
}
.field .dv {
  display: block;
  margin-top: 0.1rem;
  font-size: var(--step--1);
  color: var(--text);
  overflow-wrap: anywhere;
}
/* the conversation number (e.g. max-vcrm), or the short internal code when the register
   holds none (app/convo.py) -- monospace and quiet either way */
.code-id { font-family: ui-monospace, "SF Mono", Menlo, monospace; color: var(--muted); letter-spacing: 0.03em; }

/* the Latest topic is a link straight into that conversation: signal it's tappable */
.latest-link { display: block; }
.latest-link .dv { color: var(--ember); }
.latest-link .dv::after { content: " \203A"; color: var(--faint); font-weight: 700; }
@media (hover: hover) { .latest-link:hover .dv { color: var(--ember-deep); } }

/* ---- needs-you action rows (handlungsbedarf first, with a direct jump) ----------------- */
/* A calm card per pending call, a line icon and a colour by kind (palette, no harsh
   contrast). In the project overview the whole row is a link straight into the chat. */
.acts { display: grid; gap: 0.4rem; margin-bottom: 0.6rem; }
.act {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  min-width: 0;
  padding: 0.7rem 0.8rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--info);
  border-radius: var(--radius-sm);
  color: var(--text);
}
@media (hover: hover) { a.act:hover { border-color: var(--border2); } }
.act-error { border-left-color: var(--ember-deep); }
.act-ico { flex: none; margin-top: 0.12rem; color: var(--info); }
.act-error .act-ico { color: var(--ember-deep); }
.act-body { min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.act-lead { font-weight: 700; font-size: var(--step--1); }
.act-text { color: var(--muted); font-size: var(--step--1); overflow-wrap: anywhere; }

/* soft detail lines (work state, gentle notes) and the confirmed live address */
.pdetails .drow.soft { color: var(--muted); }
/* G7b: the ONE allowed real-tech aside (CONCEPT Abnahmecheck Punkt 1): "(not committed)" /
   "(not pushed)" behind the work-state line, set small and faint, only here in the open details. */
.techtag { font-size: 0.82em; color: var(--faint); white-space: nowrap; }
.pdetails .drow.live a { color: var(--ember); text-decoration: underline; text-underline-offset: 2px; overflow-wrap: anywhere; }
.dv-note { color: var(--faint); font-weight: 400; }

/* work areas: a quiet toggle, then a small list each tapping into one area */
/* (The old aufklappbare "workspaces" sub-list styles lived here; removed with the list itself,
   replaced by the .plat-area buttons above. Operator 2026-07-18.) */

/* The platform's permanent work areas: prominent, one full-width tappable button each, at the top of
   the platform project page. Vertical list (never a squeezed row of chips) so it reflows cleanly at
   100% and 125% iPhone zoom: each name may wrap onto its own second line, the state line and the
   chevron hold their place. Warm ember dot marks an area with a live conversation. */
.plat-areas { margin-bottom: 1.8rem; }
.plat-area-list { display: flex; flex-direction: column; gap: 0.55rem; }
.plat-area {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  width: 100%;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.9rem 1rem;
  color: var(--text);
  font: inherit;
  cursor: pointer;
  transition: border-color 0.15s ease;
}
.plat-area:hover { border-color: var(--faint); }
.plat-area:focus-visible { outline: 2px solid var(--ember); outline-offset: 2px; }
.plat-area:disabled { opacity: 0.55; cursor: default; }
/* The state dot reuses the shared .dot vocabulary (colour by real state: ember = working, grey =
   resting/idle, blue = needs you, green = done), so a button matches its conversation card below
   instead of a flat "orange = anything here". Just nudge it up to sit centred with the two lines. */
.plat-area .dot { align-self: center; }
/* More than one conversation in an area: stack a dot per conversation, top-to-bottom in the SAME
   order as the cards that expand below, so each dot's colour maps to its conversation. Vertically
   centred as a group; a single conversation keeps the one centred dot above. */
.plat-area .dot-stack { align-self: center; flex: none; display: flex; flex-direction: column; align-items: center; gap: 0.3rem; }
/* Each area is a row: the big tappable button, and a small round + to open a NEW conversation right
   here. The + is on every row (even empty areas, whose plain tap starts the first anyway) so all rows
   keep the same width. */
.plat-area-row { display: flex; align-items: center; gap: 0.5rem; }
.plat-area-row .plat-area { flex: 1 1 auto; width: auto; }
.plat-area-add {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem; border-radius: 50%;
  background: var(--surface); color: var(--ember);
  border: 1px solid rgba(239, 162, 74, 0.4);
  cursor: pointer; transition: border-color 0.15s ease, background 0.15s ease;
}
.plat-area-add:hover { border-color: var(--ember); background: rgba(239, 162, 74, 0.1); }
.plat-area-add:focus-visible { outline: 2px solid var(--ember); outline-offset: 2px; }
.plat-area-add:disabled { opacity: 0.55; cursor: default; }
.plat-area-main { flex: 1 1 auto; min-width: 0; }
.plat-area-name { display: block; font-weight: 700; font-size: var(--step-0); line-height: 1.25; text-wrap: balance; }
.plat-area-sub { display: block; font-size: var(--step--1); color: var(--muted); margin-top: 0.12rem; }
.plat-area-chev { color: var(--faint); flex: none; transition: transform 0.15s ease; }
/* On a multi-conversation area the chevron points DOWN (it expands, not navigates) and flips up when open. */
.plat-area-chev.chev-toggle { transform: rotate(90deg); }
.plat-area.open .plat-area-chev.chev-toggle { transform: rotate(-90deg); }
/* The "more than one conversation here" marker: a warm amber warning triangle + count, never red. */
.plat-area-warn {
  display: inline-flex; align-items: center; gap: 0.25rem; flex: none;
  color: var(--ember); background: rgba(239, 162, 74, 0.12);
  border: 1px solid rgba(239, 162, 74, 0.4); border-radius: 2rem;
  padding: 0.12rem 0.45rem; font-size: 0.78rem; font-weight: 800;
}
/* The area's conversations, expanded in place under its button (same cards as Happening-now). */
.plat-area-sub-list {
  display: flex; flex-direction: column; gap: 0.5rem; list-style: none;
  margin: 0.15rem 0 0.35rem 0.6rem; padding: 0.15rem 0 0.2rem 0.8rem;
  border-left: 2px solid rgba(239, 162, 74, 0.35);
}
.plat-area-sub-list[hidden] { display: none; }

/* Happening now, framed per work area (worktree): a soft box with the worktree name as a quiet
   header and its running conversations inside, so several sessions in one worktree read as one group. */
.live-group { border: 1px solid var(--border); border-radius: var(--radius); padding: 0.5rem; background: rgba(30, 24, 19, 0.4); }
.live-group-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0.45rem 0.55rem; }
.lg-name { font-size: var(--step--1); font-weight: 700; color: var(--muted); overflow-wrap: anywhere; }
.lg-count { flex: none; font-size: 0.76rem; font-weight: 800; color: var(--ember); background: rgba(239, 162, 74, 0.14); border-radius: 2rem; padding: 0.03rem 0.5rem; }
.lg-cards { display: flex; flex-direction: column; gap: 0.5rem; list-style: none; }

/* bench space: how much room a conversation has left, calm, counts down */
/* a bit more air UNDER the bench/workspace-space reading (operator 2026-06-26): the state phrase
   sat too tight to what follows in the expanded card and the conversation-info box. The more-menu
   keeps its own tighter spacing via .filesheet-bench below. */
.bench { margin: 0.7rem 0 1rem; }
.bench-head { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; margin-bottom: 0.3rem; }
.bench-k { font-size: 0.72rem; letter-spacing: 0.13em; text-transform: uppercase; color: var(--faint); font-weight: 700; }
.bench-left { font-size: var(--step--1); color: var(--muted); }
.bench-bar { height: 0.5rem; background: var(--bg); border: 1px solid var(--border); border-radius: 1rem; overflow: hidden; }
.bench-fill { display: block; height: 100%; background: var(--grad); border-radius: 1rem; }
/* Warm amber fill bands that deepen as the bench fills (G7a). Never a traffic light:
   stays in the amber/ember family (CONCEPT: warm, no red/green). `level` comes from
   fillgauge.bench_space and tracks the absolute stage (low = quiet, mid = SOFT/filling,
   high = FIRM+URGENT/getting full), in sync with the phrase and the segmented bar (P9/P10). */
.bench-fill.low  { background: linear-gradient(120deg, #F6C07E, #EDA85A); }
.bench-fill.mid  { background: linear-gradient(120deg, #F3B05C, #E07B3C); }
.bench-fill.high { background: linear-gradient(120deg, #E89A3F, #C9621F); }
.bench-phrase { margin-top: 0.3rem; font-size: var(--step--1); color: var(--muted); }
/* The handoff choice under the gauge when the bench is getting full (operator 2026-06-29):
   a warm primary "hand off" plus a quiet "keep working". Stacked so each is a full-width tap. */
.bench-act { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.8rem; }
.bench-handoff, .bench-keep {
  font: inherit;
  font-size: var(--step--1);
  text-align: center;
  padding: 0.6rem 0.9rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.bench-handoff {
  background: linear-gradient(120deg, #F3B05C, #E07B3C);
  color: #2A1808;
  font-weight: 700;
  border: 0;
}
.bench-keep {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border);
}
@media (hover: hover) { .bench-keep:hover { color: var(--text); border-color: var(--muted); } }

/* the actions row in the details: a clear "Step inside / Open it" so it's obvious the
   arrow leads further in, plus the pin toggle */
.pactions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-top: 0.8rem; }
.step-in {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  color: #1b1206;
  background: var(--grad);
  border: 1px solid transparent;
  border-radius: 2rem;
  padding: 0.45rem 1rem;
  font-size: var(--step--1);
  font-weight: 700;
}
.step-in svg { margin-right: -0.15rem; }

/* G6a: "Pick this back up" wakes a closed conversation. Dezent, secondary to "Come on in":
   a quiet outlined pill in the warm palette, never shouting. */
.wake-btn {
  display: inline-flex;
  align-items: center;
  color: var(--ember);
  background: transparent;
  border: 1px solid var(--ember);
  border-radius: 2rem;
  padding: 0.42rem 0.95rem;
  font-family: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  cursor: pointer;
  line-height: 1.2;
}
.wake-btn:disabled { opacity: 0.6; cursor: default; }
.wake-msg { flex-basis: 100%; margin: 0.4rem 0 0; }

/* ---- start-new form ------------------------------------------------------------------- */
.starter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
}
.field-label {
  flex: 1 1 100%;
  font-size: var(--step--1);
  color: var(--faint);
  margin: 0.2rem 0 -0.2rem;
}
.starter input,
.starter select {
  flex: 1 1 9rem;
  min-width: 0;
  min-height: 3rem;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.6rem 0.85rem;
  font: inherit;
  font-size: var(--step-0);
}
.starter input:focus,
.starter select:focus,
.login input:focus,
.composer textarea:focus {
  outline: none;
  border-color: var(--ember-deep);
  box-shadow: 0 0 0 2px rgba(224, 123, 60, 0.18);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 3rem;
  background: var(--grad);
  color: #1b1206;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 0 1.4rem;
  font-family: inherit;
  font-size: var(--step-0);
  font-weight: 700;
  cursor: pointer;
  line-height: 1.2;
}
.btn:disabled { opacity: 0.5; cursor: default; }
.starter .btn { flex: 0 0 auto; }

/* ---- login ---------------------------------------------------------------------------- */
.login-shell {
  max-width: 26rem;
  margin: clamp(2.5rem, 1.5rem + 5vw, 5rem) auto 0;
}
.login-shell h1 { font-size: var(--step-2); }
.login-shell .lead {
  color: var(--muted);
  font-size: var(--step-1);
  margin: 0.8rem 0 1.8rem;
  max-width: 34ch;
}
.login {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}
.login input {
  min-height: 3.2rem;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.8rem 1rem;
  font: inherit;
  font-size: var(--step-0);
}
.login .btn { min-height: 3.2rem; }
.form-error {
  border: 1px solid var(--border2);
  background: rgba(224, 123, 60, 0.12);
  border-radius: var(--radius-sm);
  padding: 0.75rem 0.9rem;
  margin-bottom: 1.1rem;
  color: var(--text);
  font-size: var(--step--1);
}

/* the login gets the workshop scene behind it (same curated imagery as the lina4.ai site),
   so the door into Lina feels warm, not a bare password box. The image is dark and warm
   already; a gradient deepens the lower half so the card and text stay legible. */
.login-scene {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* focal point a touch right so the lamp hangs fully in frame over the card,
     centred above it rather than cut off at the right edge (operator 2026-06-23) */
  background: url("/static/img/scene-workshop.jpg") 62% 30% / cover no-repeat;
}
.login-scene::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    rgba(12, 11, 10, 0.45) 0%,
    rgba(12, 11, 10, 0.74) 52%,
    rgba(12, 11, 10, 0.95) 100%
  );
}
.login-page .bar { background: transparent; backdrop-filter: none; border-bottom: 0; position: relative; z-index: 2; }
.login-page .wrap { position: relative; z-index: 2; }
/* the card floats lower so the lamp and bench show above it, then sits in a soft frosted
   panel for warmth and readability */
.login-page .login-shell {
  margin-top: clamp(7rem, 34vh, 16rem);
  background: rgba(18, 15, 12, 0.66);
  backdrop-filter: blur(7px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: clamp(1.4rem, 1rem + 2vw, 2rem) clamp(1.2rem, 0.8rem + 2vw, 1.8rem) clamp(1.5rem, 1rem + 2vw, 2.1rem);
}
.login-page .login-shell .lead { margin-bottom: 1.5rem; }

/* Short / landscape viewports: the keyboard + Apple's autofill bar eat the lower
   half (worst on phones turned sideways, ~40% of the height). Pull the card to the
   top and slim it down so the field and "Come in" stay above that band; the focus
   handler in login.html also scrolls the field into view. */
@media (max-height: 600px) and (orientation: landscape) {
  .login-page .bar { padding-top: max(0.4rem, env(safe-area-inset-top)); }
  .login-page .login-shell {
    margin-top: clamp(0.6rem, 2vh, 1.4rem);
    padding: 1rem 1.1rem 1.2rem;
  }
  .login-shell h1 { font-size: var(--step-1); }
  .login-shell .lead,
  .login-page .login-shell .lead { margin: 0.45rem 0 1rem; font-size: var(--step-0); }
  .login { gap: 0.7rem; }
  .login input,
  .login .btn { min-height: 2.8rem; }
}

/* ---- session: stage views ------------------------------------------------------------- */
.loading { color: var(--muted); font-size: var(--step-0); padding: 1rem 0; }

/* Glance: a sticky status line, replaced in place, never appended */
.statusline {
  position: sticky;
  /* App-shell (A1): the bar now lives OUTSIDE the scroll container, so the sticky status line docks
     at the very top of #view (top:0). This also retires the old --bar-h + --vv-offset sticky-top and
     the sub-pixel jump it could cause (a fractional sticky top made iOS re-round the pill's contents
     on each re-render); top:0 is integer, so that class of jump cannot occur. */
  top: 0;
  z-index: 5;
  /* T2 TEARDOWN + REBUILD #2 (operator 2026-06-26, "rip it out, different logic AND design"):
     1) LOGIC: CSS GRID, not flex. Flex on iOS mis-aligned the text item on the cross axis (it sat low
        no matter what we did, while Chromium centred it perfectly, which is why it was unfindable).
        Grid centres each cell by its own box and does NOT have that baseline quirk.
     2) DESIGN: the old strip was #322817 with a grey border, almost the bubble colour, so it blurred
        into the conversation when scrolling. Now a clearly warmer AMBER strip with an ember edge and
        an amber hairline, so it reads as "this is the status", not a message.
     3) JUMP: a compositor layer (translateZ) stops the sticky strip from re-rounding/jittering while
        you scroll back and forth. */
  display: grid;
  grid-template-columns: auto 1fr;   /* icon | text ONLY; the nav/go float on top (see below) */
  align-items: center;
  column-gap: 0.6rem;
  min-width: 0;
  height: 2.4rem;
  line-height: 1.25;
  padding: 0 3.4rem 0 0.85rem;   /* right room for the floating nav/go so the text never runs under it */
  /* No transparent bottom margin: a margin under an OPAQUE sticky bar is an unpainted band the
     conversation scrolls through (operator 2026-06-29: "text scrolls through between the status line
     and the needs-you banner"). Content sits flush under the bar now; the bottom hairline below gives
     the calm separation instead of empty air. */
  margin-bottom: 0;
  /* DISTINCT and clearly BOXED (operator 2026-06-26: it must stand out AND have visible top/bottom
     edges to judge the centring). A warm amber fill, clearly lighter/warmer than the grey-brown
     bubbles, framed by an amber border (not the grey bubble border) plus the strong ember edge. */
  /* The pill's OWN opaque background shows now (the old ::before shelf, z-index:-1, painted OVER this
     because sticky+z-index makes a stacking context, so the shelf colour was what you saw, not this,
     that's why it looked like a bubble, then black; operator 2026-06-26). Opaque + flush under the bar
     means nothing scrolls through behind it, no shelf needed. */
  /* CALM resting look (operator 2026-06-26, picked V5 from the colour mock-up): the old mid-brown
     fill with a full ember frame read as "brown on orange" and was disliked. Now a dark warm fill
     with a faint amber wash flowing in from the left edge, and the ember edge as the only firm
     accent, no hard frame. Reads as warm and settled, not a colour block. The ATTENTION states
     (needs/error) override this below to stand out. */
  /* Fill lifted off near-black: the old #14100b sat barely above the canvas (--bg #0C0B0A) and was
     even darker than the bubbles (--surface #1E1813), so the bar blurred into the conversation
     (operator 2026-06-29). A warmer base, a touch above the bubbles, reads as its own warm bar. */
  background: linear-gradient(90deg, rgba(239, 162, 74, 0.16), rgba(239, 162, 74, 0.04) 38%, transparent), #261D13;
  /* 3px to match Lina's bubble rail (.msg.assistant border-left:3px), so the warm left edge runs as
     ONE straight rail from the bar into the messages instead of stepping 4px->3px (operator 2026-06-29). */
  border-left: 3px solid var(--ember);
  /* Square: flush under the .bar, and the orange left edge runs straight down (no inward bow). Square
     also avoids the rounded-corner triangles leaking conversation text now that content sits flush. */
  border-radius: 0;
  color: var(--text);
  font-size: var(--step-0);
  overflow-wrap: anywhere;
  /* A calm warm hairline + a light shadow define the bottom edge (operator 2026-06-29: replaces the
     transparent-air separation). */
  border-bottom: 1px solid rgba(239, 162, 74, 0.20);
  box-shadow: 0 0.15rem 0.35rem rgba(0, 0, 0, 0.3);
}
/* every cell sits on the row centre */
.statusline > * { align-self: center; }
/* T2 KEY FIX: an inline <svg> sits on the text baseline and drags a descent gap under it, which on
   iOS inflates the flex line and pushes the text low. As a block it is a clean box with no baseline
   pull, so the row height is exactly the content and the text centres. (operator 2026-06-26 rebuild) */
.statusline svg { display: block; }
/* While you're writing, the status line steps aside so the field and the conversation get the
   room; it returns the moment you leave the field (operator 2026-06-25). */
body.composing .statusline { display: none; }
/* In the ask view, the "Needs you" strip steps aside once you've seen the question (operator
   2026-06-28): it has reminded you, so it stops taking space and nagging until the next question.
   Toggled live in updateAskBanner(). */
.statusline.ask-hidden { display: none; }
/* No more ::before shelf: it caused the whole colour bug (it painted OVER the pill because of the
   stacking context, so the shelf colour was what showed). The pill is opaque and flush under the bar
   now, so nothing scrolls through behind it (operator 2026-06-26). */
/* G2: one symbol + word. The symbol carries the state; warm ember throughout (no terminal
   colours). Working gently breathes so you can tell at a glance she's busy. */
.statusline .status-ico {
  flex: none;
  display: inline-flex;
  align-items: center;
  color: var(--ember);
}
/* working = a slowly TURNING gear, thinking = the breathing SPARK (operator HKDG 2026-06-27).
   Both states render as .state-working, so the animation keys off the ICON class, not the line. */
/* Spin an INNER <g>, NOT the outer <svg>: iOS Safari does not reliably animate transform on an outer
   svg (esp. with display:flex) and the gear looked frozen on the phone, while in Chromium it turned;
   an inner group with fill-box + centre turns in place everywhere (operator HKDG 2026-06-27). */
/* The spinner is now a wrapping HTML <span class="ico-working gearspin"> (statusIcon in app.js): rotate
   the plain HTML element, which iOS Safari animates reliably, instead of an SVG <g> (that stayed frozen
   on the phone, operator 2026-06-30). Faster 1.8s turn so "she's busy" reads at a glance. */
.statusline .ico-working.gearspin {
  animation: spin 1.8s linear infinite;
  transform-origin: center;
}
/* WORKING gets a quiet warm accent so you can tell at a glance something is running, especially at the
   start of a fresh conversation (operator 2026-06-30). Deliberately gentler than the filled NEEDS bar:
   a warmer left edge + a brighter ember icon + slightly bolder word, not the loud deep-warm fill. */
.statusline.state-working {
  border-left-color: var(--ember);
}
.statusline.state-working .status-text { font-weight: 600; }
.statusline.state-working .status-ico { color: var(--ember); }
.statusline .status-ico.ico-thinking { animation: breathe 2.4s ease-in-out infinite; }
/* ATTENTION state: a question/permission is waiting (needs_you/waiting). N2 from the colour
   mock-up (operator 2026-06-26): a FILLED deep-warm bar with light, bolder text, so a real call
   for you stands out sharply against the calm dark normal look and is never missed. */
.statusline.state-needs {
  background: var(--ember-deep);
  border-left-color: #a8531f;          /* a darker warm edge, still legible on the filled bar */
  color: #fff6ec;
  font-weight: 700;
}
.statusline.state-needs .status-ico,
.statusline.state-needs .status-text { color: #fff6ec; }
/* The jump-to-the-question arrow on the right must read on the filled "needs" bar too: its default
   ember-deep colour sat ON the ember-deep fill, so the arrow was all but invisible and the tap-back
   looked unfinished (operator 2026-06-30). Lift it to the same cream as the icon and text. */
.statusline.state-needs .status-go { color: #fff6ec; }
/* error stays the calm bar with a deep-warm edge + icon (a snag to look at when you can, not a
   call that blocks you), so it reads as clearly different from "needs you". */
.statusline.state-error { border-left-color: var(--ember-deep); }
.statusline.state-error .status-ico { color: var(--ember-deep); }
.statusline.state-done .status-ico { color: var(--done); }   /* green, matching the .dot done colour (operator 2026-06-28: status symbols follow the dot palette) */
.statusline.state-idle .status-ico { color: var(--muted); }
.statusline.state-idle .status-text,
.statusline.state-done .status-text { color: var(--muted); }
@keyframes breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .statusline .status-ico,
  .statusline .ico-working.gearspin,
  .statusline .status-ico.ico-thinking { animation: none; }
}
/* Block-step arrows on the right of the status line: jump turn-to-turn. Muted, warm on hover. */
.statusline .status-text {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* A NORMAL line-box (inherits 1.25) fully contains ascenders AND descenders, so overflow:hidden
     (for the ellipsis) never clips a p/g/y. Grid centres the line-box perfectly on the device
     (MEASURED on the operator's iPhone via ?debug self-measure: text offset 0 in every mode and
     every switch, no jump). The ONE remaining knob is --sl-nudge, set from the TEXT-INDEPENDENT
     cap-centre, not the ink box (the ink box moves with each word's descenders, which is why a
     first attempt centred on a descender word left "at the bench" sitting high). Measured on the
     operator's iPhone: fAsc 17 / fDesc 5, capHeight 11.2 at 16.2px font, so the cap centre rests
     +0.4px low by default; -0.4px / 16.2px = 0.025em lifts it onto the pill centre for EVERY word
     (cap offset reads ~0). In em so it holds across zoom. Re-measure CAP after any change. */
  transform: translateY(var(--sl-nudge, -0.025em));
}
/* The nav arrows float on the RIGHT, OUT of the grid flow, so whether they are present (Clean/Full)
   or absent (Quiet) the status row is structurally IDENTICAL and the text never shifts by mode
   (operator 2026-06-26: that mode-dependent shift was the real bug). Vertically centred by top/bottom. */
.status-nav { position: absolute; right: 0.85rem; top: 0; bottom: 0; display: inline-flex; align-items: center; gap: 0.15rem; }
.status-navbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--muted);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
@media (hover: hover) { .status-navbtn:hover { color: var(--ember); } }
/* In the question view the status line hides itself while the question is in view (JS toggles this),
   so the question gets the whole screen; it reappears when you scroll up. */
.statusline.ask-hidden { display: none; }
.statusline[data-legend] { cursor: pointer; }
/* Cross-conversation nudge: another running conversation needs you. Attention edge (ember-deep,
   like needs/error); the whole line is a tap-through, with a small chevron at the end. */
.statusline.state-elsewhere { border-color: var(--ember-deep); cursor: pointer; }
.statusline.state-elsewhere .status-ico { color: var(--ember-deep); }
/* The cross-conversation nudge now rides as a SECOND banner directly under your own status line
   (operator 2026-06-28), so both stay visible. It sticks just below the normal line (its height
   2.4rem + 0.5rem margin = 2.9rem). When the normal line steps aside in the ask view (.ask-hidden),
   the banner takes the top slot so there's no empty gap above it. */
/* Dock the cross-conversation banner FLUSH under the normal line (top = the line's 2.4rem height,
   border-box), so the two opaque bars stack with no transparent band between them for the
   conversation to scroll through (operator 2026-06-29). */
.statusline.second-banner { top: 2.4rem; z-index: 4; }
.statusline.ask-hidden + .statusline.second-banner { top: 0; }
.status-go {
  position: absolute;
  right: 0.85rem;
  top: 0;
  bottom: 0;
  display: inline-flex;
  align-items: center;
  color: var(--ember-deep);
}

/* The legend: a small overlay that explains what each status symbol means (G2). */
/* The question guard: shown when you send in the reader while a question is open, so your words are
   never silently routed into the question and never lost (operator 2026-06-25). Calm overlay like
   the legend; the primary choice keeps your words and takes you to the question. */
.guard-scrim {
  position: fixed;
  inset: 0;
  z-index: 32;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--gutter);
  background: rgba(8, 7, 6, 0.72);
}
.guard-card {
  width: 100%;
  max-width: 23rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.2rem 1.25rem;
}
.guard-head { font-weight: 700; margin-bottom: 0.6rem; }
.guard-text { color: var(--muted); font-size: var(--step--1); line-height: 1.55; margin: 0 0 1rem; text-wrap: pretty; }
.guard-btn {
  display: block;
  width: 100%;
  margin-top: 0.55rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: var(--step-0);
  text-align: left;
  cursor: pointer;
}
.guard-btn.primary { background: var(--grad); color: #1b1206; border-color: transparent; font-weight: 600; }
.guard-btn.ghost { background: transparent; color: var(--muted); text-align: center; }
@media (hover: hover) { .guard-btn:hover { border-color: var(--ember-deep); } }
.legend {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--gutter);
  background: rgba(8, 7, 6, 0.72);
}
.legend[hidden] { display: none; }
.legend-card {
  width: 100%;
  max-width: 22rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem 1.2rem;
}
.legend-head { font-weight: 700; margin-bottom: 0.8rem; }
.legend-row { display: flex; align-items: flex-start; gap: 0.7rem; padding: 0.5rem 0; color: var(--text); }
.legend-row .status-ico { flex: none; color: var(--ember); margin-top: 0.15rem; }
.legend-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.legend-sub { font-size: var(--step--1); color: var(--muted); text-wrap: pretty; }
.legend-close {
  margin-top: 1rem;
  width: 100%;
  border: 0;
  border-radius: var(--radius);
  background: var(--grad);
  color: #1b1206;
  font: inherit;
  font-weight: 600;
  padding: 0.6rem;
  cursor: pointer;
}

/* Quiet/Glance: calm event cards, only shown when you are actually needed */
.events { display: grid; gap: 0.9rem; }
.event {
  min-width: 0;
  padding: 1.1rem 1.2rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--ember-deep);
  border-radius: var(--radius);
}
.event.kind-question { border-left-color: var(--ember); }
.event.kind-error { border-left-color: var(--ember-deep); }
.event-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}
.event-head .act-ico { flex: none; color: var(--info); }
.event.kind-error .event-head .act-ico { color: var(--ember-deep); }
.event-lead {
  color: var(--ember);
  font-size: var(--step--1);
  font-weight: 700;
}
.event-text { white-space: pre-wrap; overflow-wrap: anywhere; }

/* Decide by tap: answer buttons under an open question/permission. Warm and inviting (you
   answer right here), wrapping cleanly at phone width and 125% (one per row when narrow). */
.choices {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.9rem;
}
.choice-btn {
  flex: 1 1 auto;
  min-width: min(100%, 14ch);
  text-align: left;
  background: var(--surface2);
  color: var(--text);
  border: 1px solid var(--ember-deep);
  border-radius: var(--radius);
  padding: 0.7rem 0.95rem;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  cursor: pointer;
  text-wrap: pretty;
}
.choice-btn:hover { border-color: var(--ember); background: var(--surface); }
.choice-btn:active { transform: translateY(1px); }

/* Instant tap feedback (lockAsk, audit 2026-07-02): the moment you commit a choice/step/send, the
   whole ask panel locks so a second tap can't fire a second keypress into the live tmux menu (the
   send-keys-vs-poll race), and the control you tapped stays lit (the "chosen" look) so it's obvious
   what you picked. The next poll rebuilds the panel and clears this; a failed action brings the
   question back unlocked. */
.ask.is-locked { pointer-events: none; }
.ask.is-locked .choice-btn,
.ask.is-locked .ask-submit,
.ask.is-locked .ask-navbtn,
.ask.is-locked .ask-steps-row,
.ask.is-locked .send,
.ask.is-locked .ask-leave,
.ask.is-locked .ask-answer-input { opacity: 0.5; transition: opacity 120ms ease; }
.ask.is-locked .is-pressed {
  opacity: 1;
  background: var(--grad);
  color: #1b1206;
  border-color: transparent;
  font-weight: 700;
}

/* Multi-select toggles (operator 2026-06-28): each option keeps its ticked state so you can see at
   a glance what you've picked, several at once. A small box on the left fills with a tick when on;
   the whole row warms when selected. One per row so the box and label never crowd at phone width. */
.choices.multi { flex-direction: column; }
.choices.multi .choice-btn { flex: 1 1 100%; display: flex; align-items: center; gap: 0.6rem; }
.choice-btn.toggle .cbox {
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  border: 1.5px solid var(--ember-deep);
  border-radius: 0.35rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--bg);
}
.choice-btn.toggle .cbox .step-check { width: 0.85rem; height: 0.85rem; }
.choice-btn.toggle.on {
  border-color: var(--ember);
  background: rgba(224, 123, 60, 0.14);
}
.choice-btn.toggle.on .cbox { background: var(--ember); border-color: var(--ember); }
.ask-multi-note { margin: 0.9rem 0 0.2rem; font-size: var(--step--1); color: var(--muted); }
/* The explicit Send for a multi-select: warm and clearly the way forward, full width under the
   toggles so it never reads as just another option. Quieter while nothing is picked yet. */
.ask-submit {
  margin-top: 0.9rem;
  width: 100%;
  background: var(--ember);
  color: var(--bg);
  border: 1px solid var(--ember);
  border-radius: var(--radius);
  padding: 0.8rem 1rem;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 700;
  cursor: pointer;
}
.ask-submit:active { transform: translateY(1px); }
.ask-submit.is-empty { background: var(--surface2); color: var(--muted); border-color: var(--ember-deep); }

/* The open-question block: the question owns the screen, clearly its own thing and visibly
   separated from the conversation above it (operator 2026-06-23). A heading names it as a
   question; the question text is large and calm; the answer buttons sit below. */
/* Tail spacer below the question frame: lets a short thread scroll up far enough to bring the
   frame flush under the status line; its height is set live in scrollAskToTop. */
.ask-spacer { width: 100%; }
.ask {
  min-width: 0;
  margin: 1.6rem 0 2rem;
  padding: 1.3rem 1.3rem 1.4rem;
  background: var(--surface);
  border: 1px solid var(--ember-deep);
  border-radius: var(--radius);
  box-shadow: 0 0 0 4px rgba(224, 123, 60, 0.07);
}
/* While a question is open the page becomes a full-height column and the frame is pushed to the
   BOTTOM (operator 2026-06-24: bottom edge near the screen bottom, conversation above it). The
   frame stays content-sized so the whole question + all options read in one screen. */
/* App-shell (A1): #view is already a flex:1 scroll container, so the asking layout only needs to be
   a column whose ask frame bottom-anchors (margin-top:auto). The old min-height:100svh and the
   --vv-bottom keyboard padding are gone: the keyboard now shrinks the whole shell, so a
   bottom-docked permission frame rides above it for free. */
.wrap.asking { display: flex; flex-direction: column; }
/* Flush sticky status line in the ask view too. The asking state REPLACES has-composer (set
   mutually exclusive in applyComposerVisibility), so without this it fell back to the base .wrap
   padding-top (~20px measured) and the sticky "Needs you" strip pinned ~20px BELOW the bar, with
   thread chrome showing through the gap on scroll-up (operator 2026-06-29, IMG_0888). Same reason
   and value as .wrap.has-composer: resting position == sticky position == flush under the bar. */
.wrap.asking { padding-top: 0; }
/* Default: bottom-dock the frame (a permission/error sits in thumb reach for a quick OK, operator
   2026-06-25). A QUESTION instead sits HIGH, right under the status line, so it uses the space and
   reads from the top instead of floating low on a short thread (operator 2026-06-29). */
.wrap.asking .ask { margin-top: auto; }
.wrap.asking .ask.kind-question { margin-top: 0; }
/* a clear divider so the question reads as separate from the chat history scrolled above */
.ask::before {
  content: "";
  display: block;
  height: 1px;
  background: linear-gradient(to right, var(--ember-deep), transparent);
  margin: 0 0 1.1rem;
}
.ask-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--ember);
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: 0.7rem;
}
.ask-head .act-ico { flex: none; color: var(--ember); }
.ask.kind-error .ask-head, .ask.kind-error .ask-head .act-ico { color: var(--ember-deep); }
/* A quiet X to leave the question and return to the chat (operator 2026-06-28). Sits in the top
   corner of the frame, visually small and muted so it never competes with the question; a roomy
   2rem hit area for the thumb without growing the head (negative margins absorb the box). */
.ask-leave {
  margin-left: auto;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  margin-top: -0.5rem;
  margin-right: -0.4rem;
  margin-bottom: -0.5rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  border-radius: 50%;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  text-transform: none;
}
.ask-leave:hover, .ask-leave:active { color: var(--ember); background: rgba(224, 123, 60, 0.12); }
.ask-q {
  font-size: var(--step-1);
  line-height: 1.5;
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}
/* Review/submit screen of a multi-part question: each part as a calm "question -> your answer"
   pair, so you read back exactly what's about to be sent instead of one run-together block
   (operator 2026-06-30). The question is quiet/secondary, your answer stands out. */
.ask-review { display: flex; flex-direction: column; gap: 0.85rem; }
.ask-review-item {
  padding-left: 0.7rem;
  border-left: 3px solid var(--border2);
}
.ask-review-q {
  font-size: var(--step--1);
  line-height: 1.45;
  color: var(--muted);
  overflow-wrap: anywhere;
}
.ask-review-a {
  margin-top: 0.2rem;
  font-size: var(--step-0);
  line-height: 1.45;
  color: var(--text);
  font-weight: 600;
  overflow-wrap: anywhere;
}
/* "What this is about": context for a permission so it's never a blind OK, in Lina's own
   last words (operator 2026-06-25). Quiet, secondary to the question itself. */
.ask-about {
  margin-top: 0.9rem;
  font-size: var(--step--1);
  color: var(--muted);
  text-wrap: pretty;
  overflow-wrap: anywhere;
}
.ask-about-k {
  display: block;
  color: var(--ember);
  font-weight: 600;
  margin-bottom: 0.2rem;
}
/* B2: the open-turn prose Lina wrote right before the question, recovered off the live screen
   (operator 2026-06-26). Sits ABOVE the question as calm, secondary context with a quiet left
   marker; scrolls if long so it never pushes the question and its options off the screen. */
.ask-context {
  margin-bottom: 0.9rem;
  /* Show the whole lead-up; only a very long one scrolls within the box (operator 2026-06-26: the
     start was cut off and the field too small). Generous height so the normal case shows in full. */
  max-height: 42dvh;
  overflow-y: auto;
  padding: 0.1rem 0 0.1rem 0.7rem;
  border-left: 2px solid var(--border2);
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--muted);
  text-wrap: pretty;
  overflow-wrap: anywhere;
}
.ask-context-k {
  display: block;
  color: var(--ember);
  font-weight: 600;
  margin-bottom: 0.2rem;
}
/* A calm caution for a hard-to-undo permission (operator 2026-06-25): a warm warning tone, a
   left marker, never alarming red. */
.ask-caution {
  margin-top: 0.9rem;
  padding: 0.55rem 0.8rem;
  border-left: 3px solid var(--ember-deep);
  background: rgba(224, 123, 60, 0.08);
  border-radius: var(--radius-sm);
  font-size: var(--step--1);
  color: var(--text);
  text-wrap: pretty;
}

.ask .choices { margin-top: 1.1rem; }

/* A multi-part question: a short note that says how it works (answer this, the next comes up,
   no swiping), then a step row with the current part lit and the rest waiting. Informational,
   not buttons (it doesn't jump), so it never looks tappable. */
.ask-steps { margin-bottom: 1.1rem; }
.ask-steps-note {
  margin: 0 0 0.6rem;
  color: var(--muted);
  font-size: var(--step--1);
  text-wrap: pretty;
}
/* Plain text, not chips: a simple progress line so it never invites a tap (operator
   2026-06-23). Current part in ember, the rest muted, separated by middots. */
/* 4a: a flex row (not inline text flow) so every step's TEXT sits on the same line,
   regardless of the outlined current part's extra padding (operator 2026-06-24). The
   outline of .step.current may overhang up/down without pushing its siblings. */
.ask-steps-row { margin: 0; font-size: var(--step--1); font-weight: 600; display: flex; flex-wrap: wrap; align-items: center; row-gap: 0.3rem; }
.step { display: inline-flex; align-items: center; gap: 0.25rem; color: var(--muted); }
.step.done { color: var(--text); }
/* A heading you can tap to jump to that part: a quiet underline cue so it reads as tappable
   without shouting, and a roomy tap target for the thumb (operator 2026-06-28). The current part
   carries no cue (tapping it is a no-op). */
.step.tappable {
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--border2);
  text-underline-offset: 0.2em;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) { .step.tappable:hover { color: var(--ember); text-decoration-color: var(--ember); } }
.step.tappable:active { color: var(--ember); }
/* The part you're on gets a clear outline, so you always know where you are, independent of the
   answered ticks (operator 2026-06-24). */
.step.current {
  color: var(--ember);
  border: 1px solid var(--ember);
  border-radius: 2rem;
  padding: 0.12rem 0.65rem;
}
/* The tick sits a hair high next to the label (its box centres, but the cap-height text reads lower);
   nudge it down so it lines up with the step text (operator 2026-06-27). */
.step-check { flex: none; color: var(--ember); position: relative; top: 0.09em; }
.step-sep { color: var(--border2); margin: 0 0.4rem; }

/* The chosen answer, shown when you step back to an answered part: clearly inverted so it's
   obvious what is currently selected. */
.choice-btn.chosen {
  background: var(--grad);
  color: #1b1206;
  border-color: transparent;
  font-weight: 700;
}

/* In-frame free-text answer: clearly labelled as answering in your own words, sits in the
   question frame, mirrors the normal composer (textarea + round send). */
.ask-answer { margin-top: 1.1rem; }
.ask-answer-label {
  margin: 0 0 0.5rem;
  color: var(--muted);
  font-size: var(--step--1);
  font-weight: 600;
}
.ask-answer-row { display: flex; align-items: center; gap: 0.6rem; }
.ask-answer-input {
  flex: 1 1 auto;
  min-width: 0;
  resize: none;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--ember-deep);
  border-radius: var(--radius);
  padding: 0.7rem 0.9rem;
  font: inherit;
}

/* Step back/forward between the parts of a multi-part question; a lone Next stays on the right. */
.ask-nav { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 1.1rem; }
.ask-navbtn {
  background: transparent;
  border: 1px solid var(--border2);
  border-radius: 2rem;
  color: var(--ember);
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  padding: 0.45rem 0.9rem;
  cursor: pointer;
}
.ask-navbtn.prev { margin-right: auto; }
.ask-navbtn:hover { border-color: var(--ember-deep); }

/* the "all quiet" resting state, warm not empty */
.quiet-rest {
  text-align: center;
  color: var(--muted);
  font-size: var(--step-1);
  max-width: 30ch;
  margin: clamp(2.5rem, 1.5rem + 6vw, 5rem) auto;
  text-wrap: balance;
}
.quiet-rest .lamp { margin: 0 auto 1.1rem; display: block; }

/* Full: the flowing conversation (kept, restyled) */
.thread { display: grid; gap: 1rem; }
/* The open-turn prose above a question is its own thread block (operator 2026-06-26): give it the
   same 1rem gap the chat bubbles have, so it reads as the next message in the flow, not glued on. */
.openturn-thread { margin-top: 1rem; }
.msg {
  position: relative;                       /* anchor for the top-corner "Read it big" on long bubbles */
  min-width: 0;
  padding: 1.1rem 1.2rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
/* "Read it big" at the TOP corner of a genuinely long bubble (operator 2026-06-28), so the reader is
   reachable without scrolling to the bottom tools row. Quiet until touched; the bubble's top padding
   keeps the first line clear of it. */
.reader-top {
  position: absolute;
  top: 0.45rem;
  right: 0.55rem;
  opacity: 0.65;
  z-index: 1;
}
.msg.user {
  background: var(--surface2);
  max-width: 93%;
  justify-self: end;                      /* your words to the right, Lina full width on the left */
  border-right: 3px solid var(--info);    /* a blue edge on your side, so it reads by colour+side */
}
/* Lina's messages carry a warm ember edge on the left. Speaker is clear by colour + side + label,
   not mainly by width, so the text stays wide (which matters on a phone). */
.msg.assistant:not(.interim) { border-left: 3px solid var(--ember); }
.who {
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: 0.02em;
  margin-bottom: 0.5rem;
}
.msg.assistant .who { color: var(--ember); }
.msg.user .who { color: #7CC4F2; }
/* A cancelled step: not a message but a calm event note, so it drops the bubble chrome (no surface,
   border or speaker edge) and sits centred and muted between the bubbles. */
.msg.system {
  background: none;
  border: none;
  padding: 0.4rem 0.6rem;
  justify-self: center;
  max-width: 88%;
  text-align: center;
}
.msg.system .msg-text {
  color: var(--muted);
  font-size: var(--step--1);
  font-style: italic;
}
.msg-text { overflow-wrap: anywhere; }
/* Reply bar inside YOUR bubble: the words you're answering, shown calm and italic with the quoted
   voice's edge (Lina = ember by default, since you mostly reply to her). Marks "this answers that"
   without competing with the bubble's own sender edge; tap to jump up to the quoted spot. */
.reply-quote {
  margin: 0 0 0.55rem;
  padding: 0.3rem 0.65rem;
  border-left: 3px solid var(--ember);
  background: rgba(239, 162, 74, 0.07);
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-style: italic;
  font-size: var(--step--1);
  line-height: 1.5;
  cursor: pointer;
  overflow-wrap: anywhere;
}
.reply-quote > :first-child { margin-top: 0; }
.reply-quote > :last-child { margin-bottom: 0; }
.reply-quote p { margin: 0.15rem 0; }
/* A long quoted question is clamped to a few lines; tapping it (qexpand) unfolds the full wording
   in place. The answer beneath the box always stays whole (operator 2026-06-30). */
.reply-quote.qclamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}
.reply-quote.qclamp.open {
  display: block;
  -webkit-line-clamp: none;
  overflow: visible;
}
/* Who the quoted words belong to, named above the quote (chat-reply style): one upright line (not
   italic like the quote) in the same blue as the "You" sender label, with only the name in ember,
   so "you answered Lina" reads at a glance without a heavy header. */
.reply-quote-who {
  display: block;
  margin-bottom: 0.25rem;
  font-style: normal;
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: 0.01em;
  color: #7CC4F2;            /* the same blue as .msg.user .who ("You") */
}
.reply-quote-name { color: var(--ember); }   /* only "Lina's" in ember: whose words they are */
@media (hover: hover) { .reply-quote:hover { border-color: var(--ember-deep); } }

/* Interim ("still working") lines, shown only in Full: a quiet aside with a thin left rule, not
   a full message card, so the real answers stand out (operator 2026-06-24). Clean hides them. */
.msg.interim {
  background: transparent;
  border: 0;
  border-left: 2px dashed var(--faint);   /* a dashed side-rail (vs Lina's solid ember card) reads "aside" */
  border-radius: 0;
  padding: 0.1rem 0 0.1rem 0.95rem;
}
.msg.interim .who { opacity: 0.65; margin-bottom: 0.2rem; }
/* Readable (not grey), only a hair smaller than a real answer; the "aside" feel comes from italic +
   the indented dashed rail + no card, NOT from shrinking or greying the text (operator 2026-06-24). */
.msg.interim .msg-text {
  color: #D8D0C2;
  font-size: calc(var(--step-0) - 0.05rem);
  font-style: italic;
}
.msg.interim .tools { margin-top: 0.3rem; }

/* rich text (markdown), shared by messages and the reader */
.rt p { margin: 0 0 0.7rem; }
.rt p:last-child { margin-bottom: 0; }
.rt a { color: var(--ember); text-decoration: underline; text-underline-offset: 2px; }
.rt a:hover { color: var(--ember-deep); }
.rt strong { color: var(--text); font-weight: 700; }
.rt code {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 0.35rem;
  padding: 0.05rem 0.35rem;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
  overflow-wrap: anywhere;
}
.rt .code {
  display: block;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.8rem 0.9rem;
  margin: 0.7rem 0;
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.86em;
  line-height: 1.55;
  /* Wrap long lines instead of forcing a sideways scroll: on the phone you should read a code/box
     block straight down, never swipe right to see the end of a line. pre-wrap keeps the block's own
     line breaks and indentation; overflow-wrap breaks even an unbroken long token. operator 2026-07-01 */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.rt .md-h { font-weight: 700; color: var(--text); margin: 0.8rem 0 0.3rem; }
.rt ul { margin: 0.5rem 0; padding-left: 1.3rem; }
.rt li { margin: 0.25rem 0; }

.tools { display: flex; gap: 0.6rem; margin-top: 0.8rem; flex-wrap: wrap; }
.tool-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  background: transparent;
  color: var(--ember);
  border: 1px solid var(--border2);
  border-radius: 2rem;
  padding: 0.4rem 0.95rem;
  font-family: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  cursor: pointer;
}
.tool-btn:hover { border-color: var(--ember-deep); }
.tool-btn:disabled { opacity: 0.4; cursor: default; }
/* "Read it big" as a quiet icon-only button on each message in Full: square, muted, not loud
   (operator 2026-06-25), so it sits beside the dots without shouting. */
.tool-icon { padding: 0.42rem; color: var(--muted); border-color: var(--border2); }
.tool-ico { flex: none; }

/* Block E: a small, quiet "more" trigger on EVERY message; tapping opens the per-message sheet
   (Read it big / Write back / Copy / Share). A visible symbol, not a hidden gesture (long-press
   and swipe are unreliable on an iOS home-screen app), but understated so the words stay the
   focus. Generous tap target via padding even though the glyph is small. */
.msg-more {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: auto;   /* dots to the far right; Read it big sits at the left (operator 2026-06-25) */
  width: 2.4rem;
  height: 2.4rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  opacity: 0.55;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) { .msg-more:hover { opacity: 1; color: var(--ember); } }
.msg.user .tools { justify-content: flex-end; }   /* your row aligns to your side (right) */

/* ---- composer ------------------------------------------------------------------------- */
.composer {
  position: fixed;
  left: 0;
  right: 0;
  /* The field rides the keyboard's top edge (--vv-bottom = how far the keyboard plus its grey
     accessory bar overlaps from below, set in app.js). NOTE 2026-06-25: a strip of conversation
     still shows through between the field and the keyboard on iPhone; the bottom:0 + padding-bottom
     approach made the gap WORSE (operator), so this is reverted to the prior behaviour. Open: solve
     the show-through differently (see TODO / handoff). */
  bottom: var(--vv-bottom, 0px);
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.6rem;
  padding: 0.7rem var(--gutter);
  padding-bottom: max(0.7rem, env(safe-area-inset-bottom));
  /* Solid, not translucent (operator 2026-06-26): stops conversation text showing through the
     field; matches the now-opaque top bar. The separate 89px lift below the field is a position
     issue, tracked via the viewport measure, not show-through. */
  background: var(--bg);
  border-top: 1px solid var(--border);
}
/* App-shell (A1): the MAIN composer (#composer) is a normal flex item at the bottom of the body
   column, not a fixed overlay, so it sits flush at the visible bottom with nothing scrolling under
   it. The base .composer rules above still apply to the READER's own composer (.reader-composer),
   which stays fixed inside the reader overlay and is intentionally NOT changed here. */
#composer {
  position: static;
  bottom: auto;
  flex: none;
}
/* While typing, the home-indicator safe-area gap is redundant (the keyboard covers it). */
body.composing .composer { padding-bottom: 0.7rem; }

/* G6a: a RESTING conversation's bottom bar. No text field; the whole bar is the calm way to wake
   it back up. Rides the same static bottom tile as #composer (app-shell), so nothing scrolls under
   it. The .composer base gives it the border-top, padding and background; .wake-rest only fixes the
   positioning and stacks its own content. */
.wake-rest { position: static; bottom: auto; flex: none; }
/* the resting prompt as one wide, calm button: a quiet lead line, then the ember call to action */
.wake-rest-btn {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.2rem;
  width: 100%; text-align: left;
  background: var(--surface); border: 1px solid var(--ember); border-radius: var(--radius);
  padding: 0.85rem 1rem; font: inherit; color: var(--text); cursor: pointer;
}
@media (hover: hover) { .wake-rest-btn:hover { border-color: var(--ember-deep); } }
.wake-rest-lead { color: var(--muted); font-size: var(--step--1); }
.wake-rest-cta { color: var(--ember); font-weight: 700; font-size: var(--step-0); }
/* the confirm step: a short question, then a clear go / not-now pair (a stray tap never wakes it) */
.wake-rest-confirm { display: flex; flex-direction: column; gap: 0.6rem; }
.wake-rest-confirm[hidden] { display: none; }
.wake-rest-ask { margin: 0; color: var(--text); font-size: var(--step-0); text-align: center; }
.wake-rest-actions { display: flex; gap: 0.6rem; }
.wake-rest-go {
  flex: 1 1 auto;
  background: var(--grad); color: #1b1206; border: 0; border-radius: var(--radius);
  padding: 0.8rem; font: inherit; font-weight: 700; cursor: pointer;
}
.wake-rest-go:disabled { opacity: 0.6; cursor: default; }
.wake-rest-cancel {
  flex: 0 0 auto;
  background: transparent; color: var(--muted); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 0.8rem 1rem; font: inherit; cursor: pointer;
}
.wake-rest-msg { margin: 0.5rem 0 0; color: var(--muted); font-size: var(--step--1); text-align: center; }
.wake-rest-msg[hidden] { display: none; }

/* When the Lina app has taken this over: the composer is swapped for this calm notice + one clear
   button to bring it back. Rides the same static bottom tile as #composer (border-top, padding,
   background from .composer). A direct press reclaims it; no arming step (a tap means "carry on
   here"). */
.reclaim-app { position: static; bottom: auto; flex: none; }
.reclaim-app[hidden] { display: none; }
.reclaim-app-lead { margin: 0; color: var(--muted); font-size: var(--step--1); text-align: center; text-wrap: pretty; }
.reclaim-app-btn {
  width: 100%;
  background: var(--grad); color: #1b1206; border: 0; border-radius: var(--radius);
  padding: 0.85rem 1rem; font: inherit; font-weight: 700; cursor: pointer;
}
.reclaim-app-btn:disabled { opacity: 0.6; cursor: default; }
.reclaim-app-msg { margin: 0.5rem 0 0; color: var(--muted); font-size: var(--step--1); text-align: center; }
.reclaim-app-msg[hidden] { display: none; }
/* the actual input + send sit in a row; the held banner stacks above them */
.composer-row { display: flex; align-items: center; gap: 0.6rem; }
.composer textarea {
  flex: 1 1 auto;
  min-width: 0;
  resize: none;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* a touch more top/bottom room so the first line never clips against the frame while the field
     scrolls (operator 2026-06-25, iPhone Safari); scroll-padding keeps the top line clear too */
  /* Calmer, roomier field (operator 2026-07-01, step 3): a touch more padding and a taller resting
     height so writing feels unhurried, not cramped into a thin strip. */
  padding: 1rem 1.05rem;
  scroll-padding-top: 0.3rem;
  font: inherit;
  /* At least 16px so iOS Safari never auto-zooms the page on focus (step 3); --step-0 already floors
     at 1rem, the max() keeps the floor explicit even if the base size ever changes. */
  font-size: max(16px, var(--step-0));
  line-height: 1.5;
  min-height: 5.6rem;   /* about four lines: room to write and to see it */
  max-height: 30vh;     /* beyond that it scrolls inside, never eats the chat */
  overscroll-behavior: contain;   /* the field's own scroll must not leak to the page behind */
}
.composer textarea:disabled { opacity: 0.55; }

/* small round ember send, tucked in the corner, never a wide slab over the field */
.send {
  flex: none;
  width: 2.9rem;
  height: 2.9rem;
  border-radius: 50%;
  border: 0;
  background: var(--grad);
  color: #1b1206;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.send:disabled { opacity: 0.45; cursor: default; }

/* ---- "take it with you" (G6b counter-direction): a calm offer above the field ----------- */
/* Stacks in the composer column above the input (like the old held banner), so it rides with
   the bottom bar and never covers the chat permanently. Warm card with an ember left accent,
   the same language as the file sheet. */
.offer { display: flex; flex-direction: column; gap: 0.5rem; }
.offer[hidden] { display: none; }
.offer-lead { margin: 0; font-size: var(--step--1); color: var(--muted); }
.offer-items { display: flex; flex-direction: column; gap: 0.5rem; }
.offer-take {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  width: 100%;
  text-align: left;
  padding: 0.65rem 0.8rem;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--ember);
  border-radius: var(--radius-sm);
  color: var(--text);
  cursor: pointer;
  transition: border-color 0.15s ease;
}
@media (hover: hover) { .offer-take:hover { border-color: var(--ember-deep); } }
.offer-ico { flex: none; color: var(--ember); display: inline-flex; }
.offer-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.offer-main { font-size: var(--step-0); }
.offer-sub { font-size: var(--step--1); color: var(--muted); overflow-wrap: anywhere; }

/* ---- jump to the latest: small round button, only while scrolled up ---------------------- */
/* Sits above the composer, bottom-right (thumb zone). Muted at rest; warms to ember when a
   question is waiting so it doubles as "there's something to answer, tap to reach it". */
.jumpend {
  position: fixed;
  /* Sit close to the screen edge, not flush with the content column, so the round button no longer
     clings to the text edge and hides a letter while scrolling; the opaque fill + shadow keep it
     clearly floating above the text (operator 2026-06-25). */
  right: 0.5rem;
  bottom: calc(env(safe-area-inset-bottom) + 7.2rem);
  z-index: 5;
  /* Variant B (operator 2026-06-26): deliberately smaller than the 2.8rem send button so the two
     no longer read as a twin pair stacked at the edge. */
  width: 2.4rem;
  height: 2.4rem;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.jumpend[hidden] { display: none; }
@media (hover: hover) { .jumpend:hover { border-color: var(--ember); color: var(--ember); } }
.jumpend.has-question {
  background: var(--grad);
  color: #1b1206;
  border-color: transparent;
}

/* Turn-to-turn arrows in thumb reach (operator 2026-06-28, restored 2026-07-04): same round,
   secondary look as the jump-to-latest button, stacked just above it on the right. Each auto
   show/hides (the JS sets [hidden]) so it only appears when there's a boundary that way. The down
   arrow warms to ember when a question is waiting below, the same cue as the jump-to-latest button. */
.blocknav {
  position: fixed;
  right: 0.5rem;
  z-index: 5;
  width: 2.4rem;
  height: 2.4rem;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.blockdown { bottom: calc(env(safe-area-inset-bottom) + 10rem); }
.blockup { bottom: calc(env(safe-area-inset-bottom) + 12.8rem); }
.blocknav[hidden] { display: none; }
@media (hover: hover) { .blocknav:hover { border-color: var(--ember); color: var(--ember); } }
.blockdown.has-question { background: var(--grad); color: #1b1206; border-color: transparent; }

/* Desktop reading column (operator 2026-07-01, step 4): ONE breakpoint. The thread already centres
   at --reading-max; here the full-bleed top bar and composer keep their background but inset their
   CONTENT to the same column (symmetric padding = the side gutter), and the floating jump-to-latest
   button hugs the column's right edge instead of the far screen edge. So on a wide screen the whole
   conversation lines up in one calm column instead of the bar/composer sprawling edge to edge. */
@media (min-width: 48rem) {
  .bar,
  .composer {
    padding-inline: max(var(--gutter), calc((100% - var(--reading-max)) / 2));
  }
  .jumpend {
    right: max(var(--gutter), calc((100% - var(--reading-max)) / 2));
  }
}

/* Quote-a-selection pill (operator 2026-06-28): appears above the composer when text is selected in
   a bubble, so you can drop exactly that bit into a reply. Centred and not anchored to the selection,
   so it never collides with the phone's own copy/select callout. */
#quote-bar {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(env(safe-area-inset-bottom) + 5.4rem);
  z-index: 6;
}
#quote-bar[hidden] { display: none; }
.quote-bar-btn {
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--grad);
  color: #1b1206;
  font-size: var(--step--1);
  font-weight: 700;
  padding: 0.5rem 1.1rem;
  box-shadow: 0 3px 12px rgba(0, 0, 0, 0.5);
  cursor: pointer;
}

/* ---- file & secret handover (G6b): a calm round glyph + a little sheet ------------------ */
/* The glyph sits in the corner of the composer row, the same small footprint as send, so it
   never crowds the writing area. Muted at rest; warms on hover and when Lina is asking for
   something (the auto-open nudge). */
.attach {
  flex: none;
  width: 2.9rem;
  height: 2.9rem;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
@media (hover: hover) { .attach:hover { color: var(--ember); border-color: var(--ember-deep); } }
.attach.nudge { color: var(--ember); border-color: var(--ember); }

/* The sheet floats over the composer while open (z above it), a quiet card, never full screen. */
/* ---- menu icons, the Actions pill, and the menu sheets (skeleton, operator 2026-06-26) -------- */
/* a round bar icon button (help "?", new project), same footprint/family as the gear */
.bar-iconbtn {
  flex: none;
  /* iOS Safari gives a <button> native chrome that shoves its content off-centre (to the right);
     a LINK is spared (that is why .step-out, an <a>, always sat centred). -webkit-appearance:none,
     flex centering and absolute+margin:auto all FAILED on the operator's newer iOS. The fix that
     HOLDS: the svg FILLS the WHOLE button (width/height 100%, PADDING 0) so there is no spare room
     for the native nudge to move it into. CRUCIAL: button padding must stay 0 -- any padding shrinks
     the svg to the content-box and re-opens exactly the gap the nudge exploits (that regression cost
     a round, operator 2026-07-15). The icon's visual SIZE is set instead by its viewBox padding
     (_barico for the bar icons, a padded viewBox on the gear). Verified in a real WebKit render. */
  -webkit-appearance: none;
  appearance: none;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
@media (hover: hover) { .bar-iconbtn:hover { color: var(--ember); border-color: var(--ember-deep); } }
/* The svg FILLS its round button (see the .bar-iconbtn note): no spare room, so the iOS native-button
   nudge cannot move it off-centre. Centring is the viewBox's own (symmetric around 12,12), immune to
   the paint-time nudge in every engine incl. iOS. Size is set by the svg's padded viewBox, NOT button
   padding (which would re-open the nudge gap). */
.bar-iconbtn svg, .gear svg { display: block; width: 100%; height: 100%; }

/* the conversation's SECOND header line: name on the left, Actions pill on the right */
.header-row2 { flex-basis: 100%; min-width: 0; display: flex; align-items: center; gap: 0.5rem; margin-top: 0.25rem; }
/* flex-basis 0 (not auto): the name does NOT claim its full content width, so the Actions pill always
   fits at the right, even at 125% (operator 2026-06-26: the pill fell off the right edge). The name
   hard-ellipsizes; the full name is one tap away (it opens the conversation-info box). */
.header-row2 .convo-name { flex: 1 1 0; min-width: 0; margin-top: 0; }
/* same on the project page: the name shrinks/ellipsizes so the Manage pill fits at the right. */
.header-row2 .proj-name { flex: 1 1 0; min-width: 0; margin-top: 0; }

/* The aggregated "Lina needs you" banner (home + project overview, operator 2026-06-30): a full-width
   row that wraps onto its own line INSIDE the sticky bar, so it rides with the bar (always pinned at
   the top) and pushes the list cleanly down when present, takes zero space when hidden. The filled
   ember-deep + bell + cream text match the "needs you" status line, so the attention colour is the
   same wherever it appears. JS toggles the [hidden] attribute. */
.needbar {
  flex-basis: 100%;
  min-width: 0;
  margin-top: 0.4rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.85rem;
  border-radius: var(--radius-sm);
  background: var(--ember-deep);
  color: #fff6ec;
  font-weight: 700;
  font-size: var(--step-0);
  letter-spacing: -0.01em;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.needbar[hidden] { display: none; }
.needbar svg { display: block; flex: none; color: #fff6ec; }
.needbar .needbar-text {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.needbar .needbar-go { opacity: 0.9; }
@media (hover: hover) { .needbar:hover { filter: brightness(1.06); } }

/* the Actions pill (opens the workbench sheet). Quiet at rest, ember spark glyph. */
.action-pill {
  flex: none;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 2rem;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  cursor: pointer;
}
.action-pill svg { color: var(--ember); }
@media (hover: hover) { .action-pill:hover { color: var(--text); border-color: var(--ember-deep); } }

/* the menu sheets reuse .filesheet; these are the extras */
.menu-sheet[hidden] { display: none; }
.menu-group-label { margin: 0.8rem 0 0.4rem; font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--faint); font-weight: 700; }
.menu-note { color: var(--muted); font-size: var(--step--1); margin-bottom: 0.9rem; }
.menu-help-link { margin-top: 0.4rem; }

/* the workbench's big action tiles: a 2-column grid, few + large, NOT a list of small menu points
   (operator 2026-06-26) */
.menu-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
.menu-tile {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
  min-height: 5rem;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.8rem 0.85rem;
  color: var(--text);
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  line-height: 1.25;
  cursor: pointer;
  transition: border-color 0.15s ease;
}
.menu-tile svg { flex: none; color: var(--ember); }
@media (hover: hover) { .menu-tile:hover { border-color: var(--ember-deep); } }

/* the first-project banner (home empty state): a warm, tappable invitation so a newcomer isn't met
   with a bare page and understands what to do (operator 2026-06-26) */
.firstproject-banner {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  width: 100%;
  text-align: left;
  margin: clamp(1.2rem, 0.6rem + 2vw, 2rem) 0 0.6rem;
  padding: 1.1rem 1.2rem;
  background: linear-gradient(120deg, rgba(239, 162, 74, 0.14), rgba(224, 123, 60, 0.06));
  border: 1px solid var(--ember-deep);
  border-radius: var(--radius);
  color: var(--text);
  cursor: pointer;
  transition: border-color 0.15s ease;
}
@media (hover: hover) { .firstproject-banner:hover { border-color: var(--ember); } }
.firstproject-ico {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 50%;
  background: var(--grad);
  color: #1b1206;
}
.firstproject-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.firstproject-title { font-size: var(--step-1); font-weight: 800; letter-spacing: -0.01em; }
.firstproject-sub { font-size: var(--step--1); color: var(--muted); }

/* project-menu (Manage) rows: a label with a small explanation under it, like the settings rows, so
   a complex action (take off the web, delete) is understandable (operator 2026-06-26) */
.menu-act-col { flex-direction: column; align-items: flex-start; gap: 0.2rem; }
.menu-act-label { font-size: var(--step-0); font-weight: 600; }
.menu-act-sub { font-size: var(--step--1); color: var(--muted); line-height: 1.3; }
.menu-act-danger .menu-act-label { color: var(--ember-deep); }

.filesheet {
  position: fixed;
  left: 0;
  right: 0;
  /* ride above the keyboard like the composer, so a focused field (secret, rename) is never buried;
     0 when no keyboard is up (operator 2026-06-26). */
  bottom: var(--vv-bottom, 0px);
  z-index: 8;
  padding: 0 var(--gutter);
  padding-bottom: max(0.7rem, env(safe-area-inset-bottom));
}
.filesheet-card {
  max-width: 34rem;
  margin: 0 auto;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
  box-shadow: 0 -0.5rem 2.4rem rgba(0, 0, 0, 0.55);
  /* The sheet grows UPWARD from the bottom; with more actions (a rename row was added) it could
     overflow the top of a small screen and read as "cut off at the top, frame gone" (operator
     2026-06-26). Cap it to the visible height and let it scroll instead. */
  max-height: calc(100dvh - 1.5rem);
  overflow-y: auto;
}
.filesheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.9rem;
}
.filesheet-title {
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--text);
  text-wrap: balance;
}
.filesheet-x {
  flex: none;
  width: 2.2rem;
  height: 2.2rem;
  border-radius: 50%;
  border: 0;
  background: none;
  color: var(--muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
@media (hover: hover) { .filesheet-x:hover { color: var(--ember); } }
/* Each handover option is a generous, easy tap-row. */
.filesheet-act {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  width: 100%;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.85rem 0.95rem;
  margin-bottom: 0.6rem;
  color: var(--text);
  font: inherit;
  font-size: var(--step-0);
  cursor: pointer;
  transition: border-color 0.15s ease;
}
@media (hover: hover) { .filesheet-act:hover { border-color: var(--ember-deep); } }
/* Open-live-site can be an <a> now (filesheet-act as a link): no underline, inherit the row colour.
   When nothing is live to open yet, .menu-act-off is a calm, non-tappable row (dimmed, no hover lift)
   so it reads as "not available" rather than a dead button (operator HKDG 2026-06-27). */
a.filesheet-act { text-decoration: none; }
.menu-act-off { opacity: 0.55; cursor: default; }
@media (hover: hover) { .filesheet-act.menu-act-off:hover { border-color: var(--border); } }
.filesheet-ico { flex: none; color: var(--ember); display: inline-flex; }
.filesheet-label { min-width: 0; }
/* The secret's name and value, only once you choose to tuck one away. */
.filesheet-secret {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin: 0.2rem 0 0.6rem;
}
/* the explicit display above would otherwise beat the `hidden` attribute (UA display:none) */
.filesheet-secret[hidden] { display: none; }
.filesheet-secret input {
  width: 100%;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.7rem 0.9rem;
  font: inherit;
  font-size: var(--step-0);
}
.filesheet-secret input:focus { outline: none; border-color: var(--ember-deep); }
.filesheet-go {
  align-self: flex-end;
  border: 0;
  border-radius: var(--radius);
  background: var(--grad);
  color: #1b1206;
  font: inherit;
  font-size: var(--step-0);
  font-weight: 600;
  padding: 0.6rem 1.2rem;
  cursor: pointer;
}
.filesheet-msg {
  margin: 0.4rem 0 0;
  font-size: var(--step--1);
  color: var(--ember);
  text-wrap: pretty;
}
.filesheet-msg.err { color: var(--ember-deep); }
/* While a file is being set down, a small ring spins before the text so the wait reads as progress
   (operator 2026-06-30). Calms to a steady dot when motion is reduced. */
.filesheet-msg.busy::before {
  content: "";
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  margin-right: 0.45em;
  vertical-align: -0.1em;
  border: 2px solid color-mix(in srgb, var(--ember) 35%, transparent);
  border-top-color: var(--ember);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .filesheet-msg.busy::before { animation: none; border-top-color: color-mix(in srgb, var(--ember) 35%, transparent); }
}
/* the more-menu's pin on-state and end action (a calm, warm warning, not red). The icon is ember
   in BOTH states, so the on-state needs its own clear mark (operator 2026-06-26: pinning looked the
   same as not-pinned): an ember border + faint wash + ember bold label, unmistakable at a glance. */
#more-pin.is-on {
  border-color: var(--ember);
  background: rgba(224, 123, 60, 0.10);
}
#more-pin.is-on .filesheet-label { color: var(--ember); font-weight: 600; }
.filesheet-act-end .filesheet-ico { color: var(--ember-deep); }
/* The end-confirmation row: a clearly-marked warning button next to a calm way out, set well
   apart from the settings below so it never crowds them (operator 2026-06-25). */
.filesheet-end-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 0.6rem;
  margin: 0.7rem 0 1.1rem;
}
.filesheet-end-actions[hidden] { display: none; }
/* "No, keep it": the calm way back, a quiet outline button, so tapping End is never a dead end. */
.filesheet-go-ghost {
  border: 1px solid var(--border2);
  border-radius: var(--radius);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--step-0);
  font-weight: 600;
  padding: 0.6rem 1.1rem;
  cursor: pointer;
}
/* "Yes, end it": marked as a real warning, not the normal warm button (operator 2026-06-25, "not
   enough of a warning"): deep ember fill, light text for contrast, a small warning glyph. */
.filesheet-end-confirm {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: var(--ember-deep);
  color: #fff;
}
.filesheet-end-confirm:disabled { opacity: 0.6; cursor: default; }
/* the plain-language warning shown when "End" is armed: small, calm, explains the step */
.filesheet-endnote {
  margin: 0.5rem 0.2rem 0;
  font-size: var(--step--1);
  color: var(--muted);
  text-wrap: pretty;
}
/* View preferences that moved into the more-menu (status line, name). They reuse .setting/.switch;
   a top rule sets them apart from the actions above. */
/* The length switch (Quiet/Clean/Full) inside the "..." sheet (operator 2026-07-01): the same
   segmented control as before, but stretched to the full sheet width with three equal, comfortable
   tap targets, so it reads as a calm reading control at the top of the sheet, not a crammed bar pill. */
.filesheet-stages { margin-bottom: 0.55rem; }
.filesheet-stages .stages { display: flex; width: 100%; }
.filesheet-stages .stage-btn { flex: 1 1 0; min-width: 0; padding: 0.62rem 0.3rem; }
.filesheet-views { margin-top: 0.2rem; border-top: 1px solid var(--border); }
/* Bench space inside the more-menu (Block E): set apart by a top rule, the gauge reuses the .bench
   styles shared with the overview cards. */
.filesheet-bench { margin-top: 0.2rem; padding-top: 0.4rem; border-top: 1px solid var(--border); }
.filesheet-bench[hidden] { display: none; }
.filesheet-bench .bench { margin: 0.5rem 0 0.4rem; }

/* ---- reader (big, comfortable reading view) ------------------------------------------- */
.reader {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: var(--bg);
  display: flex;
  flex-direction: column;
}
.reader[hidden] { display: none; }
/* the pull-down handle at the very top; the touch area is generous, the visible bar small */
.reader-grab {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.55rem 0 0.35rem;
  padding-top: max(0.55rem, env(safe-area-inset-top));
  touch-action: none;
  cursor: grab;
}
.reader-grab span {
  width: 2.6rem;
  height: 0.28rem;
  border-radius: 999px;
  background: var(--border);
}
.reader-bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.85rem var(--gutter);
  padding-top: max(0.85rem, env(safe-area-inset-top));
  border-bottom: 1px solid var(--border);
}
/* Whose words (You / Lina): small and quiet, sitting in the bar so it costs no extra line. */
.reader-who {
  color: var(--muted);
  font-size: var(--step--1);
  font-weight: 600;
  margin-left: 0.15rem;
  flex: none;
}
.reader-who[hidden] { display: none; }
/* "Something new arrived in THIS conversation" while you're reading: its OWN calm line under the
   bar, full width, so it can never be squeezed between the bar's buttons and wrap letter by letter
   (operator 2026-06-25, IMG_0808). A tap jumps to the newest message. */
.reader-new {
  display: block;
  width: calc(100% - 2 * var(--gutter));
  margin: 0.6rem auto 0;
  padding: 0.5rem 0.9rem;
  text-align: center;
  text-wrap: balance;
  color: #1b1206;
  background: var(--grad);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  cursor: pointer;
}
.reader-new[hidden] { display: none; }
/* Block E: quick text size, pinned to the right of the bar. Three steps, set by app.js. */
.reader-size { margin-left: auto; display: inline-flex; gap: 0.4rem; flex: none; }
.reader-size-btn { min-width: 2.6rem; justify-content: center; font-weight: 700; padding: 0.4rem 0.7rem; }
.reader-body {
  flex: 1 1 auto;
  overflow-y: auto;
  width: 100%;
  max-width: 40rem;
  margin: 0 auto;
  padding: clamp(1.6rem, 1rem + 3vw, 2.6rem) var(--gutter)
           calc(env(safe-area-inset-bottom) + 3rem);
  /* Block E: the reader's own size (--reader-size), set per device and remembered; falls back to
     the fluid default. text-size-adjust pins it so iOS doesn't inflate it on its own. */
  font-size: var(--reader-size, var(--step-1));
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  line-height: 1.7;
}

/* The actions on the text shown big: identical symbols to a chat bubble's menu, plus
   "Reply point by point". A calm row above the reply field; wraps on a narrow screen. */
/* The action symbols live IN the reader bar now (operator 2026-06-25): a small cluster set a
   touch apart from the close button; the text-size group floats to the far right. */
.reader-acts {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-left: 0.5rem;
}
.reader-act {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.42rem 0.7rem;
  background: var(--surface2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--step--1);
  cursor: pointer;
}
.reader-act[hidden] { display: none; }
.reader-act svg { flex: none; color: var(--ember); }
@media (hover: hover) { .reader-act:hover { border-color: var(--ember-deep); } }
/* Short confirmations sit just under the text now that the symbols moved up into the bar. */
.reader-note { display: block; padding: 0.4rem var(--gutter) 0; color: var(--muted); font-size: var(--step--1); }
.reader-note:empty { display: none; }

/* the reply field inside the reading view flows at the bottom (not viewport-fixed) */
.reader-composer { position: static; }
/* While a field is focused (keyboard up) the reader is already shrunk to sit ABOVE the keyboard
   (sizeReaderToViewport in app.js), so the composer's bottom edge IS the keyboard's top edge. The
   base .composer keeps a home-indicator safe-area gap there (max(0.7rem, safe-area-inset-bottom)),
   which floated the reply field a send-button's height over the keyboard bar (operator 2026-06-27).
   body.composing only fires for the MAIN composer, not the reader's, so collapse the gap here while
   typing; the safe-area gap is only needed with no keyboard, when the reader fills to the screen
   bottom. iOS-only, confirm on device. */
.reader-composer:focus-within { padding-bottom: 0.5rem; }
/* Copy and Share in the reader carry their universal symbol only (no word), so all three actions
   fit one row even at 125% zoom; Write back keeps its word (operator 2026-06-25, measured). */
.reader-act-icon { padding: 0.42rem; }
/* "Quote her words": shows up at the reply field once you tap Write back. A small, quiet chip (not
   a slab); it pulls Lina's message in, one quote block per paragraph, at the cursor. */
.reader-quote {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.34rem 0.6rem;
  margin-bottom: 0.5rem;
  background: var(--surface2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--step--1);
  cursor: pointer;
}
.reader-quote[hidden] { display: none; }
.reader-quote svg { flex: none; color: var(--ember); }
@media (hover: hover) { .reader-quote:hover { border-color: var(--ember-deep); } }
/* Select all / Clear: a small, quiet row shown only while a draft is in the reply field. Sits on
   its OWN line below the field+send row, pushed to the right (operator 2026-06-26: the send now
   rides beside the field, so these drop below instead of sharing the send's row). */
.reader-tools {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.4rem;
  margin-top: 0.5rem;
}
.reader-tools[hidden] { display: none; }
.reader-tool {
  padding: 0.28rem 0.55rem;
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border2);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: var(--step--1);
  cursor: pointer;
}
@media (hover: hover) { .reader-tool:hover { color: var(--text); border-color: var(--border); } }
/* Keep a scroll gesture INSIDE the reader: the reading body must not chain its scroll to the
   conversation behind the full-screen reader, and while the reader is open the page itself is
   locked (iOS scroll-bleed, operator 2026-06-25). */
.reader-body { overscroll-behavior: contain; }
body.reader-open { overflow: hidden; }

/* ---- project page (project-first navigation) ------------------------------------------ */
.mark { min-width: 0; overflow-wrap: anywhere; }

/* start a fresh chat: one clear button, naming tucked behind a quiet "more" */
.starter.solo {
  flex-direction: column;
  align-items: stretch;
  gap: 0.6rem;
  /* clear breathing room before the first section ("Happening now") below (operator) */
  margin-bottom: clamp(1.6rem, 1rem + 2.5vw, 2.4rem);
}
.btn.block { width: 100%; }
.more-toggle {
  align-self: center;
  background: none;
  border: 0;
  color: var(--muted);
  font: inherit;
  font-size: var(--step--1);
  padding: 0.2rem 0.4rem;
  cursor: pointer;
}
.more-toggle:hover { color: var(--text); }

/* earlier conversations: folded away so they never flood the project */
.earlier { margin-top: clamp(2rem, 1rem + 3vw, 2.8rem); }
.earlier > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.78rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ember);
  font-weight: 700;
  padding: 0.4rem 0;
}
.earlier > summary::-webkit-details-marker { display: none; }
.earlier > summary::before {
  content: "\203A";
  color: var(--faint);
  font-size: 1.1em;
  transition: transform 0.2s;
}
.earlier[open] > summary::before { transform: rotate(90deg); }
.earlier .count {
  color: var(--faint);
  font-weight: 600;
  letter-spacing: normal;
  text-transform: none;
}
.earlier .cards { margin-top: 0.9rem; }

/* ---- A2: urgency marker on a collapsed project card ----------------------------------- */
/* Reuses the .dot vocabulary (colour by state) but a touch smaller, sitting at the front
   of the row. It only matters while the card is closed; once open the full status shows. */
.alert-dot { width: 0.5rem; height: 0.5rem; align-self: center; }
.pcard.open .alert-dot { display: none; }

/* ---- A3: conversations kept close at hand (slim front-page row) ------------------------ */
.frontrow { margin: 0; }
.frontrow-list {
  list-style: none;
  margin: 0.7rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.frow {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  padding: 0.55rem 0.7rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.frow-release {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.2rem;
  height: 2.2rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ember);
  cursor: pointer;
}
@media (hover: hover) {
  .frow-release:hover { color: var(--ember-deep); }
}
.frow-open {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  min-width: 0;
  flex: 1 1 auto;
  color: var(--text);
}
.frow-name {
  min-width: 0;
  font-weight: 600;
  font-size: var(--step-0);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- A3: compact bookmark toggle (project card + open conversation) -------------------- */
/* Icon-only, faint until the conversation is kept close at hand, then ember. On a card it
   sits at the RIGHT edge so it never crowds "Come on in" or invites a mis-tap (operator
   2026-06-23); in the open conversation it lives small in the top bar. */
.bm-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.4rem;
  height: 2.4rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--faint);
  cursor: pointer;
}
@media (hover: hover) {
  .bm-btn:hover { color: var(--muted); }
}
.bm-btn[aria-pressed="true"] { color: var(--ember); }
.pactions .bm-btn { margin-left: auto; }
.bookmark-ico { flex: none; }
/* With a visible label (the conversation card pin, operator 2026-06-27) the button grows into a calm
   pill, icon + words; without one (the home project pin) it stays the bare round icon. */
.bm-btn:has(.bm-label) {
  width: auto;
  height: auto;
  gap: 0.4rem;
  padding: 0.34rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 2rem;
}
.bm-btn:has(.bm-label)[aria-pressed="true"] { border-color: var(--ember); }
.bm-label { font-size: var(--step--1); white-space: nowrap; }
/* Set the conversation-info actions (pin / rename) apart from the live details above them. */
.convoinfo-acts { margin-top: 1rem; padding-top: 0.9rem; border-top: 1px solid var(--border); }

/* ---- "something not right" note: the gear action row + the report form sheet ------------- */
/* The gear's action row reads like a settings row but is a button (opens the note form). Set off
   from the toggles above by a quiet top rule so it never looks like another switch. */
.settings-act {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  border-top: 1px solid var(--border);
  margin-top: 0.6rem;
  padding: 1rem 0 0.2rem;
  cursor: pointer;
  color: var(--text);
  font: inherit;
}
.settings-act-text { display: flex; flex-direction: column; gap: 0.25rem; flex: 1; min-width: 0; }
.settings-act-label { font-size: var(--step-0); font-weight: 600; }
.settings-act-sub { font-size: var(--step--1); color: var(--muted); text-wrap: pretty; }
/* The envelope sits in a box the SAME width as the switches above (3rem), centred, so its middle
   lines up vertically with the switch knobs' middle instead of sitting hard against the edge
   (operator 2026-06-27). */
.settings-act-ico { flex: none; width: 3rem; color: var(--ember); display: inline-flex; align-items: center; justify-content: center; }
@media (hover: hover) {
  .settings-act:hover .settings-act-label { color: var(--ember); }
}

.report-form { display: flex; flex-direction: column; }
/* the explicit display above would otherwise beat the `hidden` attribute (UA display:none) */
.report-form[hidden], .report-done[hidden] { display: none; }
.report-label {
  font-size: var(--step--1);
  color: var(--faint);
  margin: 0.8rem 0 0.35rem;
}
.report-select,
.report-text {
  width: 100%;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.65rem 0.85rem;
  font: inherit;
  font-size: var(--step-0);
}
.report-text {
  min-height: 5.5rem;
  resize: vertical;
  line-height: 1.5;
}
.report-select:focus,
.report-text:focus {
  outline: none;
  border-color: var(--ember-deep);
  box-shadow: 0 0 0 2px rgba(224, 123, 60, 0.18);
}
.report-photo-row {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 1rem 0 0.2rem;
}
.report-photo-label { font-size: var(--step--1); color: var(--faint); }
.report-photo-row input[type="file"] {
  font: inherit;
  font-size: var(--step--1);
  color: var(--muted);
}
.report-form .filesheet-go { align-self: flex-end; margin-top: 1.1rem; }
.report-done { display: flex; flex-direction: column; gap: 1rem; }
.report-done-text {
  color: var(--ember);
  text-wrap: pretty;
  margin: 0.5rem 0 0;
  line-height: 1.5;
}
.report-done .filesheet-go { align-self: flex-end; }

/* New Project V1: the guided wizard (np.html / np.js). A calm side-trip, not a conversation: one
   Lina message at a time, then either tap-choices or a place to type back. Workshop look, generous
   spacing, fluid type (clamp), unbroken at 100/125% (D-058, CONCEPT.md Nordstern). */
.bar-back {
  flex: none;
  min-width: 44px; min-height: 44px;   /* 44px tap target (operator 2026-06-29), was ~38px */
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--muted); text-decoration: none;
}
@media (hover: hover) { .bar-back:hover { color: var(--ember); } }
.np-wrap { max-width: 38rem; margin: 0 auto; padding: 1.4rem var(--gutter) 3rem; }
.np-stage { display: flex; flex-direction: column; gap: 1.1rem; }
/* Lina's message: a warm card with the workshop ember edge on the left (like her side of a chat). */
.np-step {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--ember);
  border-radius: var(--radius);
  padding: 1rem 1.15rem;
}
.np-lina { margin: 0; color: var(--text); text-wrap: pretty; line-height: 1.6; }
.np-lina + .np-lina { margin-top: 0.7rem; }
.np-lina.err { color: var(--ember-deep); margin-top: 0.8rem; }
/* The context Lina gave just before a question: shown quieter, above the question, so you read
   why before you decide (the long lead-in must never go missing, operator 2026-06-27). */
.np-lead { color: var(--muted); font-size: var(--step--1); margin: 0 0 0.6rem; text-wrap: pretty; }
.np-lead + .np-lead { margin-top: 0.4rem; }
/* Tap-choices: generous full-width rows, easy thumb targets. */
.np-choices { display: flex; flex-direction: column; gap: 0.6rem; }
.np-choice {
  width: 100%; text-align: left;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.85rem 1rem;
  color: var(--text); font: inherit; font-size: var(--step-0);
  cursor: pointer; transition: border-color 0.15s ease;
}
@media (hover: hover) { .np-choice:hover { border-color: var(--ember-deep); } }
.np-choice:disabled { opacity: 0.5; cursor: default; }
/* Type an answer back. The box grows with the text (np.js); the send button sits warm beside it. */
.np-reply { display: flex; gap: 0.6rem; align-items: flex-end; }
.np-input {
  flex: 1 1 auto; min-width: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.7rem 0.85rem;
  color: var(--text); font: inherit; font-size: var(--step-0);
  resize: none; line-height: 1.5;
}
.np-input:focus { outline: none; border-color: var(--ember); }
.np-send {
  flex: none;
  border: 0; border-radius: var(--radius);
  background: var(--grad); color: #1b1206;
  font: inherit; font-size: var(--step-0); font-weight: 600;
  padding: 0.7rem 1.2rem; cursor: pointer;
}
.np-send:disabled { opacity: 0.6; cursor: default; }
.np-open { align-self: flex-start; }
.np-home {
  align-self: flex-start;
  color: var(--ember); text-decoration: none; font-weight: 600;
}
@media (hover: hover) { .np-home:hover { text-decoration: underline; } }
/* "I'm on it" while Claude works: a calm ember pulse, never a busy spinner-y feel. */
.np-working { display: flex; align-items: center; gap: 0.8rem; }
.np-working .np-lina { margin: 0; color: var(--muted); }
.np-spinner {
  flex: none; width: 0.8rem; height: 0.8rem; border-radius: 50%;
  background: var(--ember);
  animation: np-pulse 1.4s ease-in-out infinite;
}
@keyframes np-pulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .np-spinner { animation: none; opacity: 0.8; } }

/* "A question is on its way" while the open-turn preamble is still being reconstructed: the same
   calm ember pulse, inline before the line, so the wait reads as Lina getting ready, not an error. */
.openturn-pending .msg-text { color: var(--muted); }
.preamble-dot {
  display: inline-block; width: 0.6rem; height: 0.6rem; border-radius: 50%;
  background: var(--ember); margin-right: 0.5rem; vertical-align: middle;
  animation: np-pulse 1.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .preamble-dot { animation: none; opacity: 0.8; } }

/* ---- pictures (SendUserFile from Lina, or screenshots you sent): thumbnails + lightbox --------- */
.msg-media { margin: 0.6rem 0 0; }
/* A compact, wrapping strip of thumbnails: each a fixed-height frame, width from its own aspect
   ratio, so a row reads as previews you tap to enlarge (not a wall of full-width images). */
.media-strip { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.media-tile {
  /* Width-driven frame with a max-height cap (operator 2026-07-01): the tile takes a fixed WIDTH and
     follows the picture's real aspect-ratio, but never grows taller than --thumb-maxh. A tall phone
     screenshot (ratio ~0.46) used to be sized by a fixed HEIGHT, which collapsed its width to a thin
     sliver; now it renders as a proper portrait thumbnail (cropped to the cap), while a wide picture
     stays short. */
  --thumb-w: clamp(96px, 26vw, 120px);
  --thumb-maxh: clamp(128px, 34vw, 160px);
  position: relative; flex: 0 0 auto; align-self: flex-start;
  display: block; padding: 0; overflow: hidden;
  width: var(--thumb-w); height: auto; max-width: 100%;
  max-height: var(--thumb-maxh);
  aspect-ratio: 4 / 3;                                             /* replaced inline by the real ratio */
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); cursor: zoom-in;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease;
}
/* A lone picture may sit a touch larger, still a small preview, never full-bleed. */
.media-strip.single .media-tile { --thumb-w: clamp(150px, 42vw, 196px); --thumb-maxh: clamp(190px, 50vw, 250px); max-width: 72%; }
@media (hover: hover) {
  .media-tile:hover { border-color: var(--ember); box-shadow: 0 0 0 1px var(--ember-deep), 0 6px 18px #0007; }
}
.media-tile:active { transform: scale(0.98); }
@media (prefers-reduced-motion: reduce) { .media-tile:active { transform: none; } }
.media-tile:focus-visible { outline: 2px solid var(--ember); outline-offset: 2px; }
/* Fill the frame; anchor to the TOP so a portrait screenshot cropped by the height cap shows its
   most telling part (the top of the screen), not a middle band (operator 2026-07-01). */
.media-tile img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center top; position: relative; z-index: 1; }
/* A small zoom hint in the corner, so a thumbnail clearly invites a tap. */
.media-tile::after {
  content: ""; position: absolute; right: 0.4rem; bottom: 0.4rem; z-index: 2;
  width: 1.4rem; height: 1.4rem; border-radius: 0.4rem;
  background: var(--chip) no-repeat center / 0.9rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23EFA24A' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M21 3l-7 7'/%3E%3Cpath d='M3 21l7-7'/%3E%3C/svg%3E");
  opacity: 0.9; pointer-events: none;
}
/* Calm placeholder shimmer while loading, in the ember/workbench key (no spinner, no tech look). */
.media-tile::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(100deg, var(--surface2) 30%, var(--border) 50%, var(--surface2) 70%);
  background-size: 200% 100%; animation: media-shimmer 1.6s ease-in-out infinite;
}
@keyframes media-shimmer { 0% { background-position: 150% 0; } 100% { background-position: -150% 0; } }
@media (prefers-reduced-motion: reduce) { .media-tile::before { animation: none; } }
/* If a picture can't be brought up, the tile shows a quiet line in Lina's voice, no broken icon. */
.media-broken-note {
  display: none; position: absolute; inset: 0; z-index: 3; padding: 0.8rem;
  align-items: center; justify-content: center; text-align: center;
  color: var(--muted); font-size: var(--step--1); background: var(--surface2);
}
.media-tile.media-broken { cursor: default; aspect-ratio: 3 / 2; }
.media-tile.media-broken::before, .media-tile.media-broken::after { display: none; }
.media-tile.media-broken img { display: none; }
.media-tile.media-broken .media-broken-note { display: flex; }
.media-cap {
  margin-top: 0.55rem; color: var(--muted); font-size: var(--step--1); line-height: 1.5;
}
/* A picture you sent: its caption right-aligns to sit under the You bubble's own alignment. */
.msg-media.user .media-strip { justify-content: flex-end; }
.msg-media.user .media-cap { text-align: right; }
/* display:attach -> a calm file card to keep, twin of the "take it with you" offer, no big image. */
.filecard {
  display: flex; align-items: center; gap: 0.7rem; margin-top: 0.55rem;
  padding: 0.7rem 0.9rem; text-decoration: none; color: var(--text);
  background: var(--surface2); border: 1px solid var(--border);
  border-left: 3px solid var(--ember); border-radius: var(--radius-sm);
}
.filecard-ico { color: var(--ember); display: flex; }
.filecard-body { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.filecard-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filecard-sub { color: var(--faint); font-size: var(--step--1); }
.filecard-take { color: var(--ember); font-size: var(--step--1); white-space: nowrap; }

/* A non-image document Lina sent (operator 2026-07-02): a card that reads, opens and keeps. It
   stacks (head, optional readable preview, actions) instead of the one-line save card above. */
.filecard.doc { flex-direction: column; align-items: stretch; gap: 0.6rem; }
.filecard.doc .filecard-head { display: flex; align-items: center; gap: 0.7rem; }
.doc-preview {
  margin: 0; max-height: 11rem; overflow: hidden; position: relative;
  padding: 0.6rem 0.7rem; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--muted); font-size: var(--step--1);
  line-height: 1.5; white-space: pre-wrap; word-break: break-word;
}
/* A soft fade at the foot of the clipped preview, so a longer document reads as "goes on" instead
   of a hard mid-line cut (operator 2026-07-02); "Open it" underneath opens the whole thing. */
.doc-preview::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2.2rem;
  background: linear-gradient(transparent, var(--surface)); pointer-events: none;
}
.doc-actions { display: flex; flex-wrap: wrap; gap: 0.5rem 1.1rem; align-items: center; }
.doc-open, .doc-take {
  background: none; border: none; padding: 0; cursor: pointer; font: inherit;
  color: var(--ember); font-size: var(--step--1); white-space: nowrap; text-decoration: none;
}
@media (hover: hover) { .doc-open:hover, .doc-take:hover { text-decoration: underline; } }
/* In-app reader for a text/markdown document (operator 2026-07-02): a full-screen scrollable
   overlay so a document reads inside the app, instead of a navigation that strands the PWA. */
/* ONE in-app viewer for every file Lina sends that isn't an inline image/video (operator
   2026-07-02, rebuild): text/markdown read here, a PDF/other shows an honest line + Save, so the
   experience is identical across formats and never leaves the standalone PWA. Same scrim/glass feel
   as the image lightbox. The [hidden] rule is REQUIRED: this block sets display:flex, which would
   otherwise beat the UA `[hidden]{display:none}` and the X would never close it (the earlier bug). */
.doc-reader { position: fixed; inset: 0; z-index: 22; display: flex; flex-direction: column; background: var(--bg); padding: 0; }
.doc-reader[hidden] { display: none; }
.doc-reader-card { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.doc-reader-head {
  flex: none; display: flex; align-items: center; gap: 0.7rem;
  padding: calc(0.7rem + env(safe-area-inset-top, 0px)) 1rem 0.7rem;
  border-bottom: 1px solid var(--border); background: var(--surface);
}
.doc-reader-title {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text); font-size: var(--step-0);
}
.doc-reader-take { background: none; border: none; color: var(--ember); cursor: pointer; font: inherit; font-size: var(--step--1); white-space: nowrap; padding: 0.2rem 0.2rem; }
.doc-reader-x { background: none; border: none; color: var(--muted); cursor: pointer; padding: 0.2rem; display: flex; }
.doc-reader-body {
  flex: 1; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; margin: 0;
  padding: 1rem 1rem calc(1.4rem + env(safe-area-inset-bottom, 0px));
  color: var(--text); font-size: var(--step-0); line-height: 1.6;
}
/* Rendered text/markdown reads in a calm centred column (reuses the thread's .rt markdown styling). */
.doc-reader-body .rt { max-width: 34rem; margin: 0 auto; }
/* A viewer body that isn't text (video, or the honest "can't show it here" note) centres its content. */
.doc-reader-body.is-media { display: flex; align-items: center; justify-content: center; padding: 1rem; }
.doc-reader-video { max-width: 100%; max-height: 100%; border-radius: var(--radius-sm); background: #000; }
.doc-reader-note { max-width: 24rem; text-align: center; color: var(--muted); line-height: 1.6; }
/* A clip Lina sent, played in place. A sensible cap so a portrait video never fills the thread. */
.msg-video {
  margin-top: 0.55rem; max-width: 100%; max-height: 60vh; border-radius: var(--radius-sm);
  background: #000; display: block;
}
.msg-media.user .msg-video { margin-left: auto; }

/* The "no connection" strip (operator 2026-07-02): a calm bar at the very bottom, shown only while a
   poll/send can't reach the server. It rides IN the body flex column as its own tile (flex:none, JS
   appends it last, after #composer), so it never covers the composer, it just takes its own height
   and #view shrinks to fit. Muted, not alarming; clears itself the moment a poll gets through. */
.conn-banner {
  flex: none;
  padding: 0.55rem 1rem calc(0.55rem + env(safe-area-inset-bottom, 0px));
  background: var(--surface2); color: var(--muted);
  border-top: 1px solid var(--border); font-size: var(--step--1);
  text-align: center; line-height: 1.4;
}

/* Auto-handoff overlay (operator 2026-07-02): a calm full-screen cover while the conversation is
   packed up and a fresh one is opened. No progress-bar tech, just a quiet pulse and one line in
   Lina's voice; on a handled stop it shows the line plus a Close. */
.handoff-overlay[hidden] { display: none; }
.handoff-overlay {
  position: fixed; inset: 0; z-index: 40;
  display: flex; align-items: center; justify-content: center; padding: 1.5rem;
  background: var(--scrim);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
.handoff-card {
  max-width: 22rem; text-align: center; display: flex; flex-direction: column;
  align-items: center; gap: 1.1rem;
}
.handoff-line { margin: 0; color: var(--text); font-size: var(--step-0); line-height: 1.5; }
.handoff-pulse {
  width: 2.2rem; height: 2.2rem; border-radius: 50%;
  background: var(--ember); animation: handoff-pulse 1.6s ease-in-out infinite;
}
@keyframes handoff-pulse {
  0%, 100% { opacity: 0.35; transform: scale(0.85); }
  50% { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .handoff-pulse { animation: none; opacity: 0.7; } }
.handoff-ok {
  padding: 0.6rem 1.4rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface2); color: var(--ember); font-size: var(--step--1);
}

body.lightbox-open { overflow: hidden; }
.lightbox {
  position: fixed; inset: 0; z-index: 21;                          /* one above the reader */
  background: var(--scrim); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  will-change: transform;
  animation: lightbox-in 0.16s ease-out;
}
@keyframes lightbox-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .lightbox { animation: none; } }
.lightbox-scroll {
  position: absolute; inset: 0; overflow: auto;
  display: flex; align-items: center; justify-content: center;
  padding: calc(env(safe-area-inset-top, 0px) + 3.6rem) 0.8rem
           calc(env(safe-area-inset-bottom, 0px) + 5rem);          /* clear the close + action bar */
  touch-action: pinch-zoom;                                        /* native pinch + pan */
  -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
}
.lightbox-img {
  max-width: 100%; max-height: 100%; object-fit: contain; display: block;
  border-radius: 0.4rem; box-shadow: 0 10px 40px #000a;
}
.lightbox-close {
  position: absolute; top: calc(env(safe-area-inset-top, 0px) + 0.7rem); right: 0.9rem; z-index: 3;
  width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  padding: 0; border: none; border-radius: 50%;
  background: var(--warm-glass); color: var(--text); cursor: pointer;
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
.lightbox-close:focus-visible { outline: 2px solid var(--ember); outline-offset: 2px; }
/* The action bar: a floating row of Share / Save / Reply, translucent, ember icons, above the
   home indicator. Each action is a tall tap target (icon over a small label). */
.lightbox-bar {
  position: absolute; left: 50%; transform: translateX(-50%); z-index: 3;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 0.9rem);
  display: flex; gap: 0.4rem; padding: 0.4rem;
  background: var(--warm-glass); border: 1px solid var(--border); border-radius: 1.1rem;
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  box-shadow: 0 8px 28px #000a;
}
.lightbox-act {
  display: flex; flex-direction: column; align-items: center; gap: 0.15rem;
  min-width: 68px; padding: 0.5rem 0.6rem; border: none; border-radius: 0.8rem;
  background: transparent; color: var(--text); cursor: pointer;
  font-size: 0.72rem; line-height: 1; -webkit-tap-highlight-color: transparent;
  transition: background 0.12s ease;
}
.lightbox-act:focus-visible { outline: 2px solid var(--ember); outline-offset: 2px; }
.lightbox-act svg { color: var(--ember); }
.lightbox-act:active { background: #ffffff14; }
@media (hover: hover) { .lightbox-act:hover { background: #ffffff10; } }
