/* ═══════════════════════════════════════════════════════════════════════════
   bazaar — the square
   One design system, from the poster to the room.

   Read app/DESIGN.md first. The short version:
     · Paper and ink. Hard edges, square corners, hard offset shadows.
     · Two inks carry meaning. Ultramarine is a person. Vermilion is an agent.
       Red is an error. Nothing else is colored.
     · Color never works alone. Every state has a shape, a weight, or a word.
     · Exactly one thing in this interface is allowed to show a number:
       attention. Everything else is ambient and stays countless.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1. tokens ─────────────────────────────────────────────────────────────
   Light is the default. Dark is a redefinition of the same names — no rule
   below this block may name a literal color. */

:root {
  color-scheme: light;

  /* paper: the three surfaces, lightest to deepest */
  --paper:        #E9E6DD;   /* the page */
  --paper-raise:  #F1EEE6;   /* cards, panels, the composer well */
  --paper-sink:   #DEDACD;   /* the rail, bands, inset wells */

  /* ink: text, three weights */
  --ink:          #1A1714;   /* body and headings — 10:1 on paper */
  --ink-2:        #5C554B;   /* secondary: meta, hints, timestamps — 5.7:1 */
  --ink-3:        #7A7263;   /* disabled controls ONLY — 3.4:1, exempt under 1.4.3.
                                Never a label, never a name, never body text. */

  /* rules: structural borders are ink. Soft rules are decorative only and
     are always redundant with spacing — never the sole separator. */
  --rule:         var(--ink);
  --rule-soft:    #B9B3A7;

  /* the two inks. -ink suffix is the text-safe weight (>=4.5:1 on all three
     papers); the base weight is for fills, marks, and large display only. */
  --human:        #5A5FE0;
  /* Ultramarine means a person, or a person's business. Anything that is
     merely a control gets ink. The colour that says "this concerns you" was
     painting a dozen pieces of resting furniture, so it had fired a dozen
     times before anything concerned you — which is the desensitisation the
     whole attention architecture exists to prevent. */
  --human-ink:    #4348C8;
  --human-wash:   rgba(67, 72, 200, 0.10);
  --agent:        #E7420D;
  --agent-ink:    #A93007;
  --agent-wash:   rgba(169, 48, 7, 0.10);

  /* action: the primary button. Darkened vermilion so white type clears 5.9:1 */
  --action:       #B93408;
  --action-ink:   #FFFFFF;

  /* error, and nothing else */
  --danger:       #A81E27;
  --danger-wash:  rgba(168, 30, 39, 0.10);

  /* attention is not a fourth color. Something addressed to you is a person's
     business, so it wears the person ink. */
  --attention:      var(--human-ink);
  --attention-wash: var(--human-wash);

  --scrim:        rgba(26, 23, 20, 0.55);
  --veil:         rgba(233, 230, 221, 0.92);
  --knockout:     var(--paper-raise);   /* type reversed out of a filled mark */
  --media-credit-bg:  rgba(0, 0, 0, 0.72);
  --media-credit-ink: #FFFFFF;

  /* one deliberately absurd easter-egg palette, named here so its temporary
     scene still follows the same single-source color rule as the room */
  --blackrobes-pink:              #FF7EE2;
  --blackrobes-gold:              #FFE66D;
  --blackrobes-mint:              #71F79F;
  --blackrobes-cyan:              #70E7FF;
  --blackrobes-lime:              #7DFF72;
  --blackrobes-yellow:            #DCFF75;
  --blackrobes-white:             #FFFFFF;
  --blackrobes-command:           #287A3D;
  --blackrobes-command-shadow:    rgba(72, 179, 90, 0.32);
  --blackrobes-blade:             #3F9F50;
  --blackrobes-forest:            #123719;
  --blackrobes-marquee-forest:    rgba(10, 67, 28, 0.48);
  --blackrobes-marquee-lime:      rgba(125, 255, 114, 0.48);
  --blackrobes-entry-ink:         #F4E9FF;
  --blackrobes-text-secondary:    rgba(255, 255, 255, 0.86);
  --blackrobes-text-tertiary:     rgba(255, 255, 255, 0.70);
  --blackrobes-rule:              rgba(255, 255, 255, 0.65);
  --blackrobes-rule-soft:         rgba(255, 255, 255, 0.38);
  --blackrobes-surface:           rgba(0, 0, 0, 0.16);
  --blackrobes-surface-raise:     rgba(0, 0, 0, 0.48);
  --blackrobes-surface-sink:      rgba(0, 0, 0, 0.34);
  --blackrobes-caption-backdrop:  rgba(0, 0, 0, 0.82);
  --blackrobes-marquee-ink:       rgba(255, 255, 255, 0.144);
  --blackrobes-marquee-stroke:    rgba(125, 255, 114, 0.432);
  --blackrobes-sun:               rgba(178, 111, 255, 0.46);
  --blackrobes-sky:               #26183F;
  --blackrobes-field:             #140D23;
  --blackrobes-field-deep:        #07050C;
  --blackrobes-entry-omen:        rgba(244, 233, 255, 0.72);
  --blackrobes-button:            #35204D;
  --blackrobes-button-hover:      #482966;
  --blackrobes-drop-shadow:       rgba(0, 0, 0, 0.52);

  --grass-deep: #46783D; /* Historical results and sidebar marker; no game load. */

  /* type */
  --display: "Bricolage Grotesque", "Arial Black", "Helvetica Neue", Arial, sans-serif;
  --body:    "Archivo", "Helvetica Neue", Arial, sans-serif;
  --mono:    "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;

  --t-xs:   0.6875rem;  /* 11px — marks, counts */
  --t-sm:   0.8125rem;  /* 13px — meta, labels, buttons */
  --t-md:   0.9375rem;  /* 15px — body. The room reads at this size. */
  --t-lg:   1.0625rem;  /* 17px — panel leads */
  --t-xl:   1.375rem;   /* 22px — panel titles */
  --t-2xl:  1.75rem;    /* 28px — drawer + gate headings */
  --t-3xl:  2.25rem;    /* 36px — wordmark */

  --lh-tight: 1.2;
  --lh-body:  1.65;
  /* Every vertical gap in the transcript is a fraction of one line of leading,
     so the column has a rhythm instead of four unrelated numbers. */
  --lead: calc(var(--t-md) * var(--lh-body));

  /* space — 4px base, used everywhere. No arbitrary paddings below. */
  --s1: 0.25rem;  --s2: 0.5rem;   --s3: 0.75rem;  --s4: 1rem;
  --s5: 1.5rem;   --s6: 2rem;     --s7: 3rem;

  /* structure — three border weights, two shadows, one radius rule */
  --bd:        2px;   /* every card, control, and field */
  --bd-strong: 3px;   /* structural division: shell edges, panel spines */
  --bd-hair:   1px;   /* decorative only */
  --radius:    0;     /* the square corner is the house style */
  --radius-pill: 999px; /* count chips and reaction chips only */
  /* Three depths, because a nested object must never cast the same offset as
     the object containing it. The decision card, the Approve button inside it
     and the hover strip all cast 4px, which flattened three nesting levels
     into one. Small for controls, regular for cards, large for overlays. */
  --shadow-sm: 2px 2px 0 var(--ink);
  --shadow:    4px 4px 0 var(--ink);
  --shadow-lg: 8px 8px 0 var(--ink);

  --focus: 3px solid var(--human);
  --focus-offset: 2px;

  --rail-w: 15rem;
  /* 68 real characters. `ch` is the width of Archivo's zero (8.59px) against a
     7.73px average lowercase advance, so 68ch sets a 76-character line. */
  --read-w: 33em;
  --ts-col: 3.25rem;  /* `09:02` at 11px Plex Mono measures 33px */
  /* Where the content column ends: gutter + gap + measure + gap + time. */
  --line-w: calc(1.75rem + var(--s3) + var(--read-w) + var(--s3) + var(--ts-col));
  /* The rail is a table of contents, and a table of contents with five hanging
     indents is not a designed object. Every name in it starts here. */
  --rail-glyph: 1.375rem;
  /* A rail row reserves 3px on its left for the active marker, whether or not
     it is drawing one. Rows without that border have to pay the same, or their
     text sits 3px left of everything else. */
  --rail-pad: calc(var(--s3) + var(--bd-strong));
  --rail-spine: calc(var(--rail-pad) + var(--rail-glyph) + var(--s2));
}

/* The dark palette lives in TWO places on purpose.

   The tokens used to exist only inside `@media (prefers-color-scheme: dark)`,
   so the in-app theme toggle — which sets data-theme on the root — did nothing
   at all unless it happened to agree with the operating system. Choosing Dark
   on a light Mac changed a button's pressed state and nothing else.

   `:not([data-theme="light"])` lets an explicit Light choice beat a dark OS,
   and the second block lets an explicit Dark choice win on a light one. This
   is the same pattern the landing page already uses. test-chat-theme.mjs
   asserts the two blocks declare an identical token set, so they cannot drift.

   --rule was #6E6656: 3.21:1 on --paper, 2.96:1 on --paper-raise. It is the
   border of every card, mark, button, field, shell division and the composer
   well, so at that contrast --bd (2px) and --bd-strong (3px) are
   indistinguishable and dark mode is the same layout drawn in fog. Light mode
   ships the same token at 14.30:1. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --paper:        #171512;
    --paper-raise:  #201D18;
    --paper-sink:   #29251F;
    --ink:          #EDE9DF;
    --ink-2:        #A89F90;
    --ink-3:        #857D6E;
    --rule:         #C2BAA4;
    --rule-soft:    #3D382F;
    --human:        #8A8FFF;
    --human-ink:    #A0A4FF;
    --human-wash:   rgba(160, 164, 255, 0.14);
    --agent:        #FF6A3C;
    --agent-ink:    #FF7E52;
    --agent-wash:   rgba(255, 126, 82, 0.14);
    --action:       #FF6A3C;
    --action-ink:   #14110E;
    --danger:       #FF8790;
    --danger-wash:  rgba(255, 135, 144, 0.14);
    --scrim:        rgba(0, 0, 0, 0.66);
    --veil:         rgba(23, 21, 18, 0.92);
    --knockout:     #14110E;
    --media-credit-bg:  rgba(0, 0, 0, 0.72);
    --media-credit-ink: #FFFFFF;
    /* The shadow was `var(--rule)`, which is LIGHTER than the #171512 page —
       a highlight, not a shadow, and in the same colour as every card's
       border, so it read as a doubled edge. Literals are permitted inside the
       token block, and this is what they are for. */
    --shadow-sm:    2px 2px 0 #0B0A08;
    --shadow:       4px 4px 0 #0B0A08;
    --shadow-lg:    8px 8px 0 #0B0A08;
  }
}
:root[data-theme="dark"] {
    color-scheme: dark;
    --paper:        #171512;
    --paper-raise:  #201D18;
    --paper-sink:   #29251F;
    --ink:          #EDE9DF;
    --ink-2:        #A89F90;
    --ink-3:        #857D6E;
    --rule:         #C2BAA4;
    --rule-soft:    #3D382F;
    --human:        #8A8FFF;
    --human-ink:    #A0A4FF;
    --human-wash:   rgba(160, 164, 255, 0.14);
    --agent:        #FF6A3C;
    --agent-ink:    #FF7E52;
    --agent-wash:   rgba(255, 126, 82, 0.14);
    --action:       #FF6A3C;
    --action-ink:   #14110E;
    --danger:       #FF8790;
    --danger-wash:  rgba(255, 135, 144, 0.14);
    --scrim:        rgba(0, 0, 0, 0.66);
    --veil:         rgba(23, 21, 18, 0.92);
    --knockout:     #14110E;
    --media-credit-bg:  rgba(0, 0, 0, 0.72);
    --media-credit-ink: #FFFFFF;
    /* The shadow was `var(--rule)`, which is LIGHTER than the #171512 page —
       a highlight, not a shadow, and in the same colour as every card's
       border, so it read as a doubled edge. Literals are permitted inside the
       token block, and this is what they are for. */
    --shadow-sm:    2px 2px 0 #0B0A08;
    --shadow:       4px 4px 0 #0B0A08;
    --shadow-lg:    8px 8px 0 #0B0A08;
}

/* ── 2. base ──────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
html, body { touch-action: manipulation; }  /* no double-tap zoom; pinch is untouched */

body {
  background: var(--paper);
  color: var(--ink);
  font: 400 var(--t-md)/var(--lh-body) var(--body);
  overscroll-behavior: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: var(--human-ink); text-underline-offset: 2px; }
a:hover { text-decoration-style: dotted; }

:focus-visible { outline: var(--focus); outline-offset: var(--focus-offset); }

.dim   { color: var(--ink-2); }
.hidden { display: none !important; }
.noscript { padding: var(--s6); color: var(--ink-2); }
.vh {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.brand-dot { color: var(--agent); }

/* ── 3. primitives ────────────────────────────────────────────────────────── */

/* button — the default is a bordered secondary. Modifiers below. */
button {
  font: 600 var(--t-sm)/1.4 var(--mono);
  color: var(--ink);
  background: transparent;
  border: var(--bd) solid var(--rule);
  border-radius: var(--radius);
  padding: 0.45em 0.9em;
  cursor: pointer;
}
button:hover { background: var(--paper-sink); }

/* THE PRESS GESTURE, written once.

   Four controls put `transform: translate(...)` on :HOVER, so merely pointing
   at them moved them — the Approve button on a decision card grew under the
   pointer, the send button shifted, the Needs you row jumped. That is not a
   press, it is the interface flinching, and it is half of what reads as
   jumpiness when the mouse moves across the room.

   A raised control sinks into its own offset: the travel equals its shadow, so
   the face lands exactly where the shadow was. .sound-chip has said this
   correctly since it shipped; now everything does. */
.btn-primary:active,
.gate-step button[type="submit"]:active,
.decision-acts .esc-approve:active,
.sound-chip:active,
.composer-send:active {
  transform: translate(2px, 2px);
  box-shadow: none;
}
/* A flat control has no shadow to fall into, so it darkens instead of moving.
   A 1px lurch on a shadowless button is jitter, not a press. */
button:active { background: var(--paper-sink); }
button:disabled {
  color: var(--ink-3);
  background: var(--paper-sink);
  border-color: var(--rule-soft);
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
}

/* primary — vermilion, ink border, hard shadow. One per surface. */
.btn-primary,
.gate-step button[type="submit"] {
  display: inline-block;
  font: 600 var(--t-sm)/1.4 var(--mono);
  color: var(--action-ink);
  background: var(--action);
  border: var(--bd) solid var(--rule);
  border-radius: var(--radius);
  padding: 0.45em 0.9em;
  cursor: pointer;
  text-decoration: none;
  box-shadow: var(--shadow-sm);
}
.btn-primary:hover,
.gate-step button[type="submit"]:hover { background: var(--action); }
.btn-primary:disabled { box-shadow: none; transform: none; }

/* quiet — an icon or text control with no chrome until it is touched.
   It keeps a transparent border so hover and focus never shift layout. */
.btn-quiet {
  border-color: transparent;
  background: transparent;
  color: var(--ink-2);
  padding: 0.3em 0.5em;
}
.btn-quiet:hover { color: var(--ink); background: var(--paper-sink); }

/* link-button — reads as text, behaves as a control */
.btn-link {
  border: none; background: none; padding: 0;
  color: var(--human-ink);
  font: inherit; font-size: var(--t-sm);
  text-align: left; cursor: pointer;
}
.btn-link:hover { background: none; text-decoration: underline; }

.btn-danger { color: var(--danger); border-color: var(--danger); }
.btn-danger:hover { color: var(--action-ink); background: var(--danger); }

/* fields */
input[type="text"], input[type="password"], input[type="time"],
textarea, select {
  font: 400 1em/1.5 var(--body);
  color: var(--ink);
  background: var(--paper-raise);
  border: var(--bd) solid var(--rule);
  border-radius: var(--radius);
  padding: 0.5em 0.7em;
  caret-color: var(--ink);
  width: 100%;
}
input::placeholder, textarea::placeholder { color: var(--ink-2); opacity: 1; }
textarea { resize: vertical; }
input[type="file"] { font: 400 var(--t-sm) var(--body); color: var(--ink-2); max-width: 100%; }
input[type="checkbox"] { width: auto; accent-color: var(--human-ink); }

.field { display: flex; flex-direction: column; gap: var(--s1); }
.field > label { font-weight: 600; font-size: var(--t-sm); }

/* labels — the only uppercase in the system, and only for navigation groups */
.eyebrow {
  font: 600 var(--t-xs)/1.4 var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/* card */
.card {
  background: var(--paper-raise);
  border: var(--bd) solid var(--rule);
  border-radius: var(--radius);
}
.card-lift { box-shadow: var(--shadow); }

/* status lines */
.dimline { color: var(--ink-2); font-size: var(--t-sm); }
.errline {
  color: var(--danger);
  background: var(--danger-wash);
  border-left: var(--bd-strong) solid var(--danger);
  padding: 0.5em 0.7em;
  font-weight: 600;
  font-size: var(--t-sm);
}
.errline::before { content: "! "; font-family: var(--mono); }
.errline[hidden], .dimline[hidden] { display: none; }
.hint { color: var(--ink-2); font-size: var(--t-sm); line-height: 1.5; }

/* count chip — attention only. Nothing else in this interface counts. */
.count {
  flex: none;
  min-width: 1.5em;
  padding: 0 0.4em;
  border-radius: var(--radius-pill);
  background: var(--attention);
  color: var(--knockout);
  font: 600 var(--t-xs)/1.6 var(--mono);
  text-align: center;
}

/* ── 4. the mark ───────────────────────────────────────────────────────────
   Identity is the product, so identity gets a shape. A person is a circle.
   An agent is a square with an antenna — the same two figures that stand in
   the market square on the landing page.

   The mark carries four facts at once, none of them by color alone:
     kind        circle (person) vs square-with-antenna (agent)
     who         one or two letters, knocked out of the fill
     reachable   filled (a message lands now) vs outlined (it waits)
     principal   an agent standing on a person's ink carries a foot rule

   The fill uses the -ink weights, not the display weights: the knocked-out
   letter is small text, and --agent on paper measures 3.49:1. */

.mark {
  --mark-size: 1.75rem;
  --mark-ink: var(--human-ink);
  position: relative;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--mark-size);
  height: var(--mark-size);
  border: var(--bd) solid var(--rule);
  background: var(--mark-ink);
  color: var(--knockout);
  font: 700 calc(var(--mark-size) * 0.42)/1 var(--display);
  text-transform: lowercase;
  user-select: none;
}
.mark-human { --mark-ink: var(--human-ink); border-radius: 50%; }
/* A constant radius, not a percentage. At 15% the same figure has a 3px corner
   at mark-sm, 3.3px at default and 6.6px at mark-lg — three different corners
   on one mark. A printed figure keeps its corner at every size. */
.mark-agent { --mark-ink: var(--agent-ink); border-radius: 3px; }

/* the antenna — an agent's tell at any size, and it survives monochrome */
.mark-agent::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: var(--bd);
  /* The ::after below centres itself with a translate and this did not, so the
     stem's centre sat at 9.5px against the bulb's 8.5px — a 1px kink on a 2px
     stem, on every agent mark in the product. */
  transform: translateX(-50%);
  /* And 0.3em left 0.66px of paper between the stem's top and the bulb's
     bottom. In a hard-edged print system a join overlaps; it never abuts. */
  height: 0.42em;
  margin-bottom: -1px;
  background: var(--rule);
}
.mark-agent::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: calc(100% + 0.26em);
  width: 0.32em;
  height: 0.32em;
  border-radius: 50%;
  background: var(--mark-ink);
  border: 1.5px solid var(--rule);
  transform: translateX(-50%);
}

/* out of reach: the ink empties out. Nothing you say lands right now. */
.mark-away {
  background: var(--paper-raise);
  color: var(--ink-2);
  border-style: dashed;
}
.mark-away.mark-agent::after { background: var(--paper-raise); }

/* an agent's foot rule: the person it acts for */
.mark-principal { box-shadow: 0 calc(var(--bd) + 1px) 0 -1px var(--human-ink); }

/* live — this agent holds an open inbox stream */
.mark-live { outline: var(--bd) solid var(--agent-wash); outline-offset: 1px; }

/* 22px, and one initial. At 20px the roster's marks were setting two letters
   at 8.4px, which is not a monogram, it is texture. */
.mark-sm { --mark-size: 1.375rem; border-width: 1.5px; }

/* ── 4a. drawn chrome ──────────────────────────────────────────────────────
   One stroke weight, one box, one colour source. The icons take currentColor
   so they inherit whatever ink the control they sit in already has — which is
   why a muted button, a hovered button and a disabled one all stay correct
   without a single icon-specific colour. */
.ico {
  width: 1.125rem; height: 1.125rem; display: block;
  fill: currentColor; stroke: currentColor; stroke-width: 2;
  stroke-linecap: square; stroke-linejoin: miter;
}
.ico-sm { width: 0.75rem; height: 0.75rem; display: inline-block; vertical-align: -1px; stroke-width: 2.5; }
.composer-send .ico { width: 0.875rem; height: 0.875rem; }
.rail-add { display: inline-flex; align-items: center; gap: 0.3em; }
.mark-lg { --mark-size: 2.75rem; }

/* a name and its mark travel together */
.who { display: inline-flex; align-items: center; gap: var(--s2); min-width: 0; }
.who-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── 5. shell ─────────────────────────────────────────────────────────────── */

.screen[hidden], #gate[hidden], #chat[hidden] { display: none; }

#chat {
  --chat-rail-w: var(--rail-w);
  position: relative;
  display: grid;
  grid-template:
    "top  top"    auto
    "rail main"   1fr
    "rail prompt" auto
    / var(--chat-rail-w) minmax(0, 1fr);
  height: 100vh;
  height: 100dvh;
}
#topicbar, #messages, #conversations-view, #promptbar { min-width: 0; }

#topicbar {
  grid-area: top;
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-bottom: var(--bd-strong) solid var(--rule);
  background: var(--paper-raise);
}
#topic-text { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: var(--s2); }
#topic-ws { font: 600 var(--t-xs) var(--mono); text-transform: uppercase; letter-spacing: 0.08em; }
#topic-chan {
  font: 700 var(--t-lg)/1.2 var(--body);
  color: var(--ink);
  flex: none;
  max-width: 45%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.topic-sep { color: var(--rule-soft); flex: none; }
#topic-topic {
  color: var(--ink-2);
  font-size: var(--t-sm);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#topic-tools { display: inline-flex; align-items: center; gap: var(--s1); flex: none; }
/* The strip is drawn now, so nothing in it is sized as a glyph — except `?`,
   which is a real mono character and already correct. */
#topicbar button { display: inline-flex; align-items: center; justify-content: center; }
#btn-help { font: 600 var(--t-md) var(--mono); }
#topic-tools button.muted-on { color: var(--agent-ink); background: var(--agent-wash); }
#btn-pins.has-pins { color: var(--ink); }

/* An unsent draft is stated as a mark, not a tally — the rail's one number
   belongs to attention. */
.row-draft {
  flex: none; margin-left: auto; padding-left: var(--s2);
  font-family: var(--mono); font-size: var(--t-xs); color: var(--ink-3);
}
.rail-row.has-draft .row-name { font-style: italic; }

/* ── 6. rail ──────────────────────────────────────────────────────────────── */

#rail {
  grid-area: rail;
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  border-right: var(--bd-strong) solid var(--rule);
  padding: var(--s3) 0 var(--s5);
  overflow-y: auto;
  background: var(--paper-sink);
  scrollbar-width: thin;
}
.rail-head {
  padding: 0 var(--s3) var(--s2);
  padding-left: var(--rail-spine);
  font: 800 var(--t-xl)/1 var(--display);
  letter-spacing: -0.02em;
}
.rail-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  padding: var(--s4) var(--s3) var(--s1);
  padding-left: var(--rail-spine);
  font: 600 var(--t-xs)/1.4 var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
  /* Stacked bands, like the landing page's shared-border stall grid, rather
     than labels floating over a list. */
  border-bottom: var(--bd-hair) solid var(--rule-soft);
  margin-bottom: var(--s1);
}
.rail-title-button {
  width: 100%;
  background: none;
  text-align: left;
  cursor: pointer;
}
.rail-title-button:hover { background: var(--paper-raise); color: var(--ink); }
.rail-title-link { font-size: var(--t-xs); letter-spacing: 0.04em; text-transform: none; }
.rail-state {
  margin: 0;
  padding: var(--s2) var(--s3) var(--s2) var(--rail-spine);
  color: var(--ink-2);
  font-size: var(--t-sm);
}
.conversation-rail-row {
  min-height: 0;
  padding-top: var(--s2);
  padding-bottom: var(--s2);
}
.conversation-rail-copy { min-width: 0; flex: 1; }
.conversation-rail-meta {
  display: block;
  color: var(--ink-2);
  font: 600 var(--t-xs)/1.35 var(--mono);
}
.conversation-rail-channel { color: var(--ink); font-weight: 700; }
.conversation-rail-author { font-weight: 500; }
.conversation-rail-excerpt {
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  color: var(--ink-2);
  font-style: italic;
  font-weight: 500;
  font-size: var(--t-sm);
  line-height: 1.35;
}
.rail-add {
  border: none; background: none; padding: 0 var(--s1);
  color: var(--ink-2); font: 600 var(--t-xs) var(--mono);
  letter-spacing: 0.04em; cursor: pointer;
}
.rail-add:hover { color: var(--ink); background: none; text-decoration: underline; }

/* every rail row shares one shape */
.rail-row {
  display: flex;
  align-items: center;
  gap: var(--s2);
  width: 100%;
  text-align: left;
  border: none;
  border-left: var(--bd-strong) solid transparent;
  border-radius: 0;
  /* An em-based pad made the workspace rows 3px shorter than the channel rows
     purely because their name drops to 13px — an accident, not a step. */
  padding: 0 var(--s3);
  min-height: 2rem;
  color: var(--ink);
  font: 500 var(--t-md)/1.4 var(--body);
  text-decoration: none;
}
.rail-row.conversation-rail-row { align-items: flex-start; }
.conversation-rail-row .conversation-rail-sigil {
  align-self: flex-start;
  line-height: 1.35;
}
.rail-row:hover { background: var(--paper-raise); }
/* The peer's mark stands in the sigil's own box, so the spine holds. */
.row-marks {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: var(--rail-glyph);
}
.rail-row .row-name {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.blackrobes-treatment {
  display: inline-flex;
  align-items: baseline;
  font-family: var(--display);
  font-weight: 800;
  color: var(--blackrobes-pink);
  text-shadow: 0 0 0.12em var(--blackrobes-white), 0 0 0.45em currentColor, 0 0 0.8em var(--blackrobes-cyan);
}
.rail-row .blackrobes-channel {
  display: flex;
  overflow: hidden;
}
.blackrobes-letter {
  display: inline-block;
  flex: 0 0 0.62em;
  line-height: 1;
  text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
  .blackrobes-channel .blackrobes-letter,
  .blackrobes-workspace .blackrobes-letter {
    animation: blackrobes-letter-ripple 1.7s ease-in-out infinite;
    animation-delay: calc(var(--blackrobes-letter) * 0.09s - 1.7s);
  }
}
@keyframes blackrobes-letter-ripple {
  0%, 45%, 100% { font-size: 1em; color: var(--blackrobes-pink); }
  15% { font-size: 1.32em; color: var(--blackrobes-gold); }
  30% { font-size: 1.08em; color: var(--blackrobes-cyan); }
}
.blackrobes-command-mark {
  display: inline-block;
  position: relative;
  padding: 0.08em 0.2em 0.18em;
  color: var(--blackrobes-command);
  font: 900 1.08em/1.15 var(--display);
  letter-spacing: 0.035em;
  text-shadow: 0 0.04em var(--blackrobes-white), 0 0.12em var(--blackrobes-command-shadow);
}
.blackrobes-command-mark::after {
  content: "";
  position: absolute;
  right: 0.15em;
  bottom: 0;
  left: 0.15em;
  height: 0.28em;
  background:
    linear-gradient(68deg, transparent 43%, var(--blackrobes-blade) 44% 56%, transparent 57%) 0 100% / 0.38em 100% repeat-x;
  opacity: 0.9;
}
.rail-row.active {
  background: var(--paper-raise);
  border-left-color: var(--ink);
  font-weight: 700;
}
/* ambient: something new here. A weight change and a quiet dot — never a
   number. Counting ambient traffic is how a rail starts shouting. */
.rail-row.unread .row-name { font-weight: 700; }
/* On the row rather than trailing the name, so the dots form a column at the
   right edge instead of landing at a different x on every row. Its position is
   the thing being fixed here — never its nature. A dot, not a number. */
.rail-row.unread::after {
  content: "";
  flex: none;
  margin-left: auto;
  width: 0.34em; height: 0.34em;
  border-radius: 50%;
  background: var(--ink-2);
}
.rail-row.attention .row-name { font-weight: 700; }
.rail-row.muted { color: var(--ink-2); }
.rail-row.muted .row-name { font-weight: 400; }
.rail-row.muted .row-name::after, .rail-row.muted .count { display: none; }
.row-sigil {
  flex: none; width: var(--rail-glyph); text-align: center;
  color: var(--rule-soft); font: 600 var(--t-sm) var(--mono);
}
/* The name already carries unread through weight and a dot; the sigil is a
   disambiguator, not a second shout. */
.rail-row.active .row-sigil { color: var(--ink); }

/* needs you — the one destination that carries a number */
/* A full-bleed band bounded by two ink rules, so it is structurally distinct
   from the lists without needing a second colour — and so it stops being the
   fifth hanging indent in the column. */
#needs-row {
  /* Workspace lists must scroll instead of compressing this header. */
  flex-shrink: 0;
  margin: var(--s2) 0;
  width: 100%;
  padding: var(--s3) var(--s3) var(--s3) var(--rail-spine);
  border-inline: none;
  border-block: var(--bd) solid var(--rule);
  background: var(--paper-raise);
  font-weight: 600;
}
#needs-row.has-attention {
  background: var(--attention-wash);
  box-shadow: var(--shadow);
}
#needs-row.has-attention .row-name { font-weight: 700; }
#needs-row[aria-expanded="true"] { border-left-color: var(--ink); }
#needs-row.has-saved:not(.has-attention) .row-quiet { color: var(--ink); font-weight: 600; }
#needs-row:hover { background: var(--paper); }
#needs-row.has-attention:hover { background: var(--attention-wash); }
#needs-row .row-quiet { color: var(--ink-2); font-weight: 400; font-size: var(--t-sm); }

/* the roster answers one question: can I reach them right now? */
#members { list-style: none; display: flex; flex-direction: column; }
#members li {
  display: flex; align-items: center; gap: var(--s2);
  padding: 0 var(--s3) 0 var(--rail-pad);
  min-height: 2rem;
  min-width: 0;
}
#members .m-name {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--t-sm);
}
#members li.is-away .m-name { color: var(--ink-2); }
.m-reach {
  flex: none;
  font: 500 var(--t-xs)/1.4 var(--mono);
  color: var(--ink-2);
  letter-spacing: 0.02em;
}
.m-reach.reach-live { color: var(--agent-ink); }
/* It explains the list, so it is read BEFORE the list rather than under it —
   and at 11px, at the bottom of a rail that is closed by default on a phone,
   it was explaining the marks to nobody. */
.roster-key {
  padding: 0 var(--s3) var(--s2);
  padding-left: var(--rail-spine);
  color: var(--ink-2);
  font: 400 var(--t-sm)/1.5 var(--body);
}

.ws-link .row-name { font-size: var(--t-sm); }
#ws-title[hidden], #ws-list[hidden], #topic-ws[hidden] { display: none; }
#ws-list { display: flex; flex-direction: column; }

#rail-backdrop[hidden], #drawer-backdrop[hidden] { display: none; }
#topicbar #btn-rail { display: none; position: relative; }
/* The one permitted number lives on #needs-row. This is its shape, not its
   count — enough to say "look in here", never enough to be a score. */
#btn-rail.has-attention::after {
  content: ""; position: absolute; top: 0.15rem; right: 0.15rem;
  width: 0.5rem; height: 0.5rem; border-radius: var(--radius-pill);
  background: var(--attention); border: 1px solid var(--rule);
}

#chan-create, #dm-picker { padding: var(--s1) var(--s3) var(--s2); display: flex; flex-direction: column; gap: var(--s2); }
#chan-create[hidden], #dm-picker[hidden] { display: none; }
#dm-picker .dm-pick {
  display: flex; align-items: center; gap: var(--s2);
  width: 100%; text-align: left; border: none; padding: 0.2em 0;
  font: 400 var(--t-sm) var(--body); color: var(--ink);
}
#dm-picker .dm-pick:hover { background: var(--paper-raise); }
#dm-picker .dm-start { align-self: flex-start; }
.dm-hint { font-size: var(--t-xs); color: var(--ink-2); }

/* ── 7. the transcript ─────────────────────────────────────────────────────
   Two columns: a gutter that holds the speaker's mark, and everything they
   said. Consecutive messages from one member keep the gutter empty, so a
   run of messages reads as one turn without repeating the identity. */

#messages {
  grid-area: main;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--s4) var(--s5) var(--s5);
  scrollbar-width: thin;
  scrollbar-color: var(--rule-soft) transparent;
}

#conversations-view {
  grid-area: main;
  overflow-y: auto;
  padding: var(--s5);
  scrollbar-width: thin;
}
#conversations-view[hidden], #messages[hidden], #promptbar[hidden] { display: none; }
.conversations-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--s4);
  max-width: 48rem;
  margin: 0 auto var(--s5);
  padding-bottom: var(--s4);
  border-bottom: var(--bd-strong) solid var(--rule);
}
.conversations-head h1 { margin: 0; font: 800 var(--t-2xl)/1.1 var(--display); }
.conversations-head p { margin: var(--s1) 0 0; color: var(--ink-2); }
#conversation-history-list { max-width: 48rem; margin: 0 auto; }
.conversation-card {
  width: 100%;
  display: block;
  text-align: left;
  margin: 0 0 var(--s4);
  padding: var(--s4);
  border: var(--bd) solid var(--rule);
  background: var(--paper-raise);
  color: var(--ink);
  box-shadow: var(--shadow);
}
.conversation-card:hover, .conversation-card:focus-visible { transform: translate(-2px, -2px); }
.conversation-card.unread { border-left-width: 0.5rem; }
.conversation-card-head, .conversation-card-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: baseline;
  color: var(--ink-2);
  font: 600 var(--t-xs)/1.4 var(--mono);
}
.conversation-card-root { margin: var(--s2) 0; font-weight: 700; }
.conversation-card-summary { margin: 0 0 var(--s2); color: var(--ink-2); }
#conversations-more { display: block; margin: var(--s5) auto; }
#conversations-more[hidden] { display: none; }

.line {
  position: relative;
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 1fr);
  gap: 0 var(--s3);
  margin-top: var(--lead);
  word-wrap: break-word;
  overflow-wrap: anywhere;
  /* Selected, thread-active and mentioned can all be true at once, and three
     separate `box-shadow` declarations cannot compose — the last rule in the
     sheet wins the whole property and the others simply vanish. That is how
     .thread-active came to erase the selection edge, and how selecting a
     message that names you erased the mention wash. One list, three slots.
     Edges first: the first shadow in the list paints topmost. */
  --thread-edge: 0 0 transparent;
  --sel-edge:    0 0 transparent;
  --sel-fill:    0 0 transparent;
  box-shadow: var(--thread-edge), var(--sel-edge), var(--sel-fill);
}
/* A quarter of a line: visibly a second utterance, still visibly the same
   turn. The old 1.5px read as one wrapped paragraph. */
.line.cont { margin-top: calc(var(--lead) * 0.25); }
.line > .mark { grid-row: span 2; }
.line.cont > .mark { visibility: hidden; height: 0; }

/* The time hangs in a fixed right column. It used to trail the handle inside a
   flex row, so its x moved with the length of the name — measured at 84, 88,
   69 and 223 down four consecutive turns. A ragged column of times is the
   loudest single thing separating this from a printed transcript. */
.msghead {
  grid-column: 2;
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  flex-wrap: nowrap;
  max-width: calc(var(--read-w) + var(--s3) + var(--ts-col));
}
.msghead .ts {
  flex: none;
  width: var(--ts-col);
  margin-left: auto;
  text-align: right;
  font-variant-numeric: tabular-nums;
  order: 2;
}
/* On a pending line the word `sending` takes the auto margin instead, so the
   row reads `mara ——— sending 14:22` and the time stays on its column. */
.line.is-pending .msghead .ts { margin-left: var(--s2); }
/* A long on-behalf-of line truncates rather than pushing the time off the
   column and onto a second row. */
.msghead .nick { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msghead .nick { font-weight: 700; color: var(--ink); }
.msghead .nick.is-agent { color: var(--agent-ink); }
.msghead .nick.is-human { color: var(--human-ink); }
.msghead .obo {
  color: var(--ink-2);
  font-weight: 400;
  font-size: var(--t-sm);
}
.line.cont .msghead { display: none; }

.ts { color: var(--ink-2); font: 400 var(--t-xs) var(--mono); cursor: default; }

.msgbody { grid-column: 2; max-width: var(--read-w); min-width: 0; }
.msgbody .body { display: block; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
.msgbody .body a { color: var(--human-ink); }

/* a mention of you: a left rule, a wash, and a heavier name. Three cues. */
.line .mention { color: var(--human-ink); font-weight: 600; }
.line .mention.is-agent { color: var(--agent-ink); }
.line .mention-me {
  color: var(--human-ink);
  font-weight: 700;
  background: var(--human-wash);
  padding: 0 0.15em;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 2px;
}
/* The negative margin has to pay for the border too, or the one line that must
   read as emphasised is the one line whose left edge is wrong — measured, its
   mark sat 3px right of every other mark. And the wash stops with the
   paragraph rather than painting a 1000px band around a 584px column. */
.line.mentioned {
  --inset: var(--s3);
  border-left: var(--bd-strong) solid var(--human-ink);
  background: var(--human-wash);
  padding: var(--s2) var(--inset);
  /* Both margins are derived from the inset, so a narrower inset at the phone
     breakpoint cannot leave the compensation behind and knock the line's mark
     and time off the columns everything else sits on. */
  margin-left: calc((var(--inset) + var(--bd-strong)) * -1);
  margin-right: calc(var(--inset) * -1);
  /* Wide enough that the head inside it still gets its measure PLUS the time
     column, or the one emphasised line is the one whose time breaks the
     column — which is the defect this whole section is about. */
  max-width: calc(var(--read-w) + var(--ts-col) + 1.75rem + var(--s3) * 2
                  + var(--inset) * 2 + var(--bd-strong));
}

.line.action .body { font-style: italic; color: var(--ink-2); }

/* system lines sit in the gutter-free full width, quiet and rule-marked */
.line.sysline {
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
  gap: var(--s3);
  margin-top: var(--s4);
  color: var(--ink-2);
  font-size: var(--t-sm);
}
.line.sysline .sysline-body { display: flex; align-items: center; gap: var(--s2); }
.line.sysline .sysline-body::before {
  content: "";
  flex: none;
  width: 1.25rem;
  height: 0;
  border-top: var(--bd) solid var(--rule-soft);
}
.line.sysline.is-err { color: var(--danger); }
.line.sysline.is-err .sysline-body::before { border-color: var(--danger); }
.grass-result .body { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--grass-deep); font-weight: 600; }
.line.grass-result .sysline-body::before { border-color: var(--grass-deep); }
.grass-result .ts { flex: none; }

.line.tombstone { color: var(--ink-2); font-style: italic; }

/* ── in flight ─────────────────────────────────────────────────────────────
   A message is drawn the moment it is typed, so it has to say plainly that the
   server has not taken it yet. Dimming the whole line would make it look like a
   system message; this dims only the timestamp's place and holds the body at
   full strength, because the words are real — it is the delivery that is not
   settled. No spinner: a spinner in a transcript is an animation that never
   stops being noticed. */
.line.is-pending .msgbody { opacity: 0.72; }
.line.is-pending .mark { border-style: dashed; }
.line.is-pending .msghead::after {
  content: "sending";
  font: 500 var(--t-xs) var(--mono);
  color: var(--ink-2);
  letter-spacing: 0.04em;
  /* Pushed left of the time, which keeps its column: `mara  sending  14:22` */
  margin-left: auto;
  order: 1;
}
/* A pending message that groups under the previous turn has no head to put
   `sending` in — its head AND its mark are both hidden. Without this the only
   remaining cue is reduced opacity, which is not a cue. */
.line.is-pending.cont .msgbody::after {
  content: "sending";
  margin-left: var(--s2);
  font: 500 var(--t-xs) var(--mono);
  color: var(--ink-2);
  letter-spacing: 0.04em;
}

.line.is-failed { border-left: var(--bd-strong) solid var(--danger); padding-left: var(--s3); margin-left: calc((var(--s3) + var(--bd-strong)) * -1); }

.line.is-failed .msgbody { opacity: 1; }
.send-failed {
  grid-column: 2;
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  margin-top: var(--s1);
  font-size: var(--t-sm);
}
.send-failed-why { color: var(--danger); font-weight: 600; }

/* a date belongs on the transcript once, not on every line */
/* Two lines of air above, half a line below: a head belongs to what follows
   it. The old 8px below collapsed under the next line's own margin, so the
   rule had more air beneath it than above — the relationship inverted. */
.day-rule {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin: calc(var(--lead) * 2) 0 calc(var(--lead) * 0.5);
  color: var(--ink-2);
  font: 600 var(--t-xs) var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.day-rule::before, .day-rule::after {
  content: "";
  flex: 1;
  border-top: var(--bd-hair) solid var(--rule-soft);
}
.day-rule + .line, .line.new-divider + .line { margin-top: 0; }

/* the line you stopped reading at */
.line.new-divider {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin: calc(var(--lead) * 2) 0 calc(var(--lead) * 0.5);
  color: var(--human-ink);
  font: 600 var(--t-xs) var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.line.new-divider::before, .line.new-divider::after {
  content: "";
  flex: 1;
  border-top: var(--bd) solid var(--human-ink);
}

/* Per-message actions.

   The hidden strip stays out of flow but in the keyboard order. Visible action
   strips never float over the transcript: click selection or keyboard focus
   puts the same node in flow below the message and its reactions. Earlier
   wide-screen offsets only moved the overlap from body copy into the timestamp
   lane, and a long reaction row could still run underneath them. */
.acts {
  position: absolute;
  top: 0;
  left: 0;
  margin-left: 0;
  /* On an unconstrained transcript, never wrap. max-width:100% here resolves against .chips, whose width is
     max-content — so it squeezed the strip to 38px and stacked all six buttons
     into a 189px vertical column. The strip is one row, always. */
  flex-wrap: nowrap;
  white-space: nowrap;
  z-index: 5;
  display: inline-flex;
  gap: 0.1em;
  opacity: 0;
  pointer-events: none;
  background: var(--paper-raise);
  border: var(--bd) solid var(--rule);
  box-shadow: var(--shadow-sm);
  padding: 0.15em;
}
/* Selection reaches the strip as a descendant of the line. Keyboard reveal
   belongs to the strip itself, so focusing another control in the message does
   not unexpectedly reflow its actions. */
.line.selected .acts,
.acts:focus-within { opacity: 1; pointer-events: auto; }
/* No transition on the reveal. A running CSS transition outranks every
   declaration in the cascade, so in any context that is not compositing
   frames it strands at the start value and pins the strip invisible — proof
   against an !important override. 80ms of polish is not worth a failure mode
   that can hide the primary affordance, and it also cannot be tested. */
/* The strip is ordered so that nothing which broadcasts to the room sits next
   to Reply: Reply | add | pin | the three acks | edit | delete. The acks were
   given a class to mark them as a group and never given the rule that shows
   it, so the grouping existed only in the source. A hairline before the first
   one and after the last is enough — rule 3's "a shape, a weight, a border".

   `:first-of-type` is the wrong tool for it — it means the first BUTTON among
   the siblings, which is Reply, so the group opened with no rule at all; and
   `~` marked every later button rather than the one that closes it. Adjacent
   siblings say exactly the intended thing: the ack that follows a non-ack, and
   the non-ack that follows an ack. */
.acts :not(.act-ack) + .act-ack,
.acts .act-ack + :not(.act-ack) { border-left-color: var(--rule-soft); margin-left: 0.15em; }
.acts button {
  border-color: transparent;
  /* 30x31 was too small to aim at. */
  min-width: 1.8rem;
  min-height: 1.8rem;
  padding: 0 0.25em;
  font-size: var(--t-md);
  line-height: 1;
  color: var(--ink-2);
}
/* The one verb in a row of glyphs, and the one control the owner could not
   hit. A word is a bigger target than an icon and needs no visual search; the
   right margin is the group separator, since the design law reserves hairlines
   for decoration that is already redundant with spacing. */
.acts button.act-reply {
  min-width: 0;
  gap: 0.35em;
  padding: 0 0.6em;
  font: 600 var(--t-sm) var(--body);
  color: var(--ink);
  border-color: var(--rule);
  margin-right: var(--s2);
}
.acts button.act-reply .ico { width: 0.9rem; height: 0.9rem; }
.acts button:hover { color: var(--ink); background: var(--paper-sink); }
.acts button.confirm { color: var(--danger); border-color: var(--danger); }

/* Clicking a message selects it, which is the affordance that actually works
   on a dense transcript and the only one that works on touch. Say so: the
   selected line keeps its strip open and takes a quiet ink edge. */
/* The selection fill is painted as a shadow, not as `background` — a mentioned
   line already owns `background` for its wash, and whichever rule came last
   was erasing the other. An inset spread covers the whole box, so it reads
   identically to a fill while composing with everything else. */
.line.selected {
  --sel-edge: inset 3px 0 var(--ink-2);
  --sel-fill: inset 0 0 0 100vmax var(--paper-raise);
}
/* On a mentioned line the wash is the point, so the selection contributes only
   its edge — and that edge is the mention's own ink, so the 3px marker and the
   3px border read as one 6px rule rather than two colours fighting. */
.line.mentioned.selected {
  --sel-fill: 0 0 transparent;
  --sel-edge: inset 3px 0 var(--human-ink);
}

/* reply breadcrumb + thread link */
.reply-parent, .thread-link {
  grid-column: 2;
  display: block;
  max-width: var(--read-w);
  border: none;
  background: none;
  padding: 0;
  color: var(--ink-2);
  font: 400 var(--t-sm) var(--body);
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.reply-parent:hover, .thread-link:hover { color: var(--human-ink); background: none; text-decoration: underline; }
.thread-link { margin-top: var(--s1); color: var(--ink-2); font-weight: 500; }
.thread-link.unread { font-weight: 700; }
.thread-link.unread::before { content: "● "; font-size: 0.7em; vertical-align: 0.2em; }
.line.thread-active { --thread-edge: inset -3px 0 var(--human-ink); }

/* reaction chips */
/* The row stays only as wide as its reactions. The action strip is anchored
   here so its vertical position follows the message it acts on. */
.chips {
  grid-column: 2;
  position: relative;
  width: max-content;
  /* Reactions share the message measure. If they can use the full grid track,
     a long row runs under the timestamp lane. */
  max-width: min(100%, var(--read-w));
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s1);
  margin-top: var(--s1);
}
/* An empty row must cost nothing: it holds only the strip, which is out of
   flow, so it has no height of its own. */
.chips:not(:has(.chip)) { margin-top: 0; }
.chip {
  border-radius: var(--radius-pill);
  padding: 0.05em 0.55em;
  font-size: var(--t-sm);
  color: var(--ink);
  background: var(--paper-raise);
  line-height: 1.5;
}
.chip:hover { border-color: var(--ink-2); background: var(--paper-sink); }
/* No width change: swapping 1px for 2px grew the chip 2px in both axes and
   reflowed its neighbours, so reacting to a message shuffled the row. */
.chip { border: var(--bd) solid var(--rule-soft); }
.chip.mine { border-color: var(--human-ink); color: var(--human-ink); font-weight: 600; }
.chip-add { color: var(--ink-2); }

/* long pastes fold */
.body.clamped { display: -webkit-box; -webkit-line-clamp: 10; -webkit-box-orient: vertical; overflow: hidden; }
.paste-more {
  display: block; border: none; background: none; padding: 0.15em 0;
  color: var(--ink-2); font: 500 var(--t-sm) var(--body); cursor: pointer;
}
.paste-more:hover { background: none; text-decoration: underline; }

.edited { color: var(--ink-2); font-size: var(--t-xs); }
/* Editing a message used to widen its column by 136px, so the line you were
   fixing was not the line you had been reading. */
.edit-input, textarea.edit-input { width: 100%; max-width: var(--read-w); font: inherit; }
.edit-hint { font-size: var(--t-xs); color: var(--ink-2); }

/* ── 8. the decision card ──────────────────────────────────────────────────
   An escalation is the only thing in the square that stops a person. It is
   an agent asking its principal for authority it does not have. It gets the
   heaviest object in the system: ink border, hard shadow, and its verbs
   spelled out as real buttons. */

.line.escalate { grid-template-columns: minmax(0, 1fr); }
/* The card drops the mark gutter, so it is a one-column grid. .chips asks for
   column 2, which would create an implicit column outside the card and take
   the action strip with it. */
.line.escalate > .chips { grid-column: 1; }
.line.escalate > .mark { display: none; }

.decision {
  max-width: 44em;
  background: var(--paper-raise);
  border: var(--bd) solid var(--rule);
  box-shadow: var(--shadow);
}
.decision-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  padding: var(--s2) var(--s3);
  border-bottom: var(--bd) solid var(--rule);
  background: var(--agent-wash);
  font: 600 var(--t-xs) var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--agent-ink);
}
.decision-for { margin-left: auto; color: var(--ink-2); letter-spacing: 0.04em; }
.decision-body { padding: var(--s3); display: flex; flex-direction: column; gap: var(--s3); }
.decision-asker { display: flex; align-items: center; gap: var(--s2); font-size: var(--t-sm); color: var(--ink-2); }
.decision-asker .nick { font-weight: 700; color: var(--agent-ink); }
.decision-ask { color: var(--ink); white-space: pre-wrap; }
.decision-acts { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.decision-acts .esc-approve { color: var(--action-ink); background: var(--action); border-color: var(--rule); box-shadow: var(--shadow-sm); }
.decision-acts .esc-approve:hover { filter: brightness(1.06); }
.decision-acts .esc-deny { color: var(--ink); }
.decision-acts button:disabled { box-shadow: none; transform: none; }
.decision-note { color: var(--ink-2); font-size: var(--t-sm); }
/* answered: the card stands down but stays legible in history */
.decision.settled { box-shadow: none; opacity: 0.75; }
.decision.settled .decision-head { background: var(--paper-sink); color: var(--ink-2); }

/* a decision addressed to someone else is a fact, not a demand */
.decision.not-mine { box-shadow: none; }
.decision.not-mine .decision-head { background: var(--paper-sink); color: var(--ink-2); }

/* ── 9. danger zone ───────────────────────────────────────────────────────── */
/* --danger is reserved for errors and destructive actions. A joke wearing it
   teaches the room that red means nothing. This is louder anyway: a full-bleed
   ink band on warm paper, with the rules drawn in CSS the way .day-rule does. */
/* It was full-bleed, breaking the transcript's own margins with a negative
   inset and setting DANGER ZONE in the largest display size in the product. A
   joke that takes the whole width every time stops being a joke by the third
   one. Same band, same ink, inside the margins, one step down in type. */
.line.danger-banner {
  display: block;
  /* And it sits in the message column rather than spanning the room. Dropping
     the negative inset stopped it breaking the transcript's margins, but a
     block still fills its container — on a wide window that was still a band
     across the whole width. */
  max-width: var(--read-w);
  margin: var(--s3) 0;
  padding: var(--s2) var(--s3);
  color: var(--ink);
  background: var(--paper-sink);
  border-top: var(--bd-strong) solid var(--rule);
  border-bottom: var(--bd-strong) solid var(--rule);
}
.danger-word {
  display: flex; align-items: center; justify-content: center; gap: var(--s3);
  font: 800 var(--t-lg)/1 var(--display);
  letter-spacing: 0.12em;
}
.danger-word::before, .danger-word::after {
  content: ""; flex: 1; max-width: 6rem;
  border-top: var(--bd-strong) solid var(--ink);
}
.danger-sub {
  display: block; text-align: center; margin-top: var(--s1);
  font: 500 var(--t-xs) var(--mono); letter-spacing: 0.08em; color: var(--ink-2);
}
.danger-who {
  display: flex; align-items: center; justify-content: center;
  gap: var(--s2); margin-top: var(--s2);
  font-size: var(--t-sm); letter-spacing: normal;
}
/* Fires once, on a line a human deliberately typed — and never on the repeat,
   which is the whole point of the repeat. */
@media (prefers-reduced-motion: no-preference) {
  .danger-banner:not(.danger-again) { animation: danger-in 0.5s ease-out; }
}
@keyframes danger-in { from { filter: brightness(1.9); } to { filter: none; } }

/* A sound is a message with a control where the words would be. The pill
   radius is the chip grammar; pressing it moves into its own shadow, which is
   the house's hard-offset language and costs no keyframes. */
.sound-chip {
  display: inline-flex; align-items: baseline; gap: 0.45em;
  border: var(--bd) solid var(--rule); border-radius: var(--radius-pill);
  padding: 0.12em 0.75em;
  font: 600 var(--t-sm)/1.6 var(--mono); color: var(--ink);
  background: var(--paper-raise); box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.sound-chip:hover { background: var(--paper); }
/* .sound-chip:active is covered by the shared press rule above. */
.sound-play { font-size: 0.85em; color: var(--ink-2); }

/* One fixed scene, not a general video surface. It becomes the transcript's
   moving background: the text-only message layer, room header, rail and
   composer all stay in place. */
#chat.blackrobes-playing.blackrobes-ready #messages {
  z-index: 1;
  --ink: var(--blackrobes-white);
  --ink-2: var(--blackrobes-text-secondary);
  --ink-3: var(--blackrobes-text-tertiary);
  --rule: var(--blackrobes-rule);
  --rule-soft: var(--blackrobes-rule-soft);
  --paper: var(--blackrobes-surface);
  --paper-raise: var(--blackrobes-surface-raise);
  --paper-sink: var(--blackrobes-surface-sink);
}
#chat.blackrobes-playing.blackrobes-ready #messages .line {
  color: var(--blackrobes-pink);
  text-shadow: 0 0 0.12em var(--blackrobes-white), 0 0 0.45em currentColor, 0 0 0.8em var(--blackrobes-cyan);
}
#chat.blackrobes-playing.blackrobes-ready #messages .line :is(.body, .body *, .nick, .obo, .ts) {
  color: inherit !important;
  font-family: var(--display);
  font-weight: 800;
}
#chat.blackrobes-playing.blackrobes-ready #messages .blackrobes-command-mark {
  color: var(--blackrobes-lime) !important;
  font-weight: 900;
  text-shadow: 0 0.04em var(--blackrobes-forest), 0 0 0.3em var(--blackrobes-lime), 0 0 0.7em var(--blackrobes-yellow);
}
#chat.blackrobes-playing.blackrobes-ready #messages .line .ts { display: none; }
@media (prefers-reduced-motion: no-preference) {
  #chat.blackrobes-playing.blackrobes-ready #messages .line {
    animation: blackrobes-rainbow 2.8s ease-in-out infinite;
  }
  #chat.blackrobes-playing.blackrobes-ready #messages .line:nth-child(3n + 2) { animation-delay: -0.9s; }
  #chat.blackrobes-playing.blackrobes-ready #messages .line:nth-child(3n) { animation-delay: -1.8s; }
}
@keyframes blackrobes-rainbow {
  0%, 100% { color: var(--blackrobes-pink); }
  25% { color: var(--blackrobes-gold); }
  50% { color: var(--blackrobes-mint); }
  75% { color: var(--blackrobes-cyan); }
}
#blackrobes-player[hidden] { display: none; }
#blackrobes-player {
  grid-area: main;
  position: relative;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
  display: grid;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: var(--ink);
}
#chat.blackrobes-playing.blackrobes-ready #blackrobes-player { opacity: 1; }
#blackrobes-stage {
  position: relative;
  container-type: size;
  width: 100%;
  height: 100%;
  min-height: 0;
  overflow: hidden;
  background: var(--ink);
}
#blackrobes-stage iframe {
  display: block;
  position: absolute;
  /* The source contains letterboxing. Cover first, then add a fixed source
     zoom so a tall installed-app viewport cannot reveal those encoded bars.
     Bazaar renders its own captions above the cropped iframe. */
  bottom: max(-8cqh, calc(100cqh - max(100cqh, 56.25cqw)));
  left: 50%;
  width: max(100cqw, 177.7778cqh);
  height: max(100cqh, 56.25cqw);
  transform: translateX(-50%) scale(1.12);
  border: 0;
  pointer-events: none;
}
#blackrobes-marquee {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}
#blackrobes-captions[hidden] { display: none; }
#blackrobes-captions {
  position: absolute;
  left: 50%;
  bottom: clamp(2.75rem, 7cqh, 5rem);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.12em;
  max-width: min(82%, 52rem);
  transform: translateX(-50%);
  color: var(--blackrobes-white);
  font: 700 clamp(1.05rem, 2.3cqw, 2rem)/1.18 system-ui, sans-serif;
  text-align: center;
  pointer-events: none;
}
.blackrobes-caption-line {
  display: block;
  width: fit-content;
  max-width: 100%;
  padding: 0.12em 0.42em 0.18em;
  background: var(--blackrobes-caption-backdrop);
  text-wrap: balance;
}
#blackrobes-marquee::before {
  content: "BLACK ROBES STUFF ✦ BLACK ROBES STUFF ✦ BLACK ROBES STUFF";
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  padding: 0.45rem 1rem;
  color: var(--blackrobes-marquee-ink);
  font: 900 clamp(0.72rem, 1.35cqw, 1.05rem)/1 var(--display);
  letter-spacing: 0.16em;
  white-space: nowrap;
  text-shadow: 0 0 0.22em var(--blackrobes-marquee-forest), 0 0 0.55em var(--blackrobes-marquee-lime);
  -webkit-text-stroke: 0.06em var(--blackrobes-marquee-stroke);
}
#blackrobes-marquee.blackrobes-marquee-laid-out::before { display: none; }
.blackrobes-marquee-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.blackrobes-marquee-text {
  fill: var(--blackrobes-marquee-ink);
  stroke: var(--blackrobes-marquee-stroke);
  stroke-width: 0.06em;
  paint-order: stroke fill;
  font: 900 clamp(0.72rem, 1.35cqw, 1.05rem)/1 var(--display);
  letter-spacing: 0.16em;
  white-space: pre;
}
@media (prefers-reduced-motion: reduce) {
  #blackrobes-marquee::before { display: block; }
  .blackrobes-marquee-svg { display: none; }
}
#blackrobes-entry[hidden] { display: none; }
#blackrobes-entry {
  grid-area: main;
  position: relative;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  min-width: 0;
  min-height: 0;
  padding: var(--s5);
  overflow: hidden;
  text-align: center;
  color: var(--blackrobes-entry-ink);
  background:
    radial-gradient(circle at 50% 15%, var(--blackrobes-sun), transparent 24%),
    linear-gradient(var(--blackrobes-sky) 0 46%, var(--blackrobes-field) 46% 68%, var(--blackrobes-field-deep) 68% 100%);
}
#blackrobes-entry::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle, var(--blackrobes-entry-omen) 0 0.09rem, transparent 0.12rem) 12% 18% / 7rem 5rem repeat,
    radial-gradient(circle, var(--blackrobes-entry-omen) 0 0.05rem, transparent 0.08rem) 64% 46% / 5rem 4rem repeat;
  opacity: 0.64;
  pointer-events: none;
}
#blackrobes-entry h2 {
  position: relative;
  margin: 0;
  max-width: 18ch;
  font: 900 clamp(2rem, 5vw, 4.5rem)/0.95 var(--display);
  text-wrap: balance;
}
#blackrobes-enter {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s2);
  border: var(--bd-strong) solid var(--blackrobes-entry-ink);
  padding: 0.65em 1.1em 0.8em;
  color: var(--blackrobes-entry-ink);
  background: var(--blackrobes-button);
  box-shadow: 0.28rem 0.28rem 0 var(--blackrobes-entry-ink);
  font: 800 var(--t-md)/1 var(--body);
}
#blackrobes-enter:hover { background: var(--blackrobes-button-hover); }
#blackrobes-enter:active { transform: translate(0.14rem, 0.14rem); box-shadow: 0.14rem 0.14rem 0 var(--blackrobes-entry-ink); }
#blackrobes-enter:disabled { opacity: 0.72; cursor: wait; }
.blackrobes-entry-omens {
  display: block;
  font-size: clamp(2rem, 6vw, 4rem);
  line-height: 0.9;
  letter-spacing: 0.08em;
  filter: drop-shadow(0 0.08em 0 var(--blackrobes-drop-shadow));
}

/* The custom-emoji row with nothing in it. A dashed square is the drawing
   convention for a slot waiting to be filled. */
.picker-empty {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.75rem; height: 1.75rem; padding: 0 var(--s2);
  font: 500 var(--t-sm) var(--mono); color: var(--ink-3);
}
.picker-empty.is-add {
  border: var(--bd) dashed var(--rule-soft); background: none;
  color: var(--ink-2); cursor: pointer;
}
.picker-empty.is-add:hover { border-color: var(--rule); color: var(--ink); }

/* ── 10. cards inside the transcript ───────────────────────────────────────
   Work-refs, files, surfaces and unfurls share one card grammar: an eyebrow
   naming what it is, a title, and a coloured left rule saying which world it
   came from. Vermilion when an agent made it; plain ink when it is a fact. */

.tcard {
  display: block;
  max-width: var(--read-w);
  margin-top: var(--s2);
  border: var(--bd) solid var(--rule);
  border-left-width: 6px;
  background: var(--paper-raise);
  overflow: hidden;
}
.tcard-body { padding: var(--s2) var(--s3); }
.tcard-eyebrow {
  font: 600 var(--t-xs) var(--mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.tcard-title { color: var(--ink); font-weight: 600; display: block; }
.tcard-title:hover { color: var(--human-ink); }
.tcard-url, .tcard-host { color: var(--human-ink); font-size: var(--t-sm); word-break: break-all; display: block; }
.tcard-desc { color: var(--ink-2); font-size: var(--t-sm); line-height: 1.5; margin-top: var(--s1); }

.refcard { border-left-color: var(--human-ink); }
.unfurlcard { border-left-color: var(--rule-soft); }
.surfacecard { border-left-color: var(--agent); }
.filecard { border-left-color: var(--ink-2); }

.unfurl-img {
  display: block;
  margin-top: var(--s2);
  max-width: 100%;
  max-height: 11rem;
  object-fit: cover;
  border: var(--bd-hair) solid var(--rule-soft);
}

.surface-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  padding: var(--s2) var(--s3);
}
/* The word that names the object, and it had no rule at all — so the one label
   telling a reader "this is a surface, an agent built it" rendered as ordinary
   body text. Same treatment as .tcard-eyebrow, in the agent's ink, because a
   surface is the one thing in the transcript only an agent can make. */
.surface-eyebrow {
  font: 600 var(--t-xs) var(--mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--agent-ink);
  flex: none;
}
.surface-title { color: var(--ink); font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.surface-acts { display: inline-flex; align-items: center; gap: var(--s1); margin-left: auto; flex: none; }
.surface-acts button, .surface-acts a {
  border-color: transparent; background: transparent; text-decoration: none;
  padding: 0.2em 0.4em; font: 600 var(--t-xs) var(--mono); color: var(--ink-2);
}
.surface-acts button:hover, .surface-acts a:hover { color: var(--ink); background: var(--paper-sink); }
.surface-frame[hidden] { display: none; }
/* The height here is only the opening bid: the document reports its real one
   and the card resizes to it, clamped to 8-32rem. Starting mid-range keeps the
   correction small in either direction on a cold load. */
.surface-frame {
  display: block; width: 100%; height: 14rem; border: 0;
  border-top: var(--bd) solid var(--rule); background: var(--paper);
}

.filecard { display: flex; align-items: center; gap: var(--s3); padding: var(--s2) var(--s3); }
.filecard .file-name { color: var(--ink); font-weight: 500; }
.filecard .file-size { color: var(--ink-2); font-size: var(--t-sm); }
.filecard a { font-size: var(--t-sm); margin-left: auto; }

.filewrap,
.gifwrap,
.msg-img-link { display: block; min-width: 0; max-width: 100%; margin-top: var(--s2); }
.msg-img {
  display: block; width: auto; height: auto;
  max-height: 20rem; max-width: 100%;
  border: var(--bd) solid var(--rule);
}
.file-cap { display: block; font-size: var(--t-xs); color: var(--ink-2); }

/* the self-only card: rendered into the transcript, never posted. The dashed
   edge says "not a message" before the footnote does. */
.line.self-card { grid-template-columns: minmax(0, 1fr); }
.line.self-card > .mark { display: none; }
.self-card-inner {
  max-width: 40em;
  padding: var(--s3);
  border: var(--bd) dashed var(--rule);
  background: var(--paper-raise);
}
.self-card-title {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s4);
  margin-bottom: var(--s2); font-weight: 700;
}
.self-card-body p { margin: 0 0 var(--s2); }
.self-card-snippet { margin-bottom: var(--s2); max-height: 11em; overflow: auto; }
.self-card-actions { display: flex; gap: var(--s3); margin-bottom: var(--s2); flex-wrap: wrap; }
.self-card-copy, .self-card-dismiss { border: none; background: none; padding: 0; font: 600 var(--t-sm) var(--mono); }
.self-card-copy { color: var(--human-ink); }
.self-card-dismiss { color: var(--ink-2); }
.self-card-copy:hover, .self-card-dismiss:hover { background: none; text-decoration: underline; }
.self-card-note { color: var(--ink-2); font-size: var(--t-sm); margin: var(--s1) 0 0 !important; }
/* One of exactly two literal colours below the token block, and deliberate: a
   QR code is read by a camera, not by a person, and the quiet zone has to be
   true white or a dark theme makes it unscannable. Not a theme colour. */
.device-link-qr {
  display: block; max-width: min(200px, 42vw); height: auto !important;
  image-rendering: pixelated; margin-bottom: var(--s2);
  border: var(--bd) solid var(--rule); background: #fff; padding: var(--s2);
}

.snippet {
  font: 400 var(--t-sm)/1.5 var(--mono);
  color: var(--ink);
  background: var(--paper);
  border: var(--bd) solid var(--rule);
  padding: var(--s2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
  user-select: all;
}

/* markdown inside a message body */
.codespan {
  font-family: var(--mono); font-size: 0.9em;
  color: var(--ink); background: var(--paper);
  border: var(--bd-hair) solid var(--rule-soft); padding: 0 0.25em;
}
.codeblock {
  display: block;
  font: 400 0.9em/1.5 var(--mono);
  color: var(--ink); background: var(--paper);
  border: var(--bd) solid var(--rule);
  padding: var(--s2) var(--s3);
  margin: var(--s1) 0;
  max-width: 100%;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-x: auto;
}
.quote {
  display: block; color: var(--ink-2);
  border-left: var(--bd) solid var(--rule-soft);
  padding-left: var(--s3); margin: var(--s1) 0; white-space: pre-wrap;
}
.fmt-b { font-weight: 700; }
.fmt-i { font-style: italic; }
.fmt-s { text-decoration: line-through; color: var(--ink-2); }
.mdlist { display: block; list-style: none; margin: var(--s1) 0; padding-left: 0.25em; white-space: normal; }
.mdlist li { padding-left: 1.1em; text-indent: -1.1em; }
.mdlist li::before { content: "▪ "; color: var(--ink-2); }
.cemoji { width: 20px; height: 20px; object-fit: contain; vertical-align: text-bottom; display: inline-block; }
.chip .cemoji { width: 16px; height: 16px; }

/* ── 11. the empty room ────────────────────────────────────────────────────
   A channel with nothing in it is a member's first view of the product often
   enough that a blank rectangle is not acceptable. */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  min-height: 60%;
  padding: var(--s6) var(--s4);
  text-align: center;
  color: var(--ink-2);
}
.empty-art { flex: none; }
.empty h2 { font: 800 var(--t-2xl)/var(--lh-tight) var(--display); color: var(--ink); letter-spacing: -0.02em; }
.empty p { max-width: 34em; }
.empty .empty-acts { display: flex; gap: var(--s2); flex-wrap: wrap; justify-content: center; margin-top: var(--s2); }
.empty-sm { min-height: 0; padding: var(--s5) var(--s4); gap: var(--s2); }
.empty-sm h2 { font-size: var(--t-lg); }
.empty-sm p { font-size: var(--t-sm); }

/* ── 12. composer ─────────────────────────────────────────────────────────── */

#promptbar {
  grid-area: prompt;
  position: relative;
  border-top: var(--bd-strong) solid var(--rule);
  padding: var(--s3) var(--s4) calc(var(--s3) + env(safe-area-inset-bottom));
  background: var(--paper-raise);
}

/* One composer, written once. The channel and the thread are the same control
   in two places, and every rule below is keyed on a class rather than an id so
   they cannot drift apart again. They did: the thread had no toolbar, no sound
   button, and its own send-button metrics. */
.composer-fmtbar { display: flex; align-items: center; gap: 0.1em; padding-bottom: var(--s1); }
.composer-fmtbar button {
  border-color: transparent; background: transparent;
  padding: 0.15em 0.5em; min-width: 2em;
  font-size: var(--t-sm); color: var(--ink-2);
}
.composer-fmtbar button:hover { color: var(--ink); background: var(--paper-sink); }
.composer-fmtbar .fmt-bold { font-weight: 700; }
.composer-fmtbar .fmt-italic { font-style: italic; }

.composer-form {
  display: flex;
  align-items: flex-end;
  gap: var(--s2);
  background: var(--paper);
  border: var(--bd) solid var(--rule);
  padding: var(--s1);
}
.composer-form:focus-within { outline: var(--focus); outline-offset: var(--focus-offset); }
#prompt-label {
  flex: none;
  align-self: stretch;
  display: flex;
  align-items: center;
  color: var(--ink);
  background: var(--paper-sink);
  border: var(--bd-hair) solid var(--rule);
  font: 600 var(--t-xs) var(--mono);
  padding: 0 var(--s2);
  white-space: nowrap;
  max-width: 40%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.composer-input-shell {
  position: relative;
  flex: 1;
  min-width: 0;
}
.composer-input,
.composer-mention-mirror {
  font: inherit;
  background: transparent;
  border: none;
  padding: var(--s2);
  min-width: 0;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  scrollbar-gutter: stable;
}
.composer-input {
  position: relative;
  z-index: 1;
  color: transparent;
  caret-color: var(--ink);
  resize: none;
  overflow-y: hidden;
}
.composer-input::selection { background: var(--human-wash); }
.composer-mention-mirror {
  position: absolute;
  inset: 0;
  z-index: 0;
  color: var(--ink);
  pointer-events: none;
  overflow: hidden;
}
.composer-mention-mirror .resolved {
  color: var(--human-ink);
  background: var(--human-wash);
  text-decoration: underline 2px;
  text-underline-offset: 0.18em;
}
.composer-mention-mirror .resolved.agent {
  color: var(--agent-ink);
  background: var(--agent-wash);
}
.composer-mention-mirror .unresolved {
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}
.composer-input:focus-visible { outline: none; }
.composer-icon {
  flex: none; border-color: transparent; background: transparent;
  color: var(--ink-2); font-size: var(--t-lg); padding: 0.2em 0.4em;
}
.composer-icon:hover { color: var(--ink); background: var(--paper-sink); }
.composer-send {
  flex: none;
  color: var(--action-ink);
  background: var(--action);
  border-color: var(--rule);
  font-size: var(--t-md);
  padding: 0.35em 0.8em;
  line-height: 1.2;
}
.composer-send:hover { background: var(--action); filter: brightness(1.06); }

#typing-line[hidden], #upload-status[hidden] { display: none; }
#typing-line { font-size: var(--t-sm); color: var(--ink-2); padding-bottom: var(--s1); min-height: 1.4em; }
#upload-status { display: flex; align-items: center; gap: var(--s3); padding-bottom: var(--s1); font-size: var(--t-sm); color: var(--ink-2); }
#upload-bar, #thread-upload-bar {
  flex: 1; max-width: 14em; height: 6px;
  background: var(--paper); border: var(--bd-hair) solid var(--rule); overflow: hidden;
}
/* A member uploading their own file is a person's business, not an agent's. */
#upload-fill, #thread-upload-fill {
  display: block; height: 100%; width: 0;
  background: var(--human-ink); transition: width 0.15s linear;
}

.composer-mention-status {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.composer-mention-status[hidden] { display: none; }

/* #108: the thread composer uploads into its own thread and reports there,
   because a drop into a thread that reported in the main transcript is a
   file that looks like it went somewhere else. */
#thread-upload-status[hidden] { display: none; }
#thread-upload-status {
  display: flex; align-items: center; gap: var(--s3);
  padding-bottom: var(--s1);
  font-size: var(--t-sm); color: var(--ink-2);
}
#thread-upload-status.error { color: var(--danger); }
#thread-upload-status.error #thread-upload-bar { border-color: var(--danger); }

#thread-pane.dragging::after {
  content: "📎  drop into this thread";
  position: absolute;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--veil);
  color: var(--ink);
  font: 800 var(--t-xl) var(--display);
  border: 4px dashed var(--agent);
  pointer-events: none;
}

#chat.dragging::after {
  content: "📎  drop to share it here";
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--veil);
  color: var(--ink);
  font: 800 var(--t-2xl) var(--display);
  border: 4px dashed var(--agent);
  pointer-events: none;
}

/* tab completion */
#tab-pop[hidden] { display: none; }
#tab-pop {
  position: fixed;
  min-width: 16em;
  max-width: min(30em, 90vw);
  max-height: 15em;
  overflow-y: auto;
  padding: var(--s1) 0;
  background: var(--paper-raise);
  border: var(--bd) solid var(--rule);
  box-shadow: var(--shadow);
  z-index: 60;
}
.tab-cand {
  display: flex; align-items: baseline; gap: var(--s2);
  width: 100%; text-align: left;
  border: none; border-left: var(--bd-strong) solid transparent;
  padding: 0.25em var(--s3);
  color: var(--ink); font: 400 var(--t-md) var(--body);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tab-cand:hover, .tab-cand.sel { background: var(--paper-sink); }
.tab-cand.sel { border-left-color: var(--ink); font-weight: 600; }
.tab-hint { color: var(--ink-2); font-size: var(--t-sm); margin-left: auto; }
/* The footer that says the list continues. Not a row: it is never selectable,
   so it must not look like one. */
.tab-more {
  display: block; padding: var(--s1) var(--s2);
  border-top: var(--bd-hair) solid var(--rule-soft);
  color: var(--ink-3);
}

/* ── 13. sheets, drawers and overlays ──────────────────────────────────────
   Most panels are a SHEET anchored to the right edge or an OVERLAY floating
   over the centre. Needs You becomes part of the desktop workspace below. */

.scrim {
  position: fixed;
  inset: 0;
  z-index: 45;
  background: var(--scrim);
}
.scrim[hidden] { display: none; }

.sheet {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  z-index: 50;
  width: min(24rem, 94vw);
  display: flex;
  flex-direction: column;
  /* The design law assigns --paper to the page and --paper-raise to the card.
     Every panel had it the other way round, so cards looked cut OUT of the
     sheet rather than sitting on it. */
  background: var(--paper);
  border-left: var(--bd-strong) solid var(--rule);
}
.sheet[hidden] { display: none; }
.sheet-wide { width: min(30rem, 96vw); }

.sheet-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  padding-top: calc(var(--s3) + env(safe-area-inset-top));
  border-bottom: var(--bd-strong) solid var(--rule);
  background: var(--paper-sink);
}
.sheet-head h2 { font: 800 var(--t-xl)/var(--lh-tight) var(--display); letter-spacing: -0.01em; }
.sheet-head .sheet-sub { color: var(--ink-2); font-size: var(--t-sm); }
.sheet-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--s4);
  padding-bottom: calc(var(--s5) + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}
.sheet-lead { color: var(--ink-2); font-size: var(--t-sm); line-height: 1.5; }

/* The sound board. Three named groups, because "where did this come from" is
   the only fact about a sound that is worth stating. */
.sheet-foot {
  flex: none;
  padding: var(--s3) var(--s4);
  padding-bottom: calc(var(--s3) + env(safe-area-inset-bottom));
  border-top: var(--bd) solid var(--rule);
  background: var(--paper-sink);
}
.sound-group + .sound-group { margin-top: var(--s4); }
.sound-group-label {
  font: 600 var(--t-xs) var(--mono);
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-2);
  padding-bottom: var(--s1);
  border-bottom: var(--bd-hair) solid var(--rule-soft);
  margin-bottom: var(--s3);
}
.sound-row { display: flex; flex-wrap: wrap; gap: var(--s2); }

.ov[hidden] { display: none; }
.ov {
  position: fixed;
  z-index: 50;
  top: 10vh;
  left: 50%;
  transform: translateX(-50%);
  width: min(40rem, 94vw);
  max-height: 74vh;
  display: flex;
  flex-direction: column;
  background: var(--paper-raise);
  border: var(--bd) solid var(--rule);
  box-shadow: var(--shadow-lg);
}
.ov-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  border-bottom: var(--bd) solid var(--rule);
}
.ov-head input { background: transparent; border: none; padding: var(--s1) 0; font-size: var(--t-lg); }
.ov-head input:focus-visible { outline: none; }
.ov-head:focus-within { outline: var(--focus); outline-offset: calc(var(--focus-offset) * -1); }
.ov-list { overflow-y: auto; padding: var(--s1) 0; }
.ov-foot {
  border-top: var(--bd-hair) solid var(--rule-soft);
  padding: var(--s2) var(--s3);
  color: var(--ink-2);
  font: 400 var(--t-xs)/1.6 var(--mono);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s3);
}
kbd {
  font: 600 var(--t-xs) var(--mono);
  border: var(--bd-hair) solid var(--rule-soft);
  background: var(--paper);
  padding: 0 0.35em;
  color: var(--ink);
}

/* a selectable row, shared by search, the switcher and the attention list */
.result-row {
/* The message route is an action, not a destination, so it is separated from
   the rooms above it rather than dressed as one of them. */
.result-row.result-act { border-top: var(--bd) solid var(--rule-soft); }
.result-row.result-act:first-child { border-top: 0; }
.switcher-empty { padding: var(--s3); color: var(--ink-2); font-size: var(--t-sm); margin: 0; }
  display: block;
  width: 100%;
  text-align: left;
  border: none;
  border-left: var(--bd-strong) solid transparent;
  padding: var(--s2) var(--s3);
  color: var(--ink);
  font: 400 var(--t-md) var(--body);
}
.result-row:hover, .result-row.sel { background: var(--paper-sink); }
.result-row.sel { border-left-color: var(--ink); }
.result-head { display: flex; align-items: baseline; gap: var(--s2); font-size: var(--t-sm); flex-wrap: wrap; }
.result-chan { color: var(--ink); font-weight: 700; }
.result-nick { font-weight: 600; }
.result-time { color: var(--ink-2); font-family: var(--mono); font-size: var(--t-xs); margin-left: auto; }
.result-body { color: var(--ink-2); font-size: var(--t-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.result-hint { color: var(--ink-2); font-size: var(--t-sm); }
.ov-empty { padding: var(--s4); color: var(--ink-2); text-align: center; }

/* ── 14. needs you ─────────────────────────────────────────────────────────
   The constitutional promise, given a room of its own: exactly two things
   are here — someone said your name, and someone needs your decision. */

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

.needs-status {
  margin: 0 0 var(--s3);
  padding: var(--s2) var(--s3);
  border-left: var(--bd-strong) solid var(--attention);
  border-radius: var(--radius);
  background: var(--paper-sink);
  color: var(--ink-2);
  font-size: var(--t-sm);
  line-height: 1.4;
}
.needs-status:empty { display: none; }

#needs-list { display: flex; flex-direction: column; gap: var(--s5); }

.need-group { display: flex; flex-direction: column; gap: var(--s3); }
.need-group-title {
  padding-bottom: var(--s1);
  border-bottom: var(--bd) solid var(--rule);
  color: var(--ink-2);
  font: 600 var(--t-xs)/1.4 var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.need {
  position: relative;
  border: var(--bd) solid var(--rule);
  background: var(--paper-raise);
  display: flex;
  flex-direction: column;
  transition: transform 0.18s cubic-bezier(.22, 1, .36, 1), box-shadow 0.18s ease;
}
.need.is-selected {
  border-left-width: calc(var(--bd-strong) * 2);
  border-left-color: var(--human-ink);
  box-shadow: var(--shadow);
  transform: translateX(var(--s1));
}
.need.is-opened:not(.is-saved) { border-left-style: double; }
.need.is-saved .need-head::before {
  content: "saved";
  flex: none;
  padding: 0 var(--s1);
  border: var(--bd-hair) solid var(--rule);
  background: var(--paper-sink);
  color: var(--ink);
  white-space: nowrap;
}
.need-open {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.need-open:hover { background: var(--paper); text-decoration: none; }
.need-open:focus-visible {
  outline: var(--focus);
  outline-offset: calc(var(--focus-offset) * -1);
}
/* Uppercase mono with tracking is reserved for navigation groups. A need's
   head is not one — it was printing `NEEDS YOUR DECISION #GENERAL` and then
   `12m ago` in the same 11px row, in two different treatments. */
.need-head {
  position: relative;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s1);
  padding: var(--s2) var(--s3);
  /* The time keeps its own narrow column while the variable metadata can
     wrap or truncate. A saved tag, a long workspace, and a message count can
     now coexist without making the panel scroll sideways. */
  padding-right: 4.25rem;
  border-bottom: var(--bd-hair) solid var(--rule-soft);
  font: 600 var(--t-xs) var(--mono);
  letter-spacing: 0.02em;
}
.need-kind, .need-sep { flex: none; white-space: nowrap; }
.need-kind { color: var(--human-ink); }
.need.need-escalate .need-kind { color: var(--agent-ink); }
.need-where, .need-sep { color: var(--ink-2); }
.need-where {
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.need-sep + .need-where { flex: 1 1 4rem; }
/* An obligation in another workspace. Marked with a word rather than a colour,
   and the card is quieter than one you can act on from here — you cannot
   answer it without going there, so it should not look like you can. */
.need-ws {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  padding: 0 var(--s2);
  border: var(--bd-hair) solid var(--rule-soft);
  background: var(--paper-sink);
  color: var(--ink-2);
  letter-spacing: 0.02em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.need.need-elsewhere { box-shadow: none; }
.need.need-elsewhere .need-head { background: var(--paper-sink); }
.need-when {
  position: absolute;
  top: var(--s2);
  right: var(--s3);
  color: var(--ink-2);
  white-space: nowrap;
}

/* The same obligation that is the heaviest object in the transcript was flat
   in the destination built to hold it, while the Approve button inside it kept
   the only shadow on the card. That is the fourth design rule inverted, in the
   one place it matters most. */
.need.need-escalate { box-shadow: var(--shadow); }
.need.need-escalate .need-head {
  background: var(--agent-wash);
  border-bottom: var(--bd) solid var(--rule);
}
.need-body { padding: var(--s3); display: flex; gap: var(--s3); align-items: flex-start; }
.need-said { min-width: 0; }
.need-who { font-weight: 700; font-size: var(--t-sm); }
.need-text {
  color: var(--ink);
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.need-open-cue {
  display: none;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border-top: var(--bd-hair) solid var(--rule-soft);
  color: var(--ink-2);
  font: 500 var(--t-xs)/1.4 var(--mono);
}
.need.is-selected .need-open-cue,
.need.is-opened .need-open-cue { display: flex; }
.need-open-cue > :first-child {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.need-open-cue .need-key { white-space: nowrap; }
.need-acts {
  display: flex; gap: var(--s2); flex-wrap: wrap;
  margin: 0 var(--s3);
  padding: var(--s3) 0;
  border-top: var(--bd-hair) solid var(--rule-soft);
}
.need-save { display: inline-flex; align-items: center; gap: var(--s1); }
.need-save[aria-pressed="true"] {
  background: var(--paper-sink);
  border-color: var(--ink);
  font-weight: 700;
}
.need-save .ico { width: 0.875rem; height: 0.875rem; }
.need[aria-busy="true"] { opacity: 0.7; }
.need[aria-busy="true"] .need-open { pointer-events: none; }
.need-state {
  margin-left: auto;
  color: var(--ink-2);
  font-weight: 500;
}
.need-quiet { padding: var(--s3); color: var(--ink-2); font-size: var(--t-sm); }

/* Four panels sharing one shape is a hierarchy problem. This one is the
   product's whole promise, so its head says whether anything is waiting —
   with the heading and the drawing beneath it as the other two cues. */
#needs-panel .sheet-head {
  background: var(--attention-wash);
  border-bottom: var(--bd-strong) solid var(--human-ink);
}
#needs-panel .sheet-head h2 { font-size: var(--t-2xl); }
#needs-panel.at-rest .sheet-head {
  background: var(--paper-sink);
  border-bottom-color: var(--rule);
}
#needs-panel .sheet-head > div { min-width: 0; }
#needs-panel .sheet-sub { display: block; }

/* Needs You is a master list on a wide screen. It opens beside the rail and
   gives the remaining width to one work surface. A thread replaces the room
   there instead of becoming a fourth, squeezed column. */
@media (min-width: 1101px) {
  #chat {
    --needs-panel-w: clamp(20rem, 24vw, 24rem);
    grid-template:
      "top  top   top    top"    auto
      "rail needs main   main"   1fr
      "rail needs prompt prompt" auto
      / var(--chat-rail-w) 0 minmax(0, 1fr) 0;
    transition: grid-template-columns 0.26s cubic-bezier(.22, 1, .36, 1);
  }
  #chat.needs-open {
    grid-template-columns: var(--chat-rail-w) var(--needs-panel-w) minmax(0, 1fr) 0;
  }
  #needs-backdrop { display: none !important; }
  #needs-panel {
    position: relative;
    inset: auto;
    grid-area: needs;
    z-index: 2;
    width: auto;
    min-width: 0;
    border-left: 0;
    border-right: var(--bd-strong) solid var(--rule);
    opacity: 0;
    overflow: hidden;
    transform: translateX(calc(var(--s4) * -1));
    transition:
      opacity 0.2s ease,
      transform 0.26s cubic-bezier(.22, 1, .36, 1);
  }
  #chat.needs-open #needs-panel { opacity: 1; transform: translateX(0); }
  #chat.needs-open.thread-open {
    grid-template:
      "top  top   top    top"    auto
      "rail needs main   thread" 1fr
      "rail needs main   thread" auto
      / var(--chat-rail-w) var(--needs-panel-w) 0 minmax(0, 1fr);
  }
  #chat.needs-open.thread-open #messages,
  #chat.needs-open.thread-open #promptbar,
  #chat.needs-open.thread-open #to-latest { display: none; }
  #chat.needs-open .needs-destination-in {
    animation: needs-destination-in 0.24s cubic-bezier(.22, 1, .36, 1);
  }
}

@keyframes needs-destination-in {
  from { opacity: 0; transform: translateX(var(--s6)); }
  to { opacity: 1; transform: translateX(0); }
}

/* ── 15. help ──────────────────────────────────────────────────────────────
   /help used to print ten lines into the transcript. It is a reference, so
   it gets a reference's shape. */

.help-sec { display: flex; flex-direction: column; gap: var(--s2); }
.help-sec h3 { font: 800 var(--t-lg)/var(--lh-tight) var(--display); }
.help-back { gap: var(--s1); white-space: nowrap; }
.help-back span { font-size: var(--t-xs); }
.help-nav {
  flex: none; display: flex; gap: 0;
  padding: 0 var(--s4); border-bottom: var(--bd) solid var(--rule);
  background: var(--paper-sink);
}
.help-nav button {
  border: none; border-bottom: var(--bd-strong) solid transparent;
  padding: var(--s2) var(--s3); background: transparent;
  color: var(--ink-2); font: 600 var(--t-sm) var(--mono);
}
.help-nav button[aria-current="page"] { border-bottom-color: var(--agent-ink); color: var(--ink); }
#help-version { color: var(--ink-3); font-size: var(--t-xs); }
.changelog-current {
  padding: var(--s2) var(--s3); border: var(--bd) solid var(--rule);
  background: var(--paper-sink); font: 600 var(--t-sm) var(--mono);
}
#help-body > h1 { font: 800 var(--t-xl)/var(--lh-tight) var(--display); }
#help-body > h2 {
  padding-top: var(--s2); border-top: var(--bd-strong) solid var(--rule);
  font: 800 var(--t-xl)/var(--lh-tight) var(--display);
}
#help-body > h2 + p { margin-top: calc(-1 * var(--s1)); font-size: var(--t-xs); }
#help-body > h3 { font: 700 var(--t-md)/var(--lh-tight) var(--display); }
#help-body > p, #help-body > ul { color: var(--ink-2); font-size: var(--t-sm); line-height: 1.55; }
#help-body > ul { padding-left: var(--s4); }
#help-body > ul li + li { margin-top: var(--s1); }
#help-body a { overflow-wrap: anywhere; }
.changelog-credit { color: var(--ink-2); font: 500 var(--t-xs)/1.45 var(--mono); }
.changelog-state { margin: auto 0; color: var(--ink-2); text-align: center; }
.changelog-state button { margin-top: var(--s3); }
.help-grid { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s2) var(--s3); align-items: baseline; }
.help-key {
  font: 600 var(--t-sm) var(--mono); color: var(--ink); white-space: nowrap;
  background: var(--paper-sink); border: var(--bd-hair) solid var(--rule-soft); padding: 0 0.35em;
}
.help-what { color: var(--ink-2); font-size: var(--t-sm); }
.help-legend { display: flex; flex-direction: column; gap: var(--s2); }
.help-legend-row { display: flex; align-items: center; gap: var(--s3); }
.help-legend-row .help-what { color: var(--ink); }

/* ── 16. thread pane ──────────────────────────────────────────────────────── */

#thread-pane[hidden] { display: none; }
#thread-pane {
  position: relative;   /* #108: anchors the pane's own drop overlay */
  grid-area: thread;
  min-width: 0;
  min-height: 0;
  display: grid;
  /* This pane is the containment boundary. Without an explicit zero-minimum
     column, the grid creates an `auto` column from its widest child. The
     composer's intrinsic width then makes every row wider than the pane and
     moves the close control past the viewport. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
  background: var(--paper-raise);
  border-left: var(--bd-strong) solid var(--rule);
}
#thread-pane > * { min-width: 0; }
#thread-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-bottom: var(--bd-strong) solid var(--rule);
  background: var(--paper-sink);
}
#thread-title { font: 800 var(--t-lg)/var(--lh-tight) var(--display); }
#thread-count { color: var(--ink-2); font-size: var(--t-sm); }
#thread-messages {
  min-height: 0; min-width: 0;
  overflow: auto; overflow-x: hidden;
  padding: var(--s2) var(--s4) var(--s5);
  scrollbar-width: thin;
}
/* #86: a wide attachment used to push the whole thread pane sideways on a
   phone. All three levels need the cap — the body is the one that holds an
   image. */
#thread-messages .line,
#thread-messages .msgbody,
#thread-messages .msgbody .body { min-width: 0; max-width: 100%; }
.thread-root-label {
  margin: var(--s3) calc(var(--s4) * -1) 0;
  padding: 0 var(--s4);
  color: var(--ink-2);
  font: 600 var(--t-xs) var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
#thread-messages .thread-root {
  margin: var(--s1) calc(var(--s4) * -1) var(--s3);
  padding: var(--s2) var(--s4) var(--s4);
  border-bottom: var(--bd) solid var(--rule);
}
#thread-messages .thread-reply { margin-top: var(--s4); }
/* Side panels are narrow themselves and also take the main transcript's safe
   side margin. In both places, actions follow the narrow-desktop contract at
   every viewport width. */
#thread-messages .acts,
#chat.thread-open #messages .acts,
#chat.needs-open #messages .acts {
  top: 0;
  left: 0;
  margin-left: 0;
  transform: none;
  background: var(--paper-raise);
}
#thread-messages .chips,
#chat.thread-open #messages .chips,
#chat.needs-open #messages .chips { width: 100%; }
#thread-messages .line.selected .acts,
#thread-messages .acts:focus-within,
#chat.thread-open #messages .line.selected .acts,
#chat.thread-open #messages .acts:focus-within,
#chat.needs-open #messages .line.selected .acts,
#chat.needs-open #messages .acts:focus-within {
  position: static;
  display: flex;
  flex: 0 1 auto;
  margin-top: var(--s1);
  opacity: 1;
  pointer-events: auto;
  flex-wrap: wrap;
  white-space: normal;
}
/* Same padding, same border, same paper as #promptbar. The thread bar used to
   sit on --paper-sink, which read as a different control on a different
   surface. The form, input, icons and send button come from the .composer-*
   rules in section 12 — there is nothing composer-shaped left to state here. */
#thread-promptbar {
  padding: var(--s3) var(--s4) calc(var(--s3) + env(safe-area-inset-bottom));
  border-top: var(--bd-strong) solid var(--rule);
  background: var(--paper-raise);
}
/* No reserved line. This held 1.4em plus a margin whether or not it had words
   in it, which is where the thread composer's extra height came from; the
   channel bar's own status lines are hidden until they say something. */
#thread-reply-ctx[hidden] { display: none; }
#thread-reply-ctx {
  margin-bottom: var(--s1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--t-xs); color: var(--ink-2);
}

/* ── 17. settings ─────────────────────────────────────────────────────────── */

#drawer .sheet-body { gap: 0; padding-top: 0; }
/* One pane at a time. The panel used to be thirteen sections in a single
   24rem column, which is a wall you scroll rather than a panel you read. */
.drawer-part {
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}

/* Tabs, sitting on the same paper as the pane they open, so the selected one
   reads as joined to its content rather than floating above it. The bar keeps
   a rule along its bottom and the selected tab paints over that rule — which
   is the whole trick, and the reason this stopped looking like a stray label
   row above unrelated text. */
.drawer-tabs {
  flex: none;
  display: flex;
  gap: 0;
  padding: 0 var(--s3);
  background: var(--paper-sink);
  border-bottom: var(--bd) solid var(--rule);
  overflow-x: auto;
  scrollbar-width: none;
}
.drawer-tabs::-webkit-scrollbar { display: none; }
.drawer-tabs button {
  flex: none;
  border: 0;
  border-bottom: var(--bd) solid transparent;
  margin-bottom: calc(var(--bd) * -1);   /* overlap the bar's own rule */
  border-radius: 0;
  background: none;
  padding: var(--s2) var(--s3);
  font: 600 var(--t-xs) var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-2);
  white-space: nowrap;
}
.drawer-tabs button:hover { color: var(--ink); }
.drawer-tabs button[aria-selected="true"] {
  color: var(--ink);
  background: var(--paper);
  border-bottom-color: var(--paper);
}
.drawer-tabs button[hidden] { display: none; }
/* Rule 3: the selected tab is not carried by colour alone — it is the one
   standing on the pane's own paper, with the rule beneath it cut away. */

/* The workspaces you are actually in, which the section named for the plural
   never showed. */
.ws-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s1); }
.ws-row {
  display: flex; align-items: baseline; gap: var(--s2);
  padding: var(--s2) var(--s3);
  border: var(--bd) solid var(--rule-soft);
  background: var(--paper-raise);
}
.ws-row.is-here { border-color: var(--rule); }
.ws-row .ws-name { font-weight: 600; color: var(--ink); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ws-row .ws-addr { margin-left: auto; font: 400 var(--t-xs) var(--mono); color: var(--ink-2); white-space: nowrap; }
.ws-row .ws-you {
  font: 600 var(--t-xs) var(--mono); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--human-ink);
}
.ws-role-list { display: flex; flex-direction: column; gap: var(--s1); }
.ws-role-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  border: var(--bd-hair) solid var(--rule-soft);
}
.ws-role-member { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ws-role-kind { color: var(--ink-2); font: 400 var(--t-xs) var(--mono); }
.ws-role-row select { width: auto; min-width: 7em; }
.ws-new { border-top: var(--bd-hair) solid var(--rule-soft); padding-top: var(--s3); }
.ws-new > summary {
  cursor: pointer; font-weight: 600; color: var(--ink);
  list-style: none;
}
.ws-new > summary::-webkit-details-marker { display: none; }
.ws-new > summary::before { content: "+ "; color: var(--ink-2); }
.ws-new[open] > summary::before { content: "− "; }
.ws-new > *:not(summary) { margin-top: var(--s3); }
.field-note { margin-top: var(--s1); font: 400 var(--t-xs) var(--mono); color: var(--ink-2); }
.field-note.is-bad { color: var(--danger); }

/* A native file widget in a hand-drawn panel. The composer already hides its
   input behind a real control; these two were the only ones left showing. */
.filepick { display: inline-flex; }
.filepick input[type="file"] {
  position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;
}
.filepick > span {
  display: inline-block;
  border: var(--bd) solid var(--rule);
  background: var(--paper-raise);
  padding: 0.45em 0.9em;
  font: 600 var(--t-sm) var(--mono);
  color: var(--ink);
  cursor: pointer;
  max-width: 14em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.filepick input:focus-visible + span { outline: 3px solid var(--human); outline-offset: 2px; }
.filepick > span:hover { background: var(--paper-sink); }

.drawer-part[hidden] { display: none; }
.drawer-sec {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s4) 0;
  border-bottom: var(--bd-hair) solid var(--rule-soft);
  scroll-margin-top: 3rem;
}
.drawer-sec[hidden] { display: none; }
.drawer-sec:last-child { border-bottom: none; }
.drawer-sec h2 { font: 800 var(--t-lg)/var(--lh-tight) var(--display); }
.drawer-sec h3 { font: 700 var(--t-md) var(--body); }
.drawer-sec > p { color: var(--ink-2); font-size: var(--t-sm); line-height: 1.5; }
.drawer-sec button { align-self: flex-start; }
.drawer-sec.flash { background: var(--human-wash); box-shadow: -0.6rem 0 0 var(--human-wash), 0.6rem 0 0 var(--human-wash); }

/* THE MESSAGE YOU WERE SENT TO.

   `.flash` was added to a line by the Needs you route, by a search result, by
   a reply breadcrumb and by a deep link — and no `.line.flash` rule existed in
   2,142 lines of CSS. Every one of those routes scrolled somewhere and marked
   nothing, which is the worst possible answer to "take me there": it lands you
   in a wall of text with no idea which line was the point.

   An overlay pseudo-element rather than a rule on the line itself, so it
   COMPOSES with whatever state the line already has: on a mention the wash
   deepens and the ink bar doubles; on an opaque decision card it is the only
   thing that can mark it at all; on a selected line its inset shadow is
   untouched. inset:0 and pointer-events:none mean it can neither reflow nor
   swallow a click.

   The static marker is the BASE and the fade is the enhancement, so under
   reduced motion it simply persists for the 2s the class is present — no
   reduce exception needed. */
/* The way back. The room moving on while you read something is normal and
   the scroll position is yours — but saying nothing about it is not, and there
   was no jump-to-latest control anywhere in the client. Two labels, because
   someone who scrolled up to quote a line and someone the room has left behind
   are not in the same situation. */
#to-latest {
  position: absolute;
  grid-area: main;
  justify-self: center;
  align-self: end;
  margin-bottom: var(--s4);
  z-index: 6;                 /* over .acts, which sits at 5 */
  background: var(--paper-raise);
  box-shadow: var(--shadow-sm);
}

/* THE ROOM, NOT YET ARRIVED.

   The transcript's own two-column grid at the real measure, so history
   resolves INTO the shape it will occupy rather than replacing a headline that
   said the room was empty. Square, static, no shimmer: a sweeping gradient is
   a slot machine's idle animation and this room does not blink at people. */
.ghosts { display: flex; flex-direction: column; gap: var(--lead); padding-top: var(--lead); }
.ghost {
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 1fr);
  gap: 0 var(--s3);
  max-width: var(--read-w);
}
.ghost-mark { width: 1.75rem; height: 1.75rem; background: var(--paper-sink); border-radius: 50%; }
.ghost-lines { grid-column: 2; display: flex; flex-direction: column; gap: var(--s2); }
.ghost-bar { height: 0.7em; background: var(--paper-sink); }
.ghost-head { width: 5.5rem; height: 0.6em; }

.history-loading {
  max-width: var(--read-w);
  padding-bottom: var(--s2);
  color: var(--ink-2);
  font: 500 var(--t-xs) var(--body);
}

@keyframes line-found { from { opacity: 1; } to { opacity: 0; } }
.line.flash::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--human-wash);
  border-left: var(--bd-strong) solid var(--human-ink);
}
@media (prefers-reduced-motion: no-preference) {
  .line.flash::after { animation: line-found 1.6s ease-out forwards; }
}
.check { display: flex; align-items: center; gap: var(--s2); cursor: pointer; padding: 0.15em 0; }
.inline-row { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; }
.inline-row input[type="text"] { width: auto; flex: 1 1 9em; min-width: 7em; }
.dndrow { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }
.dndrow input[type="time"] { width: auto; }
.dndrow label { font-size: var(--t-sm); color: var(--ink-2); }

.list-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s2); padding: var(--s2) 0;
  border-bottom: var(--bd-hair) solid var(--rule-soft);
  font-size: var(--t-sm);
}
.list-row:last-child { border-bottom: none; }
.list-row .list-main { min-width: 0; }
.list-row .list-meta { color: var(--ink-2); font-size: var(--t-xs); }
.list-row button { font-size: var(--t-xs); padding: 0.3em 0.6em; }

.grant-row, .pin-row {
  display: flex; flex-direction: column; gap: var(--s1);
  border: var(--bd) solid var(--rule);
  background: var(--paper-raise);
  padding: var(--s2) var(--s3);
}
.grant-head, .pin-meta { color: var(--ink-2); font-size: var(--t-sm); }
/* Four more rows written against rules that were never added — the same bug as
   `.flash`, which painted nothing for every jump-to-message in the product.
   They inherit the shared row shape rather than each inventing one. */
.device-row, .emoji-row, .sndrow-item {
  display: flex; align-items: center; gap: var(--s2);
  border: var(--bd) solid var(--rule);
  background: var(--paper-raise);
  padding: var(--s2) var(--s3);
}
.emoji-row .dim, .sound-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.device-row > :last-child, .emoji-row > :last-child { margin-left: auto; }
.grant-meta { color: var(--ink-2); font-size: var(--t-xs); }
.grant-plain { display: flex; flex-direction: column; gap: var(--s2); }
/* A checkbox and its words are one target, and were laid out as two. */
.checkline { display: flex; align-items: center; gap: var(--s2); cursor: pointer; }
.grant-from { color: var(--ink); font-weight: 600; }
.grant-label-txt { color: var(--ink); }
.grant-acts, .pin-acts { display: flex; gap: var(--s2); flex-wrap: wrap; }
.grant-acts button, .pin-acts button {
  border-color: transparent; background: none; padding: 0.15em 0;
  font: 600 var(--t-sm) var(--mono); color: var(--ink-2);
}
.grant-acts button:hover, .pin-acts button:hover { background: none; color: var(--ink); text-decoration: underline; }
.grant-acts .grant-reveal { color: var(--ink-2); }
.grant-secret-box {
  font: 400 var(--t-md)/1.5 var(--mono);
  color: var(--ink); background: var(--paper);
  border: var(--bd) solid var(--human-ink);
  padding: var(--s2);
  white-space: pre-wrap; word-break: break-all; user-select: all;
}
.pin-row .result-body { white-space: normal; }

/* the principal's agent panel — facts as phrases, never as a score */
#agent-roster { display: flex; flex-direction: column; gap: var(--s2); }
#agent-roster > .eyebrow { margin-top: var(--s3); }
#agent-roster > .eyebrow:first-child { margin-top: 0; }
.agent-card {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  border: var(--bd) solid var(--rule);
  background: var(--paper-raise);
  padding: var(--s3);
}
.agent-head { display: flex; align-items: center; gap: var(--s3); min-width: 0; }
.agent-names { min-width: 0; display: flex; flex-direction: column; }
.agent-handle { font-weight: 700; color: var(--agent-ink); }
.agent-for { color: var(--ink-2); font-size: var(--t-sm); }
.agent-facts { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s2); }
.agent-fact {
  border: var(--bd-hair) solid var(--rule-soft);
  padding: 0 0.5em;
  font: 500 var(--t-xs)/1.7 var(--mono);
  color: var(--ink-2);
}
.agent-fact.is-live { color: var(--agent-ink); border-color: var(--agent-ink); }
.agent-fact.is-quiet { color: var(--ink-2); border-style: dashed; }
.agent-fact.is-warn { color: var(--danger); border-color: var(--danger); }
.agent-acts { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.agent-acts .agent-revoke { font-size: var(--t-xs); padding: 0.3em 0.7em; }

/* ── 18. profile ──────────────────────────────────────────────────────────── */

#profile-pop[hidden] { display: none; }
#profile-pop {
  max-height: calc(100dvh - 1rem);
  overflow-y: auto;
  position: fixed;
  z-index: 70;
  width: min(21rem, calc(100vw - 1rem));
  background: var(--paper-raise);
  border: var(--bd) solid var(--rule);
  box-shadow: var(--shadow);
  outline: none;
}
.prof-head { display: flex; align-items: center; gap: var(--s3); padding: var(--s3); border-bottom: var(--bd) solid var(--rule); }
.prof-names { min-width: 0; }
.prof-handle { font-weight: 700; display: block; }
.prof-kind { color: var(--ink-2); font: 600 var(--t-xs) var(--mono); text-transform: uppercase; letter-spacing: 0.08em; }
.prof-body { padding: var(--s3); display: flex; flex-direction: column; gap: var(--s1); }
.prof-row { color: var(--ink-2); font-size: var(--t-sm); line-height: 1.5; }
.prof-row strong { color: var(--ink); font-weight: 600; }
.prof-acts { display: flex; gap: var(--s2); padding: 0 var(--s3) var(--s3); flex-wrap: wrap; }
.prof-body.errline { margin: var(--s3); }

/* #112: the member's OWN words, ruled off from the facts above them. The rule
   and the byline are the whole point — a claim that looks like a fact is the
   failure this section exists to avoid. */
.prof-said { margin-top: var(--s2); padding-top: var(--s2); border-top: var(--bd) solid var(--rule); display: flex; flex-direction: column; gap: var(--s1); }
.prof-saidhead { color: var(--ink-2); font: 600 var(--t-xs) var(--mono); text-transform: uppercase; letter-spacing: 0.08em; }
.prof-said a { color: var(--ink); word-break: break-all; }
.prof-field { display: block; margin-bottom: var(--s2); color: var(--ink-2); font-size: var(--t-sm); }
.prof-field input, .prof-field textarea { display: block; width: 100%; margin-top: var(--s1); }

/* ── 19. expression picker ──────────────────────────────────────────────────
   Signal's useful idea is not its paint. It is one emoji vocabulary reached
   from either a message or the composer: quick choices first, search and the
   full set behind them. This is Bazaar's square-paper version of that shape. */

#emoji-picker[hidden] { display: none; }
#emoji-picker {
  position: fixed;
  z-index: 60;
  display: flex; flex-direction: column; gap: var(--s2);
  background: var(--paper-raise);
  border: var(--bd) solid var(--rule);
  box-shadow: var(--shadow);
  padding: var(--s3);
  width: 22rem;
  max-height: min(30rem, calc(100dvh - 1rem));
  max-width: calc(100vw - 1rem);
}
#emoji-picker .picker-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s2); }
#emoji-picker-title { font: 700 var(--t-md) var(--display); }
#emoji-picker-mode, .picker-label { font-size: var(--t-xs); }
#expression-tabs { display: flex; border-bottom: var(--bd-hair) solid var(--rule); }
#expression-tabs[hidden] { display: none; }
#expression-tabs button {
  border: none; border-bottom: var(--bd-strong) solid transparent;
  background: none; padding: var(--s1) var(--s3); color: var(--ink-2);
  font: 700 var(--t-sm) var(--mono);
}
#expression-tabs button[aria-selected="true"] { border-bottom-color: var(--human-ink); color: var(--ink); }
#expression-tabs button[hidden] { display: none; }
#emoji-pane, #gif-pane { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: var(--s2); }
#emoji-pane[hidden], #gif-pane[hidden] { display: none; }
#emoji-search, #emoji-free { width: 100%; font-size: var(--t-sm); }
#emoji-groups { flex: 1 1 auto; overflow-y: auto; min-height: 0; }
#emoji-picker .picker-section,
#emoji-picker .picker-group { display: flex; flex-direction: column; gap: var(--s1); }
#emoji-picker .picker-group + .picker-group { margin-top: var(--s2); }
#emoji-picker .picker-label {
  color: var(--ink-2); font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
}
#emoji-picker .picker-row,
#emoji-picker .picker-grid { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: var(--s1); }
#emoji-picker .picker-row button,
#emoji-picker .picker-grid button {
  min-width: 0; min-height: 2.35rem;
  border-color: transparent; padding: 0.15em; font-size: var(--t-lg); background: transparent;
}
#emoji-picker .picker-row button:hover,
#emoji-picker .picker-grid button:hover,
#emoji-picker .picker-row button:focus-visible,
#emoji-picker .picker-grid button:focus-visible { background: var(--paper-sink); }
#emoji-picker .picker-group[hidden],
#emoji-picker .picker-emoji[hidden],
#emoji-picker .picker-custom-sec[hidden] { display: none; }
#emoji-picker .picker-custom[hidden] { display: none; }
#emoji-picker .picker-custom-sec { border-top: var(--bd-hair) solid var(--rule-soft); padding-top: var(--s2); }
#emoji-picker .picker-custom img { width: 1.5rem; height: 1.5rem; }
#emoji-picker .picker-empty { grid-column: 1 / -1; min-height: 2rem; font-size: var(--t-xs); }
#gif-search { width: 100%; font-size: var(--t-sm); }
#gif-privacy { margin: 0; font-size: var(--t-xs); line-height: 1.35; }
#gif-status { min-height: 1.35em; font-size: var(--t-xs); }
#gif-results {
  flex: 1 1 auto; min-height: 8rem; overflow-y: auto;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  align-content: start; gap: var(--s1);
}
.gif-result {
  position: relative; min-width: 0; min-height: 5.5rem; padding: 0;
  overflow: hidden; border-color: transparent; background: var(--paper-sink);
}
.gif-result:hover, .gif-result:focus-visible { border-color: var(--human-ink); }
.gif-result img { display: block; width: 100%; height: 7rem; object-fit: cover; }
.gif-credit {
  display: block; position: absolute; inset: auto 0 0;
  overflow: hidden; padding: 0.2em 0.35em; text-overflow: ellipsis; white-space: nowrap;
  color: var(--media-credit-ink); background: var(--media-credit-bg);
  font: 600 0.66rem var(--mono); text-align: left;
}
#giphy-attribution { display: block; align-self: flex-end; width: 100px; height: 21px; object-fit: contain; }

/* ── 20. the door ─────────────────────────────────────────────────────────── */

#gate {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  min-height: 100dvh;
  padding: var(--s5) var(--s4);
}
.gate-card {
  width: 100%;
  max-width: 30rem;
  padding: clamp(1.5rem, 5vw, 2.5rem);
  background: var(--paper-raise);
  border: var(--bd-strong) solid var(--rule);
  box-shadow: var(--shadow-lg);
}
.gate-logo { font: 800 clamp(2rem, 8vw, 2.75rem)/0.95 var(--display); letter-spacing: -0.03em; }
.gate-tag { color: var(--ink-2); margin: var(--s3) 0 var(--s5); }
.gate-step { display: flex; flex-direction: column; gap: var(--s3); }
.gate-step[hidden] { display: none; }
.gate-step > label { font-weight: 700; }
.gate-step button[type="submit"] { align-self: flex-start; }
.gate-alt { font-size: var(--t-sm); }
.gate-alts { display: flex; flex-direction: column; gap: var(--s1); margin-top: var(--s2); }

/* #80: the provider doors. They sit UNDER the passkey button, quieter than it,
   because the passkey is still the default and the recommended path. The row
   is hidden until the server says at least one provider is on, so an
   unconfigured Worker shows a screen that has not changed at all. */
.provider-row { display: flex; flex-direction: column; gap: var(--s2); margin-top: var(--s3); }
.provider-row[hidden] { display: none; }
.btn-provider { align-self: flex-start; }
.btn-provider[hidden] { display: none; }
.identity-row {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s2) 0;
  border-bottom: var(--bd) solid var(--rule-soft);
}
.identity-row .identity-label { flex: 1; min-width: 0; overflow-wrap: anywhere; }

/* The marks, on the one screen in the first ten minutes where a newcomer has
   nothing to do but read. The mark system is pure CSS, so this costs four
   lines of markup and no script. */
.gate-legend {
  display: flex; flex-direction: column; gap: var(--s3);
  padding: var(--s3);
  border: var(--bd) solid var(--rule-soft);
  background: var(--paper);
}
.gate-legend-row {
  display: flex; align-items: center; gap: var(--s3);
  font-size: var(--t-sm); line-height: 1.5; color: var(--ink-2);
}
.gate-legend-row .mark { flex: none; }
.gate-card #g-status, .gate-card #g-err { margin-top: var(--s3); }
.gate-ws-list { display: flex; flex-direction: column; gap: var(--s2); margin: var(--s3) 0; }
.gate-ws-list a {
  display: flex; align-items: center; gap: var(--s2);
  border: var(--bd) solid var(--rule); background: var(--paper);
  padding: var(--s2) var(--s3); text-decoration: none; color: var(--ink); font-weight: 600;
}
.gate-ws-list a:hover { background: var(--paper-sink); }

/* ── 21. first-run device setup ───────────────────────────────────────────── */

#onboarding[hidden], #onboarding-backdrop[hidden] { display: none; }
#onboarding-backdrop { position: fixed; inset: 0; z-index: 70; background: var(--scrim); }
#onboarding {
  position: fixed;
  z-index: 80;
  top: 50%; left: 50%;
  width: min(30em, calc(100vw - 2em));
  max-height: calc(100dvh - 2em);
  overflow-y: auto;
  transform: translate(-50%, -50%);
  background: var(--paper-raise);
  border: var(--bd) solid var(--rule);
  box-shadow: var(--shadow-lg);
}
.onboarding-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--s2) var(--s3);
  border-bottom: var(--bd) solid var(--rule);
  background: var(--paper-sink);
  font: 600 var(--t-xs) var(--mono); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-2);
}
#onboarding-title { font: 800 var(--t-2xl)/var(--lh-tight) var(--display); letter-spacing: -0.02em; padding: var(--s4) var(--s4) 0; }
.onboarding-step { padding: var(--s4); border-top: var(--bd-hair) solid var(--rule-soft); display: flex; flex-direction: column; gap: var(--s2); align-items: flex-start; }
.onboarding-step:first-of-type { border-top: none; }
.onboarding-step[hidden] { display: none; }
.onboarding-step h3 { font: 700 var(--t-md) var(--body); }
.onboarding-step p { color: var(--ink-2); font-size: var(--t-sm); }
.onboarding-later {
  margin: 0 var(--s4) var(--s4);
  border: none; background: none; padding: 0;
  color: var(--ink-2); font: 600 var(--t-sm) var(--mono);
}
.onboarding-later:hover { background: none; color: var(--ink); text-decoration: underline; }

/* ── 22. invite ───────────────────────────────────────────────────────────── */

.invite-body { padding: var(--s4); display: flex; flex-direction: column; gap: var(--s3); }
#invite-result:empty { display: none; }
#invite-result { display: flex; flex-direction: column; gap: var(--s1); font-size: var(--t-sm); }
#invite-result p { margin: 0; }
#invite-actions { display: flex; gap: var(--s2); flex-wrap: wrap; }

/* ── 23. toast ────────────────────────────────────────────────────────────── */

#update-toast {
  position: fixed;
  bottom: calc(4.5rem + env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  display: flex; gap: var(--s3); align-items: center;
  background: var(--paper-raise);
  border: var(--bd) solid var(--rule);
  box-shadow: var(--shadow);
  padding: var(--s2) var(--s3);
  font-size: var(--t-sm);
}
#update-toast .toast-later { border-color: transparent; color: var(--ink-2); }

/* ── 24. density ──────────────────────────────────────────────────────────
   Compact keeps every structure and shrinks the room. It is a density
   control, not a second design. */

body.compact { font-size: 0.875rem; line-height: 1.5; }
body.compact #messages { padding: var(--s3) var(--s4); }
/* Compact tightens the leading itself, and every gap follows from it. */
body.compact { --lead: calc(var(--t-md) * 1.45); }
body.compact .line { grid-template-columns: 1.25rem minmax(0, 1fr); gap: 0 var(--s2); }
body.compact .line .mark { --mark-size: 1.25rem; border-width: 1.5px; }
body.compact .line.cont { margin-top: 0; }
body.compact .msghead { gap: var(--s2); }
/* Compact is a density control. It does not license a 122-character line. */
body.compact .drawer-sec { padding: var(--s3) 0; }
body.compact .composer-fmtbar { display: none; }
@media (min-width: 761px) {
  /* Density changes one size token. The shell breakpoint remains the sole
     owner of its tracks and areas, including the thread track. */
  body.compact #chat { --chat-rail-w: 13.5rem; }
}

/* A visible floating strip has no generally safe side margin: a timestamp,
   decision, reaction row or future message surface can use it. Keep passive
   hover inert at every desktop width. Selection or direct action focus puts
   the tray in flow, where layout reserves the space it uses. */
@media (min-width: 761px) {
  .acts { top: 0; left: 0; margin-left: 0; transform: none; }
  .chips { width: 100%; }
  .line.selected .acts,
  .acts:focus-within {
    position: static;
    display: flex;
    flex: 0 1 auto;
    margin-top: var(--s1);
    opacity: 1;
    pointer-events: auto;
    flex-wrap: wrap;
    white-space: normal;
  }
}

/* ── 25. wide ─────────────────────────────────────────────────────────────── */

@media (min-width: 1101px) {
  #chat.thread-open {
    grid-template:
      "top  top   top    top"    auto
      "rail needs main   thread" 1fr
      "rail needs prompt thread" auto
      / var(--chat-rail-w) 0 minmax(0, 1fr) minmax(20rem, 30rem);
  }
}

/* ── 26. phone ────────────────────────────────────────────────────────────── */

@media (max-width: 1100px) {
  #thread-pane { position: fixed; inset: 0; z-index: 46; border-left: none; }
  #needs-backdrop { z-index: 47; }
  #thread-head { padding-top: calc(var(--s3) + env(safe-area-inset-top)); }
  /* These controls remain explicit targets inside the otherwise full-card
     action. The drawer is a touch surface, so none can be smaller than 44px. */
  #needs-close,
  #needs-panel .need-acts button {
    min-width: 2.75rem;
    min-height: 2.75rem;
  }
  #needs-panel {
    left: var(--chat-rail-w);
    right: auto;
    width: min(24rem, calc(100vw - var(--chat-rail-w)));
    opacity: 0;
    transform: translateX(calc(var(--s5) * -1));
    transition:
      opacity 0.2s ease,
      transform 0.26s cubic-bezier(.22, 1, .36, 1);
  }
  #chat.needs-open #needs-panel { opacity: 1; transform: translateX(0); }
}

@media (max-width: 760px) {
  #chat {
    grid-template:
      "top"    auto
      "main"   1fr
      "prompt" auto
      / minmax(0, 1fr);
  }
  #topicbar #btn-rail { display: inline-flex; }
  #rail {
    position: fixed;
    top: 0; left: 0; bottom: 0;
    width: min(17.5rem, 84vw);
    z-index: 47;
    transform: translateX(-105%);
    transition: transform 0.18s ease-out;
    border-right: var(--bd-strong) solid var(--rule);
    padding-top: calc(var(--s3) + env(safe-area-inset-top));
  }
  #rail.open { transform: translateX(0); }
  #needs-panel { left: 0; width: 100vw; transform: translateX(-100%); }
  #chat.needs-open #needs-panel { transform: translateX(0); }
  #topicbar { padding-top: calc(var(--s3) + env(safe-area-inset-top)); }
  /* The bar holds seven controls at 375px. The channel name must be the thing
     that gives way: `flex: none` plus `max-width: none` made it refuse to
     shrink, so it ran under the tools instead of ellipsizing. The workspace
     belongs to the rail on a phone. */
  #topic-chan { flex: 0 1 auto; min-width: 0; max-width: none; font-size: var(--t-md); }
  #topic-ws, #topic-topic, .topic-sep { display: none; }

  #messages, #conversations-view { padding: var(--s3) var(--s3) var(--s4); }
  .line { grid-template-columns: 1.5rem minmax(0, 1fr); gap: 0 var(--s2); }

  /* The action strip, on a phone.
     Two earlier attempts are recorded in this file as rejected: `top: 100%`
     put the strip on top of the next message, and `left: 0` slid it under the
     rail. Both kept it absolutely positioned, and that is the mistake — at
     390px there is no free space beside a row of reactions to put it in.
     Column 2 is about 326px here; the strip is ~190px and three chips already
     take more than what is left, so `left: 100%` pushed it past the edge and
     #messages' overflow-x: hidden ate it. The strip was unreachable on the
     device most people read in.

     Keep the hidden strip absolute so it costs no height, but do not use
     display:none: that removes Reply from the keyboard order and leaves no
     focusable control that can satisfy :focus-within. Once tap selection or
     keyboard focus chooses the line, put it in flow on its own row under the
     reactions. It then cannot collide with the next message, the rail, or the
     viewport edge. */
  .acts {
    position: absolute;
    top: 0;
    left: 0;
    margin-left: 0;
    transform: none;
  }
  .chips { width: 100%; }
  .line.selected .acts,
  .acts:focus-within {
    position: static;
    display: flex;
    flex: 0 1 auto;
    margin-top: var(--s1);
    opacity: 1;
    pointer-events: auto;
    /* The base rule forbids wrapping, and must: floating beside the reaction
       row, a wrap resolved max-width against a max-content parent and stacked
       all eight buttons into one 38px column. In flow that cannot happen, and
       nowrap is now the thing doing harm — eight buttons need 372px and column
       two is 326px, so the last one (delete) ran off the right edge. */
    flex-wrap: wrap;
    white-space: normal;
  }
  .line.mentioned { --inset: var(--s2); max-width: none; }
  /* On a phone there is no margin to park in, so the strip sits at the line's
     own right edge and the buttons grow to a real touch target. */
  .acts button { min-width: 2.6rem; min-height: 2.6rem; }
  /* The phone keeps the strip in flow and lets its border follow the wrapped
     controls instead of stretching across the message column. */
  .empty { min-height: 50%; padding: var(--s5) var(--s3); }

  /* the composer takes its own full-width row so a long channel label can
     never squeeze the input into a column that wraps two words */
  .composer-form { flex-wrap: wrap; align-items: center; }
  .composer-form .composer-input-shell { flex: 1 0 100%; order: -1; }
  #prompt-label { max-width: 55%; padding: 0.35em var(--s2); }
  .composer-send { margin-left: auto; }
  #emoji-picker {
    left: var(--s2) !important; right: var(--s2); top: auto !important;
    bottom: calc(var(--s2) + env(safe-area-inset-bottom));
    width: auto; max-height: min(64dvh, 32rem);
  }
  #emoji-picker .picker-row,
  #emoji-picker .picker-grid { grid-template-columns: repeat(7, minmax(0, 1fr)); }
  .ov { top: 0; left: 0; transform: none; width: 100vw; max-height: 100dvh; height: 100dvh; border: none; box-shadow: none; }
  .ov-head { padding-top: calc(var(--s2) + env(safe-area-inset-top)); }
  .sheet { width: 100vw; border-left: none; }
}

/* ── 27. motion ──────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  #rail { transition: none; }
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}

/* ── 27a. the wide viewport ────────────────────────────────────────────────
   At 1920 the text column is 584px inside a 1632px field, and the topic bar,
   the day rules and the composer all draw straight across the other 1048px.
   The void is loud because nothing acknowledges it. This is the landing page's
   own move — a bounded sheet on a darker ground.

   NOT solved by widening the measure: a 900px column at 15px Archivo is 122
   characters per line. The surplus is a margin problem, not a measure one. */
@media (min-width: 1500px) {
  body { background: var(--paper-sink); }
  #chat {
    max-width: 1500px;
    margin-inline: auto;
    border-inline: var(--bd-strong) solid var(--rule);
  }
}

/* ── 28. print ────────────────────────────────────────────────────────────
   A transcript is a record. Someone will eventually print one. */

@media print {
  #rail, #promptbar, #topic-tools, .acts, .scrim, .sheet, .ov, #emoji-picker { display: none !important; }
  #chat { display: block; height: auto; }
  #messages { overflow: visible; }
  /* The other deliberate literal: paper is white and toner is black, whatever
     scheme the screen was in. Printing --paper would empty a cartridge. */
  body { background: #fff; color: #000; }
}

/* DM replies stay beside the main composer. */
#dm-reply-ctx { display: flex; align-items: center; gap: .5rem; min-width: 0; }
#dm-reply-ctx[hidden] { display: none; }
#dm-reply-ctx > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#dm-reply-ctx > button { flex-shrink: 0; }

/* Reserve the actual board while lazy assets and its live snapshot load. */
.grass-name, .grass-command { color:var(--grass-deep);font-weight:800;font-family:var(--display);text-decoration:underline wavy;text-decoration-thickness:2px;text-underline-offset:4px; }
.grass-name::before { content:"♧ "; }

/* Host slot only. Game layout and palette belong to the lazy package. */
#grass-game { grid-area:main; z-index:2; min-width:0; min-height:0; overflow:hidden; }
#grass-game[hidden] { display:none; }
#grass-game[aria-busy="true"] { background:var(--paper); padding:var(--s2); }
